First-party `bynk` capabilities
Bynk ships a small set of capabilities with the compiler, under the reserved
bynk namespace. You consume them like any capability — consumes the unit,
then given the capability in the handlers that need it — but you never declare
or provide them: the toolchain injects the implementation for the target
platform. The bynk root namespace is reserved, so your own code can never
collide with it.
There are three units:
| Unit | Consume with | Portability |
|---|---|---|
bynk | consumes bynk { … } | Portable — the same source runs on the cloudflare and node platforms. |
bynk.cloudflare | consumes bynk.cloudflare { … } | Platform-locked — consuming it pins the deployment unit to Cloudflare. |
bynk.crypto (experimental) | consumes bynk.crypto { … } | Portable — cloudflare, node and browser, over Web Crypto. |
For why a capability is the unit of outside-world access, see
Understand the capability model;
for the general capability / provides / given rules, see
Capabilities & providers.
The portable surface — bynk
Section titled “The portable surface — bynk”consumes bynk { … } brings these into scope. They are implemented identically
on every platform (the host Date.now, crypto, fetch, console,
environment), so code that stays on this surface is portable.
| Capability | Operations |
|---|---|
Clock | now() -> Effect[Instant] — the current instant (an absolute point in time; epoch-millis under the hood). |
Random | uuid() -> Effect[Uuid] · int(lo: Int, hi: Int) -> Effect[Int] (lo-inclusive, hi-exclusive) · bytes(n: Int) -> Effect[Bytes] — n random bytes. int is not for secrets; use bytes. |
Logger | info(msg: String) -> Effect[()] · error(msg: String) -> Effect[()]. |
Fetch | send(req: Request) -> Effect[Result[Response, FetchError]] — an outbound HTTP request. |
Secrets | get(name: String) -> Effect[Option[String]] — read configuration/secrets; None if unset. |
Locale | current() -> Effect[LocaleTag] — the locale to render messages in. On Cloudflare, negotiated from the request’s Accept-Language against a context’s message bundle; a fixed "en" on every other platform, and on Cloudflare without a detectable bundle. See Understand localisation. |
Idempotency | dedup[T](key: String) -> Effect[Option[T]] · remember[T](key: String, value: T, expiresAfter: Duration) -> Effect[()] — mechanical dedup for at-least-once delivery. See below. |
Events | emit[E](event: E) -> Effect[()] — emit an event declared by the calling context, fire-and-forget. See below. |
Fetch sends a request over the public internet. On Cloudflare that holds even
when the URL is another Worker on the same account, such as a sibling Bynk
service’s workers.dev address, because every emitted Worker carries the
global_fetch_strictly_public compatibility flag (see
[workers]). Contexts in the same project don’t
need Fetch for each other: consumes reaches them through Service Bindings.
The Idempotency capability
Section titled “The Idempotency capability”Idempotency makes at-least-once delivery (a retried command, a replayed event) safe:
check dedup for a cached outcome, and on a miss, cache the value you compute with
remember so a later call with the same key gets it back instead of recomputing:
on reserve(orderId: OrderId) -> ReserveOutcome given Idempotency { let cached <- Idempotency.dedup[ReserveOutcome](Json.encode(orderId)) match cached { Some(outcome) => outcome, None => { let outcome = ... compute the real outcome ... let _ <- Idempotency.remember[ReserveOutcome](Json.encode(orderId), outcome, 24.hours) outcome } }}Both operations take their type argument explicitly (dedup[ReserveOutcome],
remember[ReserveOutcome]) — always, even where an argument’s type would otherwise make
it obvious, since a capability operation’s own type parameter is never inferred (see
Capabilities & providers). Calling
dedup alone does not cache anything — remember is what writes the entry, so
forgetting to call it after a cache miss means that key is recomputed every time, not an
error. The shipped provider is a single in-memory map, identical on every platform: state
is lost on process restart, and there is no durability or multi-instance sharing yet — a
durable, platform-native provider is a named but unfiled future direction (see
the track’s own settling notes
for why that’s a separate axis from portability, not a variant of it).
The key you pass is automatically scoped to the calling handler’s own qualified name
before it reaches the provider — Idempotency.dedup[T]("order-1") inside on call of
service ordering in context shop.reserve actually stores against
"shop.reserve.ordering.call::order-1". Two unrelated handlers can therefore never
collide on the same literal key by accident; only two calls that are genuinely the same
call site (including the same call reached via a cross-context alias) ever share a scope.
This doesn’t stop two different callers of the same handler from colliding if the key
you supply doesn’t itself distinguish them (two tenants both calling reserve with the
same order ID, say) — differentiating those is still on you, the same discipline any
caller-supplied idempotency key requires.
The Events capability
Section titled “The Events capability”Events emits an event — a typed fact a context declares — for any number
of other contexts to subscribe to with from Events(E):
context commerce.order
exports transparent { PaymentConfirmed }consumes bynk { Events }
event PaymentConfirmed = { orderId: String,}
service markPaid { on call(orderId: String) -> Effect[()] given Events { Events.emit[PaymentConfirmed](PaymentConfirmed { orderId: orderId }) }}emit’s type argument is explicit, like Idempotency’s operations above —
never inferred from the value’s type. Only the context that declares an event
may emit it, even though the type is visible cross-context for subscription
(bynk.event.emit_outside_owner otherwise); emission is released only if the
emitting handler itself commits, so an aborted handler emits nothing. See
Understand events for the full
model, the subscriber side (from Events(E), on event), and how delivery
differs between Cloudflare and Bundle targets.
The bynk unit also exports the transparent types these operations use:
type Uuid = String where Matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")type Method = enum { Get, Post, Put, Delete }type FetchError = enum { Network, Timeout, InvalidHeader }
type Request = { method: Method, url: String, contentType: Option[String], authorization: Option[String], headers: Map[String, String], body: Option[String],}
type Response = { status: Int, headers: Map[String, String], body: String,}
type EventEnvelope = { eventId: String, publisherId: String, emittedAt: Instant, schemaVersion: Int,}EventEnvelope is an on event handler’s optional second parameter — see
Understand events.
Request and response headers
Section titled “Request and response headers”contentType and authorization are typed slots for the two common request
headers. Any other header goes in headers; a record field has no default, so a
request that sends none says so with headers: Map.empty():
let req = Request { method: Get, url: "https://api.github.com/repos/bynk-lang/bynk", contentType: None, authorization: None, headers: Map.empty() .insert("user-agent", "bynk-examples/1.0") .insert("accept", "application/vnd.github+json"), body: None,}Header names are case-insensitive. Fetch.send returns Err(InvalidHeader)
without sending anything when headers:
- names
Content-TypeorAuthorizationwhile the matching typed slot isSome— neither silently wins. While the slot isNone,headersmay supply that header itself; - names a header the platform owns:
host,content-length,connection,keep-alive,te,trailer,transfer-encoding,upgradeorexpect; - names the same header twice in different case (
X-Idandx-id); or - holds a name or value that is not a legal HTTP header (a newline in a value, say).
A typed slot whose value is not a legal header value (a secret read with a
stray newline) is Err(InvalidHeader) too, even with an empty headers.
Retrying cannot fix any of these, which is why they are not Network.
Response.headers carries the response’s headers with lowercased keys, so
res.headers.get("etag") or res.headers.get("retry-after") reads one
whatever case the server sent; a header the server repeated arrives as one
value, joined with ", ". That includes set-cookie, and there the join is
lossy: a cookie’s Expires attribute contains a comma
(Expires=Wed, 09 Jun 2027 10:18:14 GMT), so a joined set-cookie cannot be
split back into its cookies reliably. A stub Fetch.send(_) returns Ok(Response { … }) in a test
gives it headers: Map.empty() (or the headers the case needs), and an
expect Fetch.send called once with req.headers.get("user-agent") == Some("…")
matches on what was sent.
The Cloudflare surface — bynk.cloudflare
Section titled “The Cloudflare surface — bynk.cloudflare”consumes bynk.cloudflare { … } exposes the platform’s own infrastructure.
Consuming it locks the deployment unit to Cloudflare (it cannot then target
node), and the generated wrangler.toml gains the matching binding stanza.
| Capability | Operations |
|---|---|
Kv | get(key: String) -> Effect[Option[String]] · put(key: String, value: String) -> Effect[()] · putTtl(key: String, value: String, ttlSeconds: Int) -> Effect[()] · delete(key: String) -> Effect[()] · list(prefix: Option[String]) -> Effect[List[String]]. |
Service (experimental) | send(req: Request) -> Effect[Result[Response, FetchError]]. Used only through a named instance; see below. |
R2 (experimental) | get(key) -> Effect[Option[Bytes]] · head(key) -> Effect[Option[R2Object]] · put(key, body: Bytes) -> Effect[R2Object] · putWith(key, body, options: R2Put) -> Effect[Option[R2Object]] · delete(key) -> Effect[()] · list(prefix: Option[String], cursor: Option[String]) -> Effect[R2Listing]. Used only through a named instance; see below. |
RateLimit (experimental) | allow(key: String) -> Effect[Bool]. Used only through a named instance that sets its limit and window; see below. |
EdgeCache (experimental) | purge(prefixes: List[String]) -> Effect[Bool]. Used only through a named instance, at most one per context, which turns Cloudflare’s edge cache on for the context’s Worker; see below. |
Kv is backed by a Worker KV namespace bound as env.KV; the
[[kv_namespaces]] stanza is derived for you.
Several namespaces: derived capabilities
Section titled “Several namespaces: derived capabilities”For more than one namespace, declare each as a derived capability:
capability Sessions = Kv is a distinct instance of Kv, with Kv’s
operations and its own namespace, bound as env.KV_SESSIONS. A context may use
the bare Kv alongside its instances, and the bare KV stanza is emitted only
when it does. See Derived capabilities.
Calling a Worker outside the project: Service
Section titled “Calling a Worker outside the project: Service”Service calls a Worker that is not part of the Bynk project over a Service
Binding: Cloudflare’s private channel, not the public internet that Fetch
uses. Each Worker is a named instance that names it, and a bare
given Service is refused (bynk.capability.instance_only):
consumes bynkconsumes bynk.cloudflare { Service }
capability Billing = Service @worker("legacy-billing")Billing binds as env.SERVICE_BILLING, a [[services]] entry with
service = "legacy-billing". send has Fetch.send’s shape: the same
Request, Response and FetchError, and the same header rules. The call is
untyped; a typed client is ordinary Bynk over it. Service is experimental, so
list "bynk.cloudflare.Service" under [unstable] allow. See
Call a Worker outside the project.
Object storage: R2
Section titled “Object storage: R2”R2 stores objects in an R2 bucket. Each bucket is a named instance, and a
bare given R2 is refused (bynk.capability.instance_only):
consumes bynk.cloudflareconsumes bynk.cloudflare { R2 }
capability Archives = R2capability Indexes = R2 @jurisdiction("eu")Archives binds as env.R2_ARCHIVES, an [[r2_buckets]] entry. The second
consumes line names the types below. @jurisdiction("eu") (or "fedramp")
pins the bucket’s jurisdiction when bynk deploy creates it; any other value
is bynk.capability.invalid_option.
Objects are read and written whole, as Bytes, buffered in the Worker’s
memory, so keep each well under the 128 MB isolate limit; streamed reads and
writes are a later increment.
getandheadgiveNonefor a missing key.headreturns the object’sR2Object:key,size,etag,uploaded(anInstant),contentType(Option[String]) and custommetadata(Map[String, String]), without the body.deleteis idempotent.putwrites unconditionally and returns the newR2Object.putWithtakes anR2Put { contentType, metadata, onlyIf }.onlyIfis anR2Condition:Always,Absent(create only: what content-addressed storage needs) orEtagMatches(etag)(compare and swap: what rewriting an index needs). It returnsNone, writing nothing, when the condition fails.R2Conditionis extensible, so amatchover it needs a wildcard.listreturns oneR2Listing { keys, cursor }: up to 1000 objects in key order, optionally under a prefix. Pass the previous page’scursorfor the next;Noneon the last page. A page may be shorter than 1000 while more remain, so trust the cursor, not the length.
Each instance has its own in-memory bucket in tests, honouring conditions,
metadata and paging, so stub Archives.get(_) and expect Archives.get called
work as for any capability. R2 is experimental: list "bynk.cloudflare.R2"
under [unstable] allow. bynk deploy creates and names the buckets; see
Deploy to Cloudflare.
Rate limiting: RateLimit
Section titled “Rate limiting: RateLimit”RateLimit counts calls per key over a fixed window, on Cloudflare’s Rate
Limiting binding. Each limiter is a named instance that sets both its limit and
its window, and a bare given RateLimit is refused
(bynk.capability.instance_only):
consumes bynk.cloudflareconsumes bynk.cloudflare { RateLimit }
capability PublishLimit = RateLimit @limit(20) @period(60.seconds)capability ReadLimit = RateLimit @limit(1000) @period(10.seconds)PublishLimit binds as env.RATELIMIT_PUBLISH_LIMIT, a [[ratelimits]]
entry with simple = { limit = 20, period = 60 }.
@limit(n)is the number of calls allowed per key in one window: a positiveIntliteral.@period(d)is the window:10.secondsor60.seconds(1.minutesis the same), the only two the platform offers.- Both are required (
bynk.capability.missing_option). A limit that isn’t a positiveIntliteral, or another window, isbynk.capability.invalid_option.
allow(key) counts one call for key and gives true while key is within
the limit, false once it is over. Refuse the request, typically with
TooManyRequests:
on PUT("/api/:token") (token: String) -> Effect[HttpResult[String]] by Visitor given PublishLimit { let allowed <- PublishLimit.allow(token) if allowed { Ok("published") } else { TooManyRequests("slow down: 20 publishes a minute per token") }}Counting is approximate. Cloudflare keeps the counts per location, and they are eventually consistent, so a key can get somewhat more than its limit, especially from many locations at once. That suits abuse and cost control. For an exact quota (billing, a hard per-account cap), use an agent, one per key, as the rate limiter example does. Each instance counts on its own: two limiters never share counts.
In tests each instance is a counter per key over windows of its @period, on
the harness clock, so a case can
exhaust a limiter and recover with advance Clock by. stub PublishLimit.allow(_)
and expect PublishLimit.allow called work as for any capability. RateLimit
is experimental: list "bynk.cloudflare.RateLimit" under [unstable] allow.
bynk deploy allocates each limiter’s namespace_id; see
Deploy to Cloudflare.
Edge caching: EdgeCache
Section titled “Edge caching: EdgeCache”@cache on its own only sets Cache-Control: Cloudflare doesn’t store a
response a Worker generates because of that header. EdgeCache turns on
Cloudflare’s edge cache (Workers Cache) for the context’s Worker, and purges it.
A context declares at most one instance (a second is
bynk.cloudflare.edge_cache_duplicate), and a bare given EdgeCache is
refused (bynk.capability.instance_only):
consumes bynk.cloudflare { EdgeCache }
capability Edge = EdgeCacheThe instance has no binding. Declaring it emits [cache] enabled = true in the
Worker’s wrangler.toml. A context without one emits exactly what it did before.
An opted-in Worker answers as follows:
| Response | Cache-Control sent |
|---|---|
a 2xx from a GET with @cache | the route’s, as without EdgeCache |
any other response to a GET or HEAD (no @cache, or not a 2xx, a 404 included) | no-store |
a 304 | copied from the 200 it stands for |
| any other method | unchanged (the cache stores only GET and HEAD) |
Cloudflare serves a cached response without running the Worker until its
max-age runs out. Only scope: public is shared there, and it is shared
even when the request carried Authorization (public allows that, RFC 9111
§3.5). The cache keys a response by its URL, so a @cache(scope: public)
route must answer every caller alike. One whose by
names an actor that authenticates is
bynk.cloudflare.edge_cache_public_authenticated, so use scope: private
there. A Worker that negotiates its Locale from Accept-Language sends
Vary: accept-language on its @cache routes, and the edge keeps one
response per language.
purge(prefixes) removes every cached response whose path starts with one of
prefixes, everywhere. It gives true once Cloudflare has accepted the purge
and false when it refused one; Cloudflare rate-limits purges at its Free-tier
limits on every plan, so the caller retries. Each prefix is a path starting
with /, with no query, fragment or control characters, and anything else
faults. An empty list purges nothing and gives true. A content-addressed path
has a fixed length, so purging it as a prefix removes exactly that response:
on POST("/takedown/:hex") (hex: String) -> Effect[HttpResult[Bool]] by v: Visitor given Edge { let purged <- Edge.purge(["/dl/sha256/\(hex)"]) Ok(purged)}A purge reaches the cache only from the Worker’s own fetch, so Edge is
used from service handlers and the providers and functions they call. An agent
handler that reaches it, directly or through a provider’s given, is
bynk.cloudflare.edge_cache_in_agent, because an agent runs in a Durable
Object. A queue consumer that reaches it is
bynk.cloudflare.edge_cache_in_queue, and an on schedule handler is
bynk.cloudflare.edge_cache_in_schedule: Cloudflare accepts a cron run’s purge
but it clears nothing. An exported capability whose provider
reaches it is bynk.cloudflare.edge_cache_exported: a consuming context would
build that provider in its own Worker. Export a service that purges instead.
In tests, each instance’s double accepts every purge.
stub Edge.purge(_) returns false refuses one, and
expect Edge.purge called with prefixes == ["/dl/sha256/abc"] checks what was
purged. Under bynk dev there is no edge cache: every request runs the
Worker, and purge gives true after logging
bynk: EdgeCache.purge is a no-op under bynk dev. EdgeCache is
experimental: list "bynk.cloudflare.EdgeCache" under [unstable] allow. See
Cache responses at Cloudflare’s edge.
Note — producing queue messages (
send/sendBatch) is not yet a first-party capability. Consuming a queue is a separate entry-point feature: see Process a queued message and the Queue reference.
Cryptography — bynk.crypto
Section titled “Cryptography — bynk.crypto”consumes bynk.crypto { Crypto } brings in digests, HMAC, constant-time
comparison, and signing and verification with imported keys. The unit is
experimental: list bynk.crypto under [unstable] allow
to use it. It runs on all three platforms, over crypto.subtle and nothing else.
No third-party crypto runs and nothing is polyfilled.
| Operation | Result |
|---|---|
digest(algorithm: Digest, data: Bytes) | Effect[Bytes] — SHA-256, SHA-384 or SHA-512. |
hmac(algorithm: Digest, key: Bytes, data: Bytes) | Effect[Bytes]. The key must not be empty. |
verifyHmac(algorithm: Digest, key: Bytes, data: Bytes, mac: Bytes) | Effect[Bool], compared in constant time. |
equal(a: Bytes, b: Bytes) | Effect[Bool], compared in constant time for equal lengths. |
importSigningKey(algorithm: Signature, pkcs8: Bytes) | Effect[Result[SigningKey, CryptoError]]. |
importVerifyKey(algorithm: Signature, key: Bytes) | Effect[Result[VerifyKey, CryptoError]] — SPKI, or the 32 raw bytes of an Ed25519 key. |
importVerifyJwk(jwk: String) | Effect[Result[VerifyKey, CryptoError]] — the algorithm comes from the key’s kty and crv. |
sign(key: SigningKey, data: Bytes) | Effect[Bytes]. |
verify(key: VerifyKey, data: Bytes, signature: Bytes) | Effect[Bool]; a malformed signature is false. |
Digest is Sha256 | Sha384 | Sha512. Signature is Ed25519 | EcdsaP256Sha256 | RsaPkcs1Sha256: ECDSA over P-256 with SHA-256, signing the raw
r || s form, and RSASSA-PKCS1-v1_5 with SHA-256. The set is what all three
platforms support. CryptoError is InvalidKey(reason) or
UnsupportedAlgorithm(name), and carries no key material. All three are
extensible, so a match over them needs a wildcard.
Effects, not pure functions. crypto.subtle is asynchronous on every
platform, so every operation is an Effect and there is no hashing in pure
functions, refinements or invariants. Encodings (toHex, toBase64,
toBase64Url and their decoders) stay pure on Bytes.
Keys are handles. SigningKey and VerifyKey are opaque: the platform holds
the key non-extractable, so its bytes can’t be read back. Like a Stream, a key
lives for the call that imported it. It can’t be stored, serialised, logged or
sent, so using one in a store, a record or event field, a handler parameter or
result, or a wire type is bynk.types.key_at_boundary. Keep the key’s bytes in
Secrets and import them where you sign or verify.
Timing. verifyHmac, verify and equal don’t return early on the first
differing byte, so they are the way to compare a secret, a MAC, a signature or a
token’s digest. equal uses the platform primitive where there is one
(crypto.subtle.timingSafeEqual on Workers, crypto.timingSafeEqual on Node),
and in a browser it HMACs both values under a fresh random key and compares the
results with subtle.verify. Bytes.compare and == are not constant-time and
must never compare secrets. digest, hmac and sign run in time that doesn’t
depend on the data in a way a program can exploit.
The browser. Web Crypto exists only in a secure context (HTTPS or
localhost). Elsewhere an operation fails loudly, naming the requirement; it is
never replaced by something weaker. An algorithm a platform lacks, such as
Ed25519 in an older browser, is an Err(UnsupportedAlgorithm("Ed25519")), by
name.
In tests, a unit-tier case runs the real cryptography, because digests, MACs
and signatures are deterministic for given inputs; stub Crypto.digest(_, _) returns … and expect Crypto.digest called once work as usual. Random.bytes
is deterministic under test, like Random.int.
Not in this unit: encryption and decryption, key generation and export, key derivation, JWT and JOSE helpers, and X.509. The runtime’s Bearer and OIDC verifiers are unchanged. See Sign and verify data.
Consuming a first-party capability
Section titled “Consuming a first-party capability”Consume the unit, then grant the capability with given in each handler that
calls it — exactly as you would a capability you declared yourself:
context greeter
consumes bynk { Clock, Logger }
service api from http { on GET("/now") () -> Effect[HttpResult[Int]] by Visitor given Clock, Logger { let t <- Clock.now() let _ <- Logger.info("checked the clock") Ok(t.toEpochMillis()) }}The Cloudflare surface is consumed the same way — consumes bynk.cloudflare { Kv }
and given Kv — with the portability trade-off noted above. To configure a
first-party capability (an API key for Fetch, say), read it from Secrets
rather than passing it as an argument; Wrap a library as an adapter
shows the pattern.
Stability
Section titled “Stability”Every first-party item, whether a unit, an item it exports or a capability operation, is stable or experimental.
- A stable item is under the compatibility promise. The kernel and the
bynk.*commons change only at a major release. A capability (inbynkorbynk.cloudflare) only grows within a major version: it may gain items and operations, but an existing one is removed or reshaped only at a major release, after a deprecation warning. The one exception: abynk.cloudflareitem may change in a minor release when Cloudflare withdraws or changes the feature under it, and that release’s notes say so. - An experimental item carries no promise. To use one, list it under
[unstable] allowinbynk.toml.
The experimental items today are bynk.cloudflare’s Service, R2,
RateLimit and EdgeCache, and the
whole of bynk.crypto, which graduates once the first-party registry has signed
with it in production. New platform capabilities may arrive experimental first.
Extensible enums
Section titled “Extensible enums”FetchError and Method, and bynk.crypto’s Digest, Signature and
CryptoError, are extensible: they may gain variants in a minor release. Listing every variant they have today is not enough, so a match that
covers one, at any depth, needs a wildcard:
context app.probe
consumes bynk { Fetch }
service probe { on call(url: String) -> Effect[String] given Fetch { let res <- Fetch.send(Request { method: Get, url: url, contentType: None, authorization: None, headers: Map.empty(), body: None }) match res { Ok(r) => Effect.pure("ok") Err(Network) => Effect.pure("network") Err(Timeout) => Effect.pure("timeout") Err(InvalidHeader) => Effect.pure("header") } }}Add the wildcard, Err(_) here, and the match keeps compiling when a new
variant arrives:
context app.probe
consumes bynk { Fetch }
service probe { on call(url: String) -> Effect[String] given Fetch { let res <- Fetch.send(Request { method: Get, url: url, contentType: None, authorization: None, headers: Map.empty(), body: None }) match res { Ok(r) => Effect.pure("ok") Err(Timeout) => Effect.pure("retry later") Err(_) => Effect.pure("failed") } }}An enum of your own with the same name is not extensible; the rule follows the first-party enum, not the name.
Related first-party modules
Section titled “Related first-party modules”Beyond capabilities, the bynk namespace also ships pure commons you bring
in with uses (not consumes) — bynk.list and bynk.map (combinators over
the List/Map kernels) and bynk.string (string helpers). These are ordinary
functions with no effects; see the type system reference.
Two more serve the Locale capability above.
bynk.locale.types is a dependency-free leaf declaring LocaleTag,
Message and MessageArg — uses it wherever you only need to name those
types, which is all a context calling Locale.current() requires.
bynk.locale declares the value-level API: the message/withText/
withWhole/withNum/withMoment builders and the bundle-free render
fallback. A commons declaring a messages bundle needs both; a context
consuming that bundle should uses only bynk.locale.types, since
bynk.locale and every message-bundle commons both export a render. See
Localisation.
Deprecated (v0.91, ADR 0116 D6): the
bynk.listfree functions whose method forms now exist —map,filter,find,any,all— emit a non-failingbynk.list.deprecated_functionwarning at each call site (the build still succeeds) with a machine-applicable fix to the method form:map(xs, f)→xs.map(f),find(xs, p)→xs.filter(p).first(), and so on. Prefer theListmethods.reverseandtraversekeep their free-function form (no method equivalent yet).
See also: Capabilities & providers, Adapters, Understand the capability model.