Skip to content

baukit 0.5.0

Choose a tag to compare

@PatrickKoss PatrickKoss released this 28 Sep 19:17
· 402 commits to main since this release

baukit v0.5.0

One release train for the Rust crates, TypeScript packages, and templates.

Summary

baukit 0.5.0 carries the cross-product feature plan (20 items) and its follow-up wave. It has many breaking changes, because no product is live yet and nothing kept a compatibility shim. Read the breaking list before upgrading a product.

New

  • baukit-egress: a guarded outbound HTTP client that blocks private and special-purpose addresses, with a 300-second Retry-After cap by default.
  • baukit-push: a push device registry with token rotation, eviction, dead-token invalidation, daily delivery claims, and deferred Expo receipt polling backed by PostgreSQL.
  • baukit-auth: a PostgreSQL API token store with grants.
  • baukit-core: signed media grants behind the media-grants feature, plus an njs edge verifier in deploy/media-grants and shared vectors in fixtures/media-grants.
  • @baukit/notifications-core and @baukit/notifications-expo: owned local notification planning and replacement that never cancels other features' notifications.
  • @baukit/localization-core: resolveZonedLocalTime with explicit gap and fold policies.
  • @baukit/data-contracts-expo-sqlite: ExpoSqliteConnection, a queued raw-SQL connection with foreign keys enforced inside transactions, plus a Node test driver and a migration conformance suite.
  • Replay-safe mutations, ETag revision preconditions, server purge horizons, and OpenAPI error responses.
  • Templates: auth products get a Redis service in Compose, stricter coverage floors, and Keycloak stack e2e helpers.

Breaking

  • Wire names are camelCase: the error envelope's requestId, nextCursor, event envelope fields, ledgerEntryId, ConnectionStatus fields, rate-limit retryAfter, and acceptLanguage.
  • baukit-http responses default to Cache-Control: private, no-store, and the CORS defaults now expose and accept more headers.
  • classify_http_status treats 425 Too Early as retryable.
  • baukit-jobs: JobStore::claim takes job types.
  • baukit-push: tokens are DeviceToken, and Accepted carries a ticket ID.
  • baukit-auth: bearer challenges use error_description, and scope is mapped by default.
  • Several error enums gained variants.

The sections below list every change per crate and package.

Verification

Every step of the local CI mirror passed on the release candidate: make ci, all Rust tests including Docker-gated ones (707), cargo deny, MSRV 1.95, all seven generated fixture flavors with coverage floors, the MCP fixture gate, the Android compile gate, Expo SQLite on an Android emulator (30 cases), and the Hermes vector and notifications checks. iOS and physical devices were not tested.

Rust crates

baukit-auth

Added

  • Add the sqlx-postgres feature with PostgresApiTokenStore, an
    ApiTokenStore over the api_tokens table in
    POSTGRES_API_TOKENS_MIGRATION_SQL. The constant is available without the
    feature. Grants go into the same INSERT as the digest.
    with_active_token_limit counts and inserts under a per-owner advisory lock.
    touch_last_used never moves last_used_at backwards, and revoke matches
    the owner. Products copy the migration and add their own owner foreign key.
  • Add erase_owner_api_tokens for owner erasure without a cascading foreign
    key, and purge_inactive_api_tokens for batched retention of revoked and
    expired tokens. Both need the sqlx-postgres feature.
  • Add opaque API token grants. NewApiToken::with_grants sets them,
    ApiTokenRecord::grants and ApiToken::grants carry them, and
    Principal::grants returns them for API-token principals. Each grant is an
    RFC 6749 scope token of at most MAX_API_TOKEN_GRANT_LENGTH (128) bytes, and
    a token carries at most MAX_API_TOKEN_GRANTS (64). Anything else fails with
    the new ApiTokenError::InvalidGrants.
  • Add Principal::scopes, read from the verified scope claim by default.
    PrincipalClaimMapping::scope_claim reads another claim. A space-delimited
    string and an array of strings both work.
  • Add PrincipalClaimMapping::profile_claims, Principal::profile_claims,
    Principal::profile_claim, and ProfileClaim for caller-selected verified
    string and boolean claims such as email and email_verified.
  • Add ClerkVerifier::with_audiences and WorkOsVerifier::with_audiences for
    products that need an aud check. Both adapters still skip the check by
    default. Add with_profile_claims and issuer to both, and
    OidcVerifier::issuer.
  • Add IssuerVerifier and MultiIssuerVerifier::from_verifiers, so one
    multi-issuer verifier routes OIDC, Clerk, and WorkOS tokens.

Changed

  • Break: ApiToken, ApiTokenRecord, and NewApiToken gain a public
    grants: BTreeSet<String> field. Struct literals must set it. Store adapters
    must persist ApiTokenRecord::grants in the same write as the digest and
    return them on ApiToken.
  • Break: ApiTokenError gains InvalidGrants. Exhaustive matches must handle
    it.
  • Break: PrincipalClaimMapping::new() and default() now map scope, so
    OIDC, Clerk, and WorkOS verification reads it. A token whose scope is
    neither a string, an array of non-empty strings, nor null fails with
    InvalidPrincipalContext, where it used to verify.
  • Break: AuthRejection challenges use the RFC 6750 error_description
    parameter instead of the nonstandard hint. Expired tokens now get
    Bearer error="invalid_token", error_description="expired" and every other
    rejected token gets error_description="invalid". Clients that parsed
    hint= must read error_description=.
  • Break: ApiTokenPolicyRejection::with_detail accepts camelCase detail names only, such as
    activeCount, because they become error details keys. A snake_case name such as
    active_count now returns InvalidDetailName. Codes stay snake_case.

baukit-config

  • No component-specific changes in this train.

baukit-core

Added

  • Add the export module with encode_csv, CsvCell, CsvOptions, and CsvEncodeError. It
    writes RFC 4180 CSV with CRLF separators and prefixes an apostrophe to text cells that start like
    a spreadsheet formula; CsvOptions::without_formula_neutralization turns that off and
    CsvOptions::with_byte_order_mark adds U+FEFF. CsvCell::Numeric holds a JSON-grammar number
    that is written unchanged, so -5 stays a number. The module uses only std and thiserror, so
    default dependencies are unchanged. It passes the same vectors as @baukit/data-contracts/export.
  • Add the optional pagination feature and the pagination module with Cursor, Page,
    PageKey, PageParams, PaginationError, DEFAULT_PAGE_LIMIT, and MAX_PAGE_LIMIT, moved
    from baukit-http so domain crates can use them without Axum. The feature adds base64,
    ring, and uuid; the default build keeps its three dependencies.
  • Add pagination::MAX_CURSOR_BYTES (4096). Breaking: Cursor::decode rejects longer input with
    PaginationError::InvalidCursor before decoding, and Cursor::encode returns the same error
    instead of issuing a longer cursor.
  • Add the optional media-grants feature and the media_grant module with MediaGrantKey,
    MediaGrantKeyRing, MediaGrant, MediaGrantRequest, VerifiedMediaGrant,
    MediaGrantKeyError, MediaGrantError, signing_input, and valid_media_path. It signs and
    verifies HMAC-SHA256 playback grants with a current and an optional previous key, compares
    signatures in constant time, and redacts key material from Debug. The feature adds base64,
    ring, and zeroize. It passes fixtures/media-grants/vectors-v1.json together with the njs
    verifier in deploy/media-grants.

Changed

  • Break: pagination::Page serializes next_cursor as nextCursor.

baukit-credential-vault

  • No component-specific changes in this train.

baukit-egress

Added

  • First release of baukit-egress, a guarded outbound HTTP client for
    destinations that users supply.
  • GuardedClient resolves through the Resolver port, rejects the lookup when
    any answer fails the AddressPolicy, connects only to the checked answers,
    never follows redirects, ignores proxy settings, and bounds the lookup, the
    connect, the request, and the response body. EgressRequest and
    EgressResponse carry the request and the read body.
  • AddressPolicy with PublicOnly and AllowLoopback, and
    is_public_address, following the IANA special-purpose registries. IPv4-mapped
    addresses and 64:ff9b::/96 are judged by their embedded IPv4 address.
  • Resolver, ResolveFuture, ResolveError, SystemResolver,
    StaticResolver, and resolve_destination.
  • validate_destination checks scheme, user info, fragment, host, and address
    literals without a lookup.
  • EgressError with code(), retry_class(), and is_retryable().
    Non-2xx statuses are classified by baukit_http::classify_http_status.
    DestinationRejection names each refused URL shape. No error carries the URL.
  • EgressOptions with EgressOptionsError: 3 s lookup, 5 s connect, 10 s
    request, and 1 MiB body by default.
  • EgressOptions::with_max_retry_after and max_retry_after cap the
    Retry-After delay in EgressError::Status at 300 s by default. A zero cap
    returns EgressOptionsError::ZeroRetryAfterCap.
  • Shared vectors in fixtures/egress/address-policy-v1.json.

Changed

  • Break: 425 Too Early is now RetryClass::Unavailable, following
    baukit_http::classify_http_status.
  • Break: a Retry-After delay above the cap is clamped instead of reported
    as sent.

baukit-events

Changed

  • Break: EventEnvelope serializes camelCase field names while staying schema version 1:
    event_id, user_id, occurred_at, source_app, and schema_version are now eventId,
    userId, occurredAt, sourceApp, and schemaVersion. type and payload are
    unchanged. Payload keys in the shared fixtures are camelCase.
  • Break: IngestOutcome serializes ledger_entry_id as ledgerEntryId.

baukit-http

Added

  • HttpOptions::with_additional_exposed_headers adds response headers to the CORS exposed set,
    next to with_additional_allowed_headers. Invalid names return
    HttpOptionsError::InvalidHeaderName and duplicates are ignored.
  • ResponseCachePolicy and HttpOptions::with_response_cache_policy control the default
    Cache-Control header. ResponseCachePolicy::HandlerOwned turns the default off.
  • Strong-ETag revision preconditions. RevisionEtag formats "<prefix><revision>" ETags and
    parses exactly one strong If-Match validator through required_if_match or
    optional_if_match. Revision holds a revision in 0..=i64::MAX. InvalidIfMatch names each
    rejected form: repeated header, empty, non-ASCII, *, weak, list, malformed, prefix mismatch,
    non-canonical revision, and out-of-range revision. PreconditionError converts into ApiError
    as 428 precondition_required, 400 invalid_if_match with details.reason, or 412
    precondition_failed with details.currentRevision when the stored revision is known.
    ensure_current_revision builds the 412 from an expected and a current revision.
    RevisionOutOfRange converts into a 500. Shared vectors live in
    fixtures/etag-preconditions/vectors-v1.json.
  • Idempotency-Key parsing. IdempotencyKeyRule::new(min, max) and try_new set per-route
    bounds up to MAX_IDEMPOTENCY_KEY_BYTES (255) over visible ASCII, and required or optional
    read the header from a HeaderMap. IdempotencyKey hides its value from Debug.
    InvalidIdempotencyKey names each rejected form: repeated header, empty, invalid character, too
    short, and too long. IdempotencyError converts into ApiError as 400
    idempotency_key_required, 400 invalid_idempotency_key with details.reason, 409
    idempotency_key_reused, or 409 idempotency_key_in_progress. IDEMPOTENCY_KEY and the four
    code constants are exported.
  • RetryHeaderOptions::with_max_retry_after clamps parsed Retry-After and vendor delays to a
    maximum. The default stays uncapped, so classify_http_status still reports what the upstream
    sent.

Changed

  • Breaking: the default CORS exposed set now also contains Retry-After, RateLimit-Limit,
    RateLimit-Remaining, and RateLimit-Reset, the headers baukit-ratelimit emits. Browsers can
    read them on responses that pass through finalize or layers.
  • Breaking: every response from finalize or layers gets Cache-Control: private, no-store
    unless the handler or an inner layer already set Cache-Control. This includes error, timeout,
    panic, 404, and preflight responses. Set ResponseCachePolicy::HandlerOwned to keep the previous
    behavior.
  • Breaking: the default CORS exposed set now also contains ETag and Location, and the default
    allowed request headers now contain If-Match.
  • Breaking: the default allowed request headers now contain Idempotency-Key.
  • The HttpOptionsError::InvalidHeaderName message now reads invalid CORS header name, because
    it covers exposed headers too.
  • baukit-http depends on baukit-core with the pagination feature and no longer depends on
    base64 or ring directly.
  • Break: the error envelope field request_id is now requestId, through the re-exported
    ErrorBody. This covers every ApiError, extractor rejection, timeout, panic, body-limit,
    404, and 405 response.
  • Break: the RequestLocale rejection detail key accept_language is now acceptLanguage.
  • Break: Page responses carry nextCursor instead of next_cursor, through baukit-core.
  • Break: classify_http_status maps 425 Too Early to RetryClass::Unavailable instead of
    Permanent, so it is retryable. RFC 8470 section 5.2 lets a client retry once the request is no
    longer sent as early data.

Removed

  • Breaking: the pagination module and the root re-exports of Cursor, Page, PageKey,
    PageParams, PaginationError, DEFAULT_PAGE_LIMIT, and MAX_PAGE_LIMIT. They moved to
    baukit_core::pagination with no re-export at the old path. From<PaginationError> for ApiError
    stays in this crate.

Migration

  • Add baukit-core with features = ["pagination"] to every crate that imports the pagination
    types, and replace baukit_http::{Cursor, Page, ...} and baukit_http::pagination::... with
    baukit_core::pagination::....
  • Delete product middleware that overwrites Access-Control-Expose-Headers to add Retry-After
    or RateLimit-*. Pass any other product headers to with_additional_exposed_headers.
  • Delete product middleware that sets Cache-Control: private, no-store or no-store on every
    response. Routes that need a different policy set Cache-Control in the handler, which wins.
  • Apply finalize after authentication and rate-limit layers so their rejections carry CORS
    headers.
  • Remove etag, location, and if-match from product with_additional_exposed_headers and
    with_additional_allowed_headers calls; duplicates are ignored, so leaving them is harmless.
  • Replace product If-Match parsers and ETag formatters with RevisionEtag, keeping the product's
    prefix. Clients see 400 invalid_if_match, 412 precondition_failed, and 428
    precondition_required in place of product-specific codes.
  • Remove idempotency-key from product with_additional_allowed_headers calls. Replace product
    Idempotency-Key parsers with an IdempotencyKeyRule whose bounds keep every key clients send
    today, and map key reuse to IdempotencyError::Reused. Clients see 400
    idempotency_key_required, 400 invalid_idempotency_key, and 409 idempotency_key_reused in
    place of product-specific codes.
  • Clients that stop on RetryClass::Permanent now retry a 425. Clients that schedule their own
    retries from RetryClass::RetryAfter should set with_max_retry_after.

baukit-integrations

Changed

  • Break: ConnectionStatus serializes camelCase field names: last_success_at,
    last_attempt_at, last_error_code, next_retry_at, and failed_attempts are now
    lastSuccessAt, lastAttemptAt, lastErrorCode, nextRetryAt, and failedAttempts.
    ConnectionHealth values stay snake_case. Stored JSON written by an older release no
    longer deserializes.

baukit-jobs

Changed

  • Breaking: JobStore::claim takes a job_types: &[&str] argument after
    worker_id and claims only pending rows or expired leases whose job_type is
    in that set. An empty set, or a blank or overlong entry, returns
    StoreError::InvalidInput. Custom stores must add the parameter and apply the
    filter; direct callers of PostgresJobStore::claim must pass the types they
    handle.
  • WorkerRunner passes its handler's job_types() to every claim. Runners with
    disjoint handlers can share one outbox. A type that no running handler
    declares stays pending and shows in worker_queue_oldest_age_seconds.
  • JobStore::ready on PostgresJobStore probes the filtered claim shape.

Added

  • Added migrations/0003_baukit_jobs_claim_by_type.sql and
    POSTGRES_MIGRATION_0003_SQL, which rebuild job_outbox_claim_idx on
    (job_type, run_after, created_at, id) for pending rows.

Migration

  • Add the 0003 migration after the existing baukit-jobs migrations. Without it,
    claims stay correct but scan pending rows of other types.
  • A worker whose handler did not declare every type it expected to run now
    leaves the undeclared types pending. Add them to job_types() or start a
    runner whose handler declares them.

baukit-openapi

Added

  • ErrorResponseRules documents error responses and response headers from product rules. A
    status rule pairs an OperationCondition (Always, Secured, HasRequestBody,
    HasPathParameter, UnsafeMethod, HasIdempotencyKey) with a status and a description, and
    adds a response with the shared error envelope where the operation lacks that status. A header
    rule adds a header to the responses a ResponseSelector picks. standard_headers() adds
    X-Request-Id, Retry-After on 429, and WWW-Authenticate on 401. apply covers every
    operation and apply_where takes a path and method filter. REQUEST_ID_HEADER,
    RETRY_AFTER_HEADER, WWW_AUTHENTICATE_HEADER, request_id_header, retry_after_header, and
    www_authenticate_header are public.
  • document_if_match documents a revision precondition on an operation: a required or optional
    If-Match header parameter, plus 400, 412, and, when required, 428 error responses with the
    shared envelope. document_etag adds the ETag header to inline 2xx responses.
    if_match_parameter, etag_header, IfMatchRequirement, IF_MATCH_HEADER, ETAG_HEADER,
    PRECONDITION_REQUIRED_CODE, PRECONDITION_FAILED_CODE, and INVALID_IF_MATCH_CODE are public.
  • Add a camelCase naming check. check_camel_case_names(&openapi, exemptions) returns
    SchemaError with one NamingViolation (JSON pointer, name, NameKind) per property or
    path or query parameter name that is not lower camelCase, and assert_camel_case_names
    panics with the list. find_naming_violations runs the same walk on a serde_json::Value,
    and is_camel_case exposes the rule. The walk skips enum and const values, defaults,
    examples, discriminator mappings, x- extensions, and header and cookie parameters. The
    exemption list is for standard-defined names such as OAuth 2.0 access_token.

Changed

  • Break: ErrorBody serializes request_id as requestId. The documented error envelope is
    now { "error": { "code", "message", "requestId", "details" } }.
  • Break: SchemaErrorKind gains the Naming variant. Exhaustive matches must handle it.

baukit-ops

  • No component-specific changes in this train.

baukit-push

Added

  • Add the DeviceRegistry port with register, rotate, unregister,
    list_for_owner, invalidate, and erase_owner, plus the provided
    invalidate_dead_tokens, which removes every DeviceNotRegistered token
    from a send's outcomes in one call. A token registered again after the send
    survives. Registering a token another owner holds moves it. Each owner keeps
    at most DEFAULT_DEVICES_PER_OWNER (10) devices; a registration over the cap
    evicts the least recently registered, tie-broken by created_at and then the
    token, and never the device being registered. rotate removes the
    predecessor first, so it never evicts another device.
  • Add DeviceToken, whose Debug output is redacted, DeviceTimeZone,
    DevicePlatform, DeviceRegistration, RegisteredDevice,
    RegistrationOutcome, dead_tokens, PushValidationError,
    PushStoreError, PushStoreFuture, MAX_DEVICE_TOKEN_LENGTH (512), and
    MAX_DEVICE_TIME_ZONE_LENGTH (64). No error text carries a token.
  • Add the DeliveryClaimStore port with claim and release, keyed by
    DeliveryClaim (owner, local date, DeliveryKind), for scheduled senders.
    It is separate from the registry, so event-driven senders ignore it.
  • Add the sqlx-postgres feature with PostgresDeviceRegistry over
    POSTGRES_PUSH_DEVICES_MIGRATION_SQL and PostgresDeliveryClaimStore over
    POSTGRES_PUSH_DELIVERY_CLAIMS_MIGRATION_SQL. Both constants are available
    without the feature. Registration runs under a per-owner advisory lock, so
    concurrent registrations cannot overshoot the cap. Add
    erase_owner_push_devices, erase_owner_delivery_claims, and the batched
    purge_delivery_claims.
  • Add MemoryDeviceRegistry and MemoryDeliveryClaimStore behind
    test-support.
  • baukit-push now depends on chrono and uuid.
  • Add deferred receipt polling. PushTicketId (1 to
    MAX_PUSH_TICKET_ID_LENGTH (128) bytes of visible ASCII) names an accepted
    notification. The PushReceiptSource port returns settled PushReceipt
    values per ticket, and ExpoPushSender implements it, splitting requests at
    MAX_RECEIPT_BATCH_SIZE (1000, Expo's limit). The PendingReceiptStore port
    records PendingReceipt values (ticket, token, sent_at), takes a bounded
    due batch and moves it back by RECEIPT_POLL_DELAY (15 minutes), deletes
    settled tickets, and purges tickets older than RECEIPT_RETENTION (24
    hours). Its provided record_accepted records every accepted outcome of a
    send. poll_pending_receipts takes one due batch, fetches receipts,
    invalidates DeviceNotRegistered tokens with each send's own sent_at,
    deletes the settled tickets, and returns a ReceiptPoll; failures are a
    ReceiptPollError. accepted_receipts is the filter behind
    record_accepted. There is no scheduler; products drive the poll from their
    job runner.
  • Add PostgresPendingReceiptStore behind sqlx-postgres over
    POSTGRES_PUSH_PENDING_RECEIPTS_MIGRATION_SQL, and MemoryPendingReceiptStore
    behind test-support. FakePushSender implements PushReceiptSource and
    gains settle_receipt and receipt_requests.

Changed

  • Break: PushMessage::token and PushOutcome::token are DeviceToken
    instead of String, so their Debug output redacts the token.
    PushMessage::new takes a DeviceToken. FakePushSender::reject and
    accept_without_receipt take a DeviceToken, and
    FakePushSender::dead_tokens returns Vec<DeviceToken>. dead_tokens no
    longer skips invalid tokens, since an outcome can no longer hold one.
  • Break: PushDeliveryStatus::Accepted carries the provider's
    PushTicketId.
  • Break: PushValidationError gains the TicketId variant.
  • Break: FakePushSender::fail_with also fails receipt requests.

baukit-ratelimit

Changed

  • Break: the rejection detail retry_after is now retryAfter. The Retry-After header is
    unchanged.

baukit-runtime

  • No component-specific changes in this train.

baukit-sync

Added

  • Add the sqlx-postgres feature. It carries every SQLx helper, so a crate that only needs the
    hybrid logical clock no longer compiles SQLx, chrono, or uuid.
  • Add horizon::check_pull_cursor, PullCursorError, and the resync_required wire constants
    (RESYNC_REQUIRED_STATUS, RESYNC_REQUIRED_CODE, HORIZON_REVISION_DETAIL,
    FULL_RESYNC_CURSOR). These need no feature. HORIZON_REVISION_DETAIL is the camelCase
    horizonRevision, so the stale-cursor detail is details.horizonRevision on the wire.
  • Add purge::TombstoneTable, purge_tombstone_batch, purge_tombstones, purge_horizon, and
    guard_pull_cursor. A batch deletes at most the caller's limit of expired tombstones and raises
    each affected owner's horizon in the same transaction, never lowering it. It skips owners whose
    revision counter row is locked instead of waiting. The pull guard locks the counter row with
    FOR KEY SHARE, so a purge for that owner cannot commit while the pull transaction is open.
  • Add the reference migration migrations/0002_baukit_sync_purge_horizons.sql and
    POSTGRES_PURGE_HORIZONS_MIGRATION_SQL for the sync_purge_horizons table.

Breaking

  • next_revision, ensure_owner, current_revision, and current_revision_for_update now need
    features = ["sqlx-postgres"]. chrono, sqlx, and uuid are optional dependencies.
  • The purge helpers need sync_purge_horizons. Its foreign key references sync_revisions, so a
    migration that drops sync_revisions must drop sync_purge_horizons first.

baukit-telemetry

  • No component-specific changes in this train.

baukit-test

Added

  • Publish fixtures/webhooks/signature-v1.json, signing and verification vectors for the
    baukit-webhook-v1 HMAC-SHA256 signature. A test runs them against
    webhook_signing_input, sign_webhook_hmac_sha256, and verify_webhook_hmac_sha256.
  • Add PostgresTestOptions to start the PostgreSQL fixture with another image
    and tag, a login app role without superuser or BYPASSRLS, and migrations.
    start_postgres and start_postgres_with_migrations keep their signatures
    and now delegate to it.
  • Add PostgresTestContainer::create_database and PostgresTestDatabases for
    one database per test on a shared container or on an external server such as
    DATABASE_URL. Each PostgresTestDatabase drops its database when it drops.
  • Add MockOidcServer::jwks_url for verifiers built with from_jwks_uri, and
    MockOidcServer::mint_with_key_id to sign with the active key under an
    unpublished kid. Document the existing jwks_request_count and
    set_jwks_delay for JWKS cache tests.
  • Add check_purge_horizon_conformance and assert_purge_horizon_conformance with the
    PurgeHorizonAdapter trait, PurgeHorizonPull, and PullPause. The check covers horizon
    monotonicity, batch limits, a purge racing an open pull, cursors at and below the horizon, owner
    isolation, and erasure.
  • Re-export assert_openapi_camel_case and check_openapi_camel_case from baukit-openapi.
  • Add check_replay_safe_mutation_conformance and assert_replay_safe_mutation_conformance with
    the ReplaySafeMutationAdapter trait, ReplayRequest, ReplayOperation, ReplayOutcome,
    ReplaySnapshot, CommitCheckpoint, InjectedRollback, ReplayConformanceInputs, and
    ReplayConformanceError. The check covers a lost response, equivalent and changed input,
    simultaneous requests with one key, owner and operation isolation, rollback before commit, a
    crash after commit, expiry, bounded cleanup, and erasure.
  • Add assert_response_matches_openapi and check_response_matches_openapi with
    ObservedResponse and OpenApiResponseError. They check a real response's status, media type,
    and JSON body against its operation in a serialized OpenAPI 3.1 document, using JSON Schema
    2020-12 with format validation. The crate now depends on jsonschema 0.58.1 without default
    features.

Changed

  • InMemoryApiTokenStore stores ApiTokenRecord::grants on the token and
    keeps the latest last_used_at when touches arrive out of order.

  • Break: PostgresTestError gains InvalidAppRole, InvalidConnectionUrl,
    and Setup variants. Exhaustive matches on it must handle them.

  • Break: check_auth_router_conformance now expects the
    error_description="expired" and error_description="invalid" bearer
    challenges that baukit-auth sends.

  • With sqlx-postgres, the container fixture now waits until PostgreSQL
    accepts a connection before it returns.

  • Break: check_auth_router_conformance expects requestId in the unauthenticated error
    envelope instead of request_id.

TypeScript packages

a11y-core

Minor Changes

  • 868b67e: Add headless menu navigation. useRovingMenu({ active, options }) gives an action menu one tab stop on an enabled item, arrow-key movement that skips disabled items and wraps, and Home and End. It returns itemProps(index), activeIndex, and a stable initialFocusRef to pass to useOverlayA11y, which keeps Escape, Tab containment, and focus restoration. With no enabled item there is no item tab stop and the overlay focuses its first focusable control. When the active item is removed or disabled while open, the tab stop moves to the item now at that position, or back to the initial item; focus follows if it fell to the document body. Keys from hosts nested inside an item are ignored. The pure nextEnabledMenuIndex(key, currentIndex, options) is exported for container-level key handlers. Both are available from the package root and from @baukit/a11y-core/web.

    useOverlayA11y now accepts any HostRef as initialFocusRef, matching useFocusTrap. Existing RefObject<View | null> values still type-check.

    No breaking changes.

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • f249d7c: Add focusAccessibilityElement(ref) to the package root. On native it resolves the view tag with findNodeHandle and calls AccessibilityInfo.setAccessibilityFocus. On web it focuses the element with preventScroll. It returns false when there is nothing to focus. useOverlayA11y now shares its view tag lookup.

    The mobile template's useRouteHeadingFocus now moves native screen-reader focus to the route heading one frame after the route gains focus. It previously did nothing on native.

    No breaking changes.

  • Release the coordinated baukit 0.5.0 train.

analytics-core

Minor Changes

  • 4f4a873: Add exact-key scrubbing and scrubErrorEvent for crash reports. scrubProperties and AnalyticsClient accept exactBlockedKeys, which redact a key only when the normalized key is equal. scrubErrorEvent redacts request headers, bodies, query strings, breadcrumb data, and frame variables, and keeps event and trace IDs and stack frame locations. Break: ip, ip_address, remote_addr, x_forwarded_for, and x_real_ip are now redacted by default through DEFAULT_EXACT_BLOCKED_KEYS.

  • bc05828: Add HydratedAnalyticsStorage to the new @baukit/analytics-posthog-native/storage subpath. It loads a list of keys from an asynchronous store such as AsyncStorage once, then serves the synchronous AnalyticsStorage port from memory and writes those keys through. A key that fails to read starts absent without dropping the others. The subpath does not import posthog-react-native.

    Add analyticsStorageKeys(prefix) to @baukit/analytics-core. It returns the consent, anonymous ID, user ID, and alias guard keys that AnalyticsClient uses.

    posthog-react-native is now an optional peer of @baukit/analytics-posthog-native. Apps that use the transport must install it themselves, which the setup instructions already said.

    The mobile template now calls HydratedAnalyticsStorage instead of generating its own class. Products that copied the template class can delete it.

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • Release the coordinated baukit 0.5.0 train.

analytics-posthog-native

Minor Changes

  • bc05828: Add HydratedAnalyticsStorage to the new @baukit/analytics-posthog-native/storage subpath. It loads a list of keys from an asynchronous store such as AsyncStorage once, then serves the synchronous AnalyticsStorage port from memory and writes those keys through. A key that fails to read starts absent without dropping the others. The subpath does not import posthog-react-native.

    Add analyticsStorageKeys(prefix) to @baukit/analytics-core. It returns the consent, anonymous ID, user ID, and alias guard keys that AnalyticsClient uses.

    posthog-react-native is now an optional peer of @baukit/analytics-posthog-native. Apps that use the transport must install it themselves, which the setup instructions already said.

    The mobile template now calls HydratedAnalyticsStorage instead of generating its own class. Products that copied the template class can delete it.

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • Release the coordinated baukit 0.5.0 train.

Patch Changes

  • Updated dependencies [4f4a873]
  • Updated dependencies [bc05828]
  • Updated dependencies [8d268e1]
  • Updated dependencies
    • @baukit/analytics-core@0.5.0

analytics-posthog-web

Minor Changes

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.
  • Release the coordinated baukit 0.5.0 train.

Patch Changes

  • Updated dependencies [4f4a873]
  • Updated dependencies [bc05828]
  • Updated dependencies [8d268e1]
  • Updated dependencies
    • @baukit/analytics-core@0.5.0

api-runtime

Minor Changes

  • 4793121: Move wire names to camelCase to match the Baukit HTTP APIs.

    Breaking changes:

    • @baukit/api-runtime: parseApiErrorEnvelope reads only error.requestId. An envelope that carries request_id and no requestId is no longer an ApiError. ApiErrorEnvelope.error.request_id is now requestId.
    • @baukit/events: EventEnvelopeSchema and EventEnvelope use eventId, userId, occurredAt, sourceApp, and schemaVersion instead of event_id, user_id, occurred_at, source_app, and schema_version, and still describe schema version 1. IngestOutcomeSchema uses ledgerEntryId instead of ledger_entry_id. EventPayloadSchema accepts camelCase keys (/^[a-z][a-zA-Z0-9]{0,63}$/) and rejects snake_case keys.
    • @baukit/sync-client: toSnakeCaseSnapshot, toSnakeCaseFailure, SnakeCaseSyncStatusSnapshot, and SnakeCaseSyncFailure are removed. Read the camelCase SyncStatusSnapshot directly. The documented resync_required detail is details.horizonRevision and the pull page flag is hasMore.
  • bb0121b: Add keyed mutation support.

    /idempotency is a new entry point, not re-exported from the package root. createIdempotencyKeyStore keeps one Idempotency-Key per account, operation, and body, compared by canonical JSON, until the outcome is definite or ttlMs passes. Keys default to crypto.randomUUID() and in-memory storage; storage takes an IdempotencyKeyStorage port to keep them across reloads. classifyMutationStatus and classifyMutationError return committed, not-committed, or possibly-committed. Network errors, aborts, 408, 429, 5xx, 409 idempotency_key_in_progress, and unknown throws are possibly committed and keep the key. sendIdempotentMutation gets the key, sends, and settles it. canonicalJson and IDEMPOTENCY_KEY_IN_PROGRESS_CODE are exported too.

    The package root exports IDEMPOTENCY_KEY_HEADER. RetryableMethod now includes POST and PATCH, and createApiFetch retries them only when retry.methods names them and the request carries Idempotency-Key. Unkeyed POST and PATCH still never retry.

    Break: before, retry.methods silently dropped POST and PATCH. A caller that already listed them now gets retries for keyed requests. Remove them from methods to keep the old behavior.

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • Release the coordinated baukit 0.5.0 train.

auth-native

Minor Changes

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • Release the coordinated baukit 0.5.0 train.

  • 118e055: Add a state decoration for themed login pages. NativeOidcClient.signIn({ stateDecoration }) forwards leading state segments to the browser flow through the new optional AuthorizationRequest.stateDecoration. appearanceStateDecoration({ mode, primaryColor?, secondaryColor? }) builds ap1.<d|l|s>[.<PRIMARY>.<SECONDARY>] segments and decoratedAuthorizationState(segments, entropy) joins them with a hex nonce of at least 32 random bytes.

    @baukit/auth-native/expo exports createExpoBrowserFlow({ randomBytes }), the AuthSession browser flow that createExpoOidcEnvironment already used. With randomBytes it puts the decoration in front of a random nonce. Without randomBytes, or when the entropy source or a segment fails, it keeps AuthSession's own state. ExpoOidcEnvironmentOptions now accepts randomBytes.

    No breaking changes.

auth-node

Minor Changes

  • 98f44a8: Add parseApiOrigin(value, { allowLoopbackHttp?, label? }) and ApiOriginError to the package root. The parser trims the value, accepts one trailing slash, and returns the URL's origin. It rejects credentials, a path, a query, or a fragment, and it allows plain HTTP only on a loopback host when allowLoopbackHttp is set. Errors carry a reason of invalid_url, insecure_scheme, or not_an_origin and never include the value. The device flow now uses the same scheme and loopback check.

    The generated MCP template now always depends on @baukit/auth-node and validates its API URL with parseApiOrigin.

    No breaking changes to the package. The CLI doctor now requires @baukit/auth-node in every generated mcp/package.json.

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • Release the coordinated baukit 0.5.0 train.

auth-web

Minor Changes

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.
  • Release the coordinated baukit 0.5.0 train.

data-contracts-dexie

Minor Changes

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.
  • Release the coordinated baukit 0.5.0 train.

Patch Changes

  • Updated dependencies [8d268e1]
  • Updated dependencies [acaab1b]
  • Updated dependencies
  • Updated dependencies [d9225d5]
  • Updated dependencies [a9fa22e]
    • @baukit/data-contracts@0.5.0

data-contracts-expo-sqlite

Minor Changes

  • f57396c: Add the @baukit/data-contracts-expo-sqlite/testing entry point for Node unit tests on the built-in node:sqlite (Node 24 or later, no new dependency). NodeSqliteDatabase implements the Expo statement methods this adapter calls, on a file path it owns or one you pass in. Exclusive transactions open a second connection to the same file, as on the device, so an overlapping root write fails with database is locked, and foreign keys stay off on every connection. createSqliteMigrationConformanceTests(adapter) returns framework-neutral cases that check a product migration runner for fresh installs, upgrades, restarts, complete rollback of a failed step, and refusal of a database migrated by a newer build.

    Export ExpoSqliteDatabase, the part of an Expo SQLiteDatabase the adapter calls. The ExpoSqliteStore constructor now accepts that type instead of SQLiteDatabase, so an Expo database still fits.

    No breaking changes.

  • c795f17: Serialize Expo SQLite operations per database file. Root record, key/value, and schema-metadata calls, initialize(), withTransaction(), and close() now run one at a time in call order on one queue shared by every adapter store on the same databasePath, so a root statement can no longer overlap an exclusive transaction and fail with database is locked. Transaction-context work stays on the transaction connection. Lock errors still reach the caller unchanged; nothing is retried.

    Breaking changes:

    • SqliteRecordStore, SqliteKeyValueStore, and SqliteSchemaMetadataStore constructors take (database, namespace) only. The third assertAvailable argument is removed, and database must expose databasePath (an Expo SQLiteDatabase does).
    • ExpoSqliteStore.records, .keyValues, and .schemaMetadata are typed as the RecordStore, KeyValueStore, and SchemaMetadataStore contracts. Their per-store initialize() is no longer part of the public type; call ExpoSqliteStore.initialize().
    • A transaction accepted before close() now runs before the adapter closes instead of failing with storage_closed.
    • close() returns the same promise to every caller. A failed close keeps rejecting with the same error instead of resolving on the next call.
    • A root call made from inside a withTransaction callback on a store for the same file now waits forever instead of failing with database is locked. Use the transaction context inside the callback.
    • A product wrapper that queued root operations and transactions around the adapter becomes redundant; remove it after pinning this release.
  • d4aca9d: Add ExpoSqliteConnection, a raw SQL connection for a product's own schema and migration runner. exec, run, get, and all bind parameters as expo-sqlite does. Root statements, transaction(work), and close() share the per-file queue with ExpoSqliteStore and the root stores, so a root statement issued during another caller's transaction runs after it instead of joining it. transaction(work) runs BEGIN IMMEDIATE on the supplied handle, commits when work resolves, and rolls back when work or the commit rejects. The transaction context's transaction() always rejects with a TypeError, and its statements reject with storage_closed once it settles. The connection runs PRAGMA foreign_keys = ON before its first statement, so foreign keys stay enforced inside transactions. close() closes the handle only with { closeDatabase: true }. New types: ExpoSqliteConnectionOptions, SqliteStatements, and SqliteTransaction.

    ExpoSqliteStore now shares its queueing, close, and transaction-context code with the connection. Its behavior is unchanged.

    Behavior change for code that shares a handle: the first statement of an ExpoSqliteConnection turns foreign keys on for that handle, so ExpoSqliteStore and root stores on the same handle run with foreign keys on from then on. The adapter's own tables have no foreign keys.

    No breaking changes.

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • Release the coordinated baukit 0.5.0 train.

Patch Changes

  • Updated dependencies [8d268e1]
  • Updated dependencies [acaab1b]
  • Updated dependencies
  • Updated dependencies [d9225d5]
  • Updated dependencies [a9fa22e]
    • @baukit/data-contracts@0.5.0

data-contracts

Minor Changes

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • acaab1b: Add parseLimitsPolicy, LimitsPolicyError, LimitError, and enforceLimit to /limits. parseLimitsPolicy(value, schema) validates a limits policy file against a schema that names the version, the keys of each section, and the keys allowed to be zero. It returns a result typed from the schema. enforceLimit(field, reason, check) turns LimitExceededError into LimitError with reason, field, measured, and allowed.

    The web and mobile templates now call these instead of generating their own parser. Products that copied the template's parseLimitsPolicy, LimitsPolicyError, and LimitError can delete them and declare a schema. LimitError gains measured and allowed, and invalid counts now throw the package's RangeError message (measured must be a non-negative safe integer) instead of the generated <field> count must be a non-negative integer.

    No breaking changes to existing package exports.

  • Release the coordinated baukit 0.5.0 train.

  • d9225d5: Add two opt-in entry points, neither re-exported from the package root and neither depending on React.

    /revisioned-writes adds createRevisionedWriteQueue. It sends one write at a time with the last acknowledged revision, keeps edits made during a write as a separate unsent value, and combines queued edits with a caller coalesce function. The write callback returns accepted, conflict, or rejected. A conflict pauses the queue until reset. A throw means the outcome is unknown: the queue keeps the exact value and revision apart from newer edits, and retry sends them again with afterUnknownOutcome: true. reset fences account and document switches and aborts the in-flight write through the injected AbortSignal. The snapshot works with useSyncExternalStore.

    /durable-draft adds createDurableDraft, which keeps a form value in any KeyValueStore as { version, value } through a versioned codec that validates unchecked JSON. The snapshot reports recovery (none, restored, corrupt, unsupported-version, unavailable), persistence (loading, idle, saving, clearing, failed), dirty, and localRevision. clear takes submitted with the confirmed local revision, which keeps newer edits, or discarded. A failed deletion stays in the snapshot. Storage failures throw DraftPersistenceError without keys or draft content.

    No breaking changes.

  • a9fa22e: Add the /export entry point, also re-exported from the package root. encodeCsv writes RFC 4180 CSV with CRLF record separators and neutralizes spreadsheet formulas by default: a text cell that starts with =, +, -, @, tab, or carriage return, or with =, +, -, or @ after leading spaces, tabs, CR, or LF, gets a leading apostrophe. Pass neutralizeFormulas: false to turn this off, and byteOrderMark: true to start the output with U+FEFF. Finite number cells and { numeric } cells (built with csvNumeric) must match the JSON number grammar and are written without neutralization, so -5 stays a number. Invalid numeric cells, unpaired surrogates, and unsupported runtime values throw CsvEncodeError with a code, row index, and column index, never the cell value. ShareOutcome and SHARE_OUTCOMES name the share or save results shared, saved, cancelled, unavailable, and failed; no Expo implementation ships yet. baukit_core::export::encode_csv passes the same vectors in fixtures/export-csv/csv-encoding-v1.json.

    No breaking changes.

events

Minor Changes

  • 4793121: Move wire names to camelCase to match the Baukit HTTP APIs.

    Breaking changes:

    • @baukit/api-runtime: parseApiErrorEnvelope reads only error.requestId. An envelope that carries request_id and no requestId is no longer an ApiError. ApiErrorEnvelope.error.request_id is now requestId.
    • @baukit/events: EventEnvelopeSchema and EventEnvelope use eventId, userId, occurredAt, sourceApp, and schemaVersion instead of event_id, user_id, occurred_at, source_app, and schema_version, and still describe schema version 1. IngestOutcomeSchema uses ledgerEntryId instead of ledger_entry_id. EventPayloadSchema accepts camelCase keys (/^[a-z][a-zA-Z0-9]{0,63}$/) and rejects snake_case keys.
    • @baukit/sync-client: toSnakeCaseSnapshot, toSnakeCaseFailure, SnakeCaseSyncStatusSnapshot, and SnakeCaseSyncFailure are removed. Read the camelCase SyncStatusSnapshot directly. The documented resync_required detail is details.horizonRevision and the pull page flag is hasMore.
  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • Release the coordinated baukit 0.5.0 train.

integrations-client

Minor Changes

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.
  • Release the coordinated baukit 0.5.0 train.

localization-core

Minor Changes

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • Release the coordinated baukit 0.5.0 train.

  • 8a31c75: Export the shared-fixture vector checks so the vectors run outside Vitest. @baukit/localization-core/vectors exports zonedTimeVectorChecks(fixture) for fixtures/zoned-time/vectors-v1.json, and @baukit/notifications-core/vectors exports notificationPlanVectorChecks(fixture) for fixtures/notifications/plan-vectors-v1.json, with the fixture types. Each check has a label, an expected value, and an actual() call whose result must deep-equal it. The packages' own Vitest suites now run these checks, and examples/expo-notifications-conformance runs them inside Hermes on an Android emulator.

    No breaking changes. The root exports are unchanged.

  • cf3a85b: Add resolveZonedLocalTime({ civilDate, civilTime, timeZone, gap, fold }), which turns a civil date and local time in an IANA zone into an instant. The gap policy (reject or shiftForward) and the fold policy (earlier or later) are required and have no default. It returns { ok: true, epochMilliseconds, transition } or { ok: false, code } with invalid_civil_date, invalid_civil_time, invalid_time_zone, or nonexistent_local_time, and throws RangeError only for an unknown policy value. It reads zone rules through Intl and adds no dependency. New exports: GapPolicy, FoldPolicy, LocalTimeTransition, ZonedLocalTime, ZonedLocalTimeCode, ZonedLocalTimeResult, INVALID_CIVIL_TIME_CODE, INVALID_TIME_ZONE_CODE, and NONEXISTENT_LOCAL_TIME_CODE. Shared vectors for this function and for Rust resolvers live in fixtures/zoned-time/vectors-v1.json.

    No breaking changes. A product with its own localDateTimeToInstant can replace it with resolveZonedLocalTime({ ..., gap: 'reject', fold: 'earlier' }), which matches the old behavior for valid input, and must then handle the returned error code instead of a thrown Error.

notifications-core

Minor Changes

  • ea5c1cf: Add @baukit/notifications-core, which plans local notifications without a notification library. resolveNotificationOccurrences({ occurrences, timeZone, gap, fold, clock, horizonDays }) resolves each { logicalId, civilDate, civilTime, contentDigest } through resolveZonedLocalTime, keeps the ones inside the horizon, and reports the rest as past, outside_horizon, or nonexistent_local_time. planNotificationReplacement(current, desired, { replaceAll }) returns deterministic keep, cancel, and schedule sets by logical ID, instant, and content digest. createOwnedNotificationScheduler(platform, { pendingLimit }) runs that plan against a NotificationPlatform port for one namespace at a time, touches only requests that carry the namespace's marker and the baukit:<namespace>:<logicalId> identifier, and returns { status, kept, cancelled, scheduled, failures } with content-free failure codes. Invalid input throws NotificationPlanError. InMemoryNotificationPlatform, NotificationPlatformFaultState, and the @baukit/notifications-core/vitest conformance suite let products test their own platform code. Shared vectors live in fixtures/notifications/plan-vectors-v1.json.

    Add @baukit/notifications-expo, the optional expo-notifications adapter. createExpoOwnedNotificationScheduler(Notifications, { pendingLimit }) schedules DATE triggers with the marker under content.data.baukitNotification, never calls cancelAllScheduledNotificationsAsync, and maps iOS provisional and ephemeral authorization to granted. expo-notifications ^57.0.13 is a peer dependency and the package imports only its types.

    No breaking changes. Both packages are new.

  • Release the coordinated baukit 0.5.0 train.

  • 8a31c75: Export the shared-fixture vector checks so the vectors run outside Vitest. @baukit/localization-core/vectors exports zonedTimeVectorChecks(fixture) for fixtures/zoned-time/vectors-v1.json, and @baukit/notifications-core/vectors exports notificationPlanVectorChecks(fixture) for fixtures/notifications/plan-vectors-v1.json, with the fixture types. Each check has a label, an expected value, and an actual() call whose result must deep-equal it. The packages' own Vitest suites now run these checks, and examples/expo-notifications-conformance runs them inside Hermes on an Android emulator.

    No breaking changes. The root exports are unchanged.

Patch Changes

  • Updated dependencies [8d268e1]
  • Updated dependencies
  • Updated dependencies [8a31c75]
  • Updated dependencies [cf3a85b]
    • @baukit/localization-core@0.5.0

notifications-expo

Minor Changes

  • ea5c1cf: Add @baukit/notifications-core, which plans local notifications without a notification library. resolveNotificationOccurrences({ occurrences, timeZone, gap, fold, clock, horizonDays }) resolves each { logicalId, civilDate, civilTime, contentDigest } through resolveZonedLocalTime, keeps the ones inside the horizon, and reports the rest as past, outside_horizon, or nonexistent_local_time. planNotificationReplacement(current, desired, { replaceAll }) returns deterministic keep, cancel, and schedule sets by logical ID, instant, and content digest. createOwnedNotificationScheduler(platform, { pendingLimit }) runs that plan against a NotificationPlatform port for one namespace at a time, touches only requests that carry the namespace's marker and the baukit:<namespace>:<logicalId> identifier, and returns { status, kept, cancelled, scheduled, failures } with content-free failure codes. Invalid input throws NotificationPlanError. InMemoryNotificationPlatform, NotificationPlatformFaultState, and the @baukit/notifications-core/vitest conformance suite let products test their own platform code. Shared vectors live in fixtures/notifications/plan-vectors-v1.json.

    Add @baukit/notifications-expo, the optional expo-notifications adapter. createExpoOwnedNotificationScheduler(Notifications, { pendingLimit }) schedules DATE triggers with the marker under content.data.baukitNotification, never calls cancelAllScheduledNotificationsAsync, and maps iOS provisional and ephemeral authorization to granted. expo-notifications ^57.0.13 is a peer dependency and the package imports only its types.

    No breaking changes. Both packages are new.

  • Release the coordinated baukit 0.5.0 train.

Patch Changes

  • Updated dependencies [ea5c1cf]
  • Updated dependencies
  • Updated dependencies [8a31c75]
    • @baukit/notifications-core@0.5.0

preferences-core

Minor Changes

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.
  • Release the coordinated baukit 0.5.0 train.

pwa-web

Minor Changes

  • Release the coordinated baukit 0.5.0 train.

sync-client

Minor Changes

  • 4793121: Move wire names to camelCase to match the Baukit HTTP APIs.

    Breaking changes:

    • @baukit/api-runtime: parseApiErrorEnvelope reads only error.requestId. An envelope that carries request_id and no requestId is no longer an ApiError. ApiErrorEnvelope.error.request_id is now requestId.
    • @baukit/events: EventEnvelopeSchema and EventEnvelope use eventId, userId, occurredAt, sourceApp, and schemaVersion instead of event_id, user_id, occurred_at, source_app, and schema_version, and still describe schema version 1. IngestOutcomeSchema uses ledgerEntryId instead of ledger_entry_id. EventPayloadSchema accepts camelCase keys (/^[a-z][a-zA-Z0-9]{0,63}$/) and rejects snake_case keys.
    • @baukit/sync-client: toSnakeCaseSnapshot, toSnakeCaseFailure, SnakeCaseSyncStatusSnapshot, and SnakeCaseSyncFailure are removed. Read the camelCase SyncStatusSnapshot directly. The documented resync_required detail is details.horizonRevision and the pull page flag is hasMore.
  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.

  • Release the coordinated baukit 0.5.0 train.

ui-tokens

Minor Changes

  • 8d268e1: Add a default export condition next to import on every export except the ESM-only ./vitest subpaths. Jest and other CommonJS-condition resolvers now find @baukit/* without a moduleNameMapper. Each package's test script packs the package and resolves every export under require conditions from the archive.
  • Release the coordinated baukit 0.5.0 train.

Commits

  • chore: prepare baukit v0.5.0 (7f617b4)
  • docs(releasing): count seventeen crates and twenty packages (ebe3236)
  • docs(plan): record the follow-up wave's final gate (16dabdc)
  • docs(plan): record F7 and start F6 (7311a91)
  • merge: F7, auth web coverage, Compose Redis, fixture coverage in CI (7b33561)
  • docs(evidence): record the F7 follow-up (938e0b0)
  • ci: run test:coverage in the generated fixture job (45301f9)
  • feat(templates): cover auth web floors and compose Redis for auth products (13a1375)
  • docs(plan): record F3 and add F7 (ba6f74f)
  • merge: F3, test type gate, mobile auth coverage, stack e2e port (adfcc34)
  • docs(evidence): record the item 19 follow-up (1bb26df)
  • feat(templates): enforce mobile auth coverage and parameterize the stack e2e port (2b76f9c)
  • test(typescript): type-check every package's tests in its test script (538edc5)
  • docs(plan): record F5 and correct the item 15 adoption lines (bc0c153)
  • merge: F5, queued raw-SQL ExpoSqliteConnection (bec2a31)
  • docs(evidence): record the queued raw-SQL connection follow-up (d7cedcd)
  • feat(data-contracts-expo-sqlite): add queued raw-SQL ExpoSqliteConnection (d4aca9d)
  • docs(plan): record F4 (b1eacd4)
  • merge: F4, Hermes vectors and notifications adapter on the emulator (2eb0ca9)
  • test(notifications): run the vectors and the Expo adapter on an Android emulator (8a31c75)
  • docs(plan): record F1 (0c2e6b0)
  • merge: F1, push token redaction and deferred receipt polling (00c9cc0)
  • docs(evidence): record the push follow-up for token redaction and receipt polling (19dfdff)
  • feat(push): redact tokens in messages and outcomes, poll deferred receipts (5db83b7)
  • docs(plan): start F5 (97de48e)
  • docs(plan): record F2 (5f11625)
  • docs(evidence): mark the Retry-After cap and 425 decisions in the webhook study (8edc3f7)
  • merge: F2, 425 retry, Retry-After cap, release-plz coverage (d48b2cb)
  • docs(core): build the media grant key ring once and rotate by restart (d2f1848)
  • build(release): list every crate in release-plz and enforce it (78e7ed2)
  • fix(http): retry 425 and cap Retry-After in the egress client (939d1e5)
  • docs(plan): add the follow-up wave with recommended decisions (c44a52a)
  • docs(plan): record the final gate (8cf5c4b)
  • docs(plan): record item 19 (5321f10)
  • docs: build analytics-posthog-native and auth-node before fixture checks (e70ea6a)
  • merge: item 19, template code into packages (a85adf6)
  • docs(evidence): record item 19 template code moves (b35dd58)
  • feat(auth-node): add parseApiOrigin and validate the MCP API URL with it (98f44a8)
  • feat(templates): add Keycloak stack e2e helpers to the web auth template (8b2156e)
  • feat(auth-native): add a themed Expo browser flow with a state decoration (118e055)
  • docs(plan): record the integrated gate on 525a55b (3a6e5f2)
  • feat(a11y-core): add focusAccessibilityElement for native route focus (f249d7c)
  • feat(analytics-posthog-native): add HydratedAnalyticsStorage (bc05828)
  • docs: refresh CLAUDE.md layout and make ci coverage (f78e1a2)
  • docs(plan): record item 18 and the live-row cap fix (c2c1760)
  • merge: item 18, signed media grants (27cacb8)
  • test(test): make the live-row cap race overlap deterministic with a read barrier (3f7921c)
  • docs(evidence): record signed media grants as evidence 54 (76e79be)
  • feat(deploy): add njs media grant verifier (5d99155)
  • feat(core): add signed media grants (acfe19e)
  • feat(data-contracts): add limits policy parser and LimitError (acaab1b)
  • docs(plan): record item 16 (d3016ea)
  • merge: item 16, guarded outbound HTTP client and webhook study (8a06044)
  • docs(evidence): record guarded egress and the webhook delivery study as evidence 52 (9d1f083)
  • test(test): publish baukit-webhook-v1 signature vectors (c3e3196)
  • feat(egress): add guarded outbound HTTP client with published address vectors (19385d5)
  • docs(plan): record item 8 (525a55b)
  • merge: item 8, OpenAPI error responses and a compatibility check (1e54427)
  • docs(evidence): record OpenAPI error responses as evidence 44 (8ac884c)
  • feat(templates): document error responses and opt-in compatibility gate (cce8ebe)
  • feat(test): add assert_response_matches_openapi (0f84f37)
  • feat(scripts): add OpenAPI compatibility check (6072315)
  • feat(openapi): add rule-driven error-response decorator (1a1615b)
  • docs(plan): record item 7, start item 19 (2c8fefb)
  • merge: item 7, replay-safe HTTP mutations (ea0bdf6)
  • docs(evidence): record replay-safe mutations as evidence 43 (5305a44)
  • feat(api-runtime): add idempotency key store and keyed POST retries (bb0121b)
  • feat(http): add Idempotency-Key parser with caller bounds (465f977)
  • feat(test): add replay-safe mutation conformance check (208e649)
  • docs(plan): record item 17, start item 18 (523a233)
  • merge: item 17, push device registry (5e73a83)
  • docs(evidence): record push device registry as evidence 53 (919aece)
  • feat(push): add device registry and daily delivery claims (13602b2)
  • docs(plan): start item 16 (dc5792a)
  • docs(plan): record item 15 and the integrated check (b25875c)
  • merge: item 15, SQLite migration conformance and Node test driver (aead6ca)
  • docs(evidence): record SQLite migration conformance and Node test driver as evidence 51 (620696f)
  • feat(data-contracts-expo-sqlite): add node:sqlite test driver and migration conformance cases (f57396c)
  • test(test): allow extra serialization retries in the live-row cap comparison (65c899f)
  • docs(plan): record item 10, start item 17 (a5227a7)
  • merge: item 10, PostgreSQL API token store with grants (78e0d7c)
  • docs(evidence): record API token store with grants as evidence 46 (2112608)
  • feat(auth): add PostgreSQL API token store with grants and verified scope claims (bcaa9f4)
  • docs(plan): record item 5, start item 8 (240bd13)
  • merge: item 5, camelCase HTTP APIs (0f72840)
  • docs(plan): start item 7 (aea2a56)
  • docs(plan): record item 6 (9bc9adb)
  • merge: item 6, strong-ETag revision preconditions (b0570ae)
  • docs(evidence): add strong-ETag revision precondition evidence (703515f)
  • feat(http): add strong-ETag revision preconditions (257bc85)
  • feat(openapi): document If-Match revision preconditions and ETag headers (2cde22a)
  • test(fixtures): add strong-ETag revision precondition vectors (92b074e)
  • docs(plan): start item 15 (9555432)
  • docs(plan): record item 12 (2f33e05)
  • fix(notifications): add default export conditions and the packed-export check (6191e7f)
  • docs(evidence): record the horizonRevision reconciliation in note 41 (f12ad8d)
  • docs(evidence): record camelCase HTTP APIs as note 41 (546c5e4)
  • feat!: move HTTP and event wire names to camelCase (4793121)
  • merge: item 12, local notification planning and owned Expo replacement (c8fcdf1)
  • docs(evidence): add local notification planning evidence (638bef9)
  • feat(notifications): plan local notifications and replace only owned requests (ea5c1cf)
  • test(fixtures): add local notification plan vectors (c7852df)
  • docs(plan): start item 10 (c2c29e9)
  • docs(plan): record item 9 (9cbdf55)
  • merge: item 9, server-side tombstone purge horizons (61391d1)
  • docs(evidence): record server purge horizons as evidence 45 (76c909b)
  • feat(sync)!: add tombstone purge horizons behind the sqlx-postgres feature (d922948)
  • feat(test): add tombstone purge-horizon conformance check (46b0bb4)
  • fix(deps): update rustls to 0.23.45 for RUSTSEC-2026-0285 (e617a56)
  • docs(plan): start item 6 (7ee710e)
  • docs(plan): record item 14 (845e5f7)
  • fix(data-contracts): add default conditions to the write queue and draft exports (8a47f35)
  • merge: item 14, revisioned writes and durable drafts (76b4fb0)
  • docs(evidence): add revisioned writes and drafts evidence (d260611)
  • feat(data-contracts): add revisioned write queue and durable draft entry points (d9225d5)
  • docs(plan): record wave 1 verification (6bb19da)
  • fix(deps): bump rustls to 0.23.45 for RUSTSEC-2026-0285 (26ff262)
  • docs(plan): start item 5 (569b376)
  • docs(plan): record item 4, wave 1 complete (b430d27)
  • docs(evidence): renumber consumer friction evidence to 40 (d7f1a49)
  • merge: item 4, consumer friction fixes (964ac54)
  • docs(plan): start item 12 (c13d76c)
  • docs(plan): record item 11 (b9014d3)
  • merge: item 11, zoned local times and calendar recipe (c7c616f)
  • docs(plan): start item 14 (f4fa786)
  • docs(platform): publish the calendar export recipe and item 11 evidence (c27621b)
  • feat(localization-core): resolve zoned local times with required DST policies (cf3a85b)
  • docs(plan): record item 20 (7c527bb)
  • merge: item 20, safe export encoder (671dae2)
  • docs(evidence): add safe export encoder evidence (83f275f)
  • feat(core): add CSV export encoder (6057274)
  • feat(data-contracts): add export entry point with CSV encoder and share outcome (a9fa22e)
  • test(fixtures): add CSV export vectors (8c59754)
  • docs(plan): record item 13 and start item 9 (45a1e32)
  • merge: item 13, headless menu navigation (b110d52)
  • docs(evidence): record headless menu navigation as evidence 49 (22f97ec)
  • feat(a11y-core): add useRovingMenu and nextEnabledMenuIndex (868b67e)
  • feat(auth): send RFC 6750 error_description in bearer challenges (9e3325e)
  • feat(test): expose MockOidcServer JWKS URL and unpublished-kid minting (252799b)
  • docs(plan): record wave 1 progress (8b2d11f)
  • feat(typescript): publish default export conditions so Jest resolves @baukit packages (8d268e1)
  • docs(evidence): renumber Expo SQLite evidence to 37 (90a9a44)
  • merge: item 1, serialize Expo SQLite operations (ddc55fa)
  • test(examples): cover root and transaction overlap on real Expo SQLite (1587397)
  • fix(data-contracts-expo-sqlite): serialize operations per database file (c795f17)
  • fix(examples): make the Expo SQLite gate rerunnable without a TTY (14fee99)
  • feat(analytics): add exact-key scrubbing and crash event scrubber (4f4a873)
  • feat(test): add PostgreSQL image, app role, and per-test database options (1c1b512)
  • docs(evidence): renumber HTTP boundary evidence to 38 (81cdd63)
  • merge: item 2, close HTTP boundary gaps (0f43e3e)
  • docs(evidence): record HTTP boundary gap decisions (d0a0834)
  • feat(http)!: close HTTP boundary gaps (9bba14a)
  • docs(evidence): renumber job claim evidence to 39 (22aabd4)
  • merge: item 3, claim only jobs the worker can handle (a27fb92)
  • docs(evidence): record job claim by type evidence (c68951c)
  • feat(jobs)!: claim only job types the handler declares (5e81185)
  • feat(observability): take product root and metric allowlist in the metric-name linter (e9c36a6)
  • docs(plan): add cross-product feature plan with execution tracker (ef6ac1a)