Actors
An actor is a nominal boundary contract — an authentication scheme plus an
optional sealed identity — that a handler consumes on its by clause. The
compiler emits the verification at the boundary, before the body runs,
fail-closed. Actors are declared inside a context. For task-oriented recipes,
see Guides — Actors & access control.
Declaration
Section titled “Declaration”actor <Name> { auth = <Scheme>(<config>) , identity = <Type> } // base actoractor <Name> = <Base> where <predicate> // refinementSchemes
Section titled “Schemes”A closed, compiler-known set. auth = <Scheme> with keyed config where noted.
| Scheme | Config | Identity | Protocols | Verification |
|---|---|---|---|---|
None | — | () | any | none (anonymous; the prelude Visitor) |
Bearer | secret = "<ENV>" | a string-constructible type | http | JWT HS256 over Authorization: Bearer …; sub → identity; 401 |
Signature | secret, header, optional timestamp + tolerance | none | http | HMAC-SHA256 over the raw body; 401 |
Oidc | issuer, audience, jwks (all URLs/ids; no secret); optional trustFrom | a string-constructible type | http | RS256/ES256 JWT verified against the provider’s JWKS over Authorization: Bearer …; iss/aud/exp checked; sub → identity, and the verified claims → claims; 401 |
Internal | — | (), or CallerId for Caller | call/cron/queue | a call’s per-edge signature proves the calling context; cron and queue are platform-dispatched |
Foreign | secret = "<ENV>", optional worker = "<name>" | none | call | the caller, a Worker that is not Bynk, signs the call as the actor under the secret (wire format); 401 unsigned, 403 where the handler does not admit it |
secretvalues name an environment variable (the source theSecretscapability reads), not the key itself.- A
BearerorOidcactor’sidentitymust be a context-owned, string-constructible type, so the minted value is sealed to the context. - A
Signatureactor takes noidentityand a handler using it must take abody; it is HTTP-only. - An
Oidcactor names no secret — its trust root is the provider’s public JWKS. It is HTTP-only and, this slice, single-actor only (not a sum member, not a refinement base);alg: noneand symmetricHS*are rejected. - A
Foreignactor is a caller that is not Bynk, declared by the context it calls. Its name starts with a capital letter, so it never reads as a context’s; it takes noidentity(the actor is the identity); itssecretmust be a string, may not be aBYNK_CALL_edge key’s name, and may not be any other actor’s secret in the context.bynk deploymints the secret, sets it on the context, and prints thewrangler secret putfor the foreign Worker’s operator, naming the Worker whenworkeris given.
Reading claims
Section titled “Reading claims”An Oidc actor’s binder carries the verified token’s claims beside its
identity, as a TokenClaims:
on POST("/release") () -> Effect[HttpResult[String]] by w: Workload { let repository = w.claims.get("repository") // Option[String] let protected = w.claims.has("environment") // Bool Ok(repository.getOrElse("-"))}| Method | Returns | Reads |
|---|---|---|
get(name) | Option[String] | the claim when it is a JSON string; None for any other type, or none at all |
has(name) | Bool | whether the claim is present, of any type |
Every claim comes from a token whose signature, iss, aud and exp the seam
verified; a token that fails is 401 before the handler runs. The claims are
read where the token arrives and never kept: a TokenClaims can’t be a field, or
cross a call, a message or an agent (bynk.types.claims_at_boundary). Read the
claim you need and keep that String. Only an Oidc binder has .claims;
Bearer claims are a later addition.
Trust roots from the environment
Section titled “Trust roots from the environment”trustFrom = "<ENV>" names an environment variable whose JSON, when it is set,
replaces the declared trust roots at run time:
actor Workload { auth = Oidc( issuer = "https://token.actions.githubusercontent.com", audience = "bynk-registry", jwks = "https://token.actions.githubusercontent.com/.well-known/jwks", trustFrom = "WORKLOAD_OIDC_TRUST" ), identity = WorkloadId}{ "issuer": "http://127.0.0.1:8123", "jwks": "http://127.0.0.1:8123/jwks", "audience": "bynk-registry" }issuer and jwks are required, audience optional. Unset, the declaration
applies, so production behaviour is exactly what the actor says; set but
malformed, every request is refused (401) and the fault log says why — it never
falls back to the declaration. It is how a test points the actor at a fake
issuer, and how a staging deployment trusts a staging one. Like a secret, the
variable is the operator’s configuration. trustFrom must be a string
(bynk.actor.oidc_trust_from_not_string) naming a variable no actor uses as its
secret (bynk.actor.oidc_trust_from_is_secret).
The by clause
Section titled “The by clause”by <binder>: <Actor> // capture the identity as <binder>.identityby <Actor> // verify, capture nothing (optional binder)by <binder>: <A> | <B> | … // an ordered sum of peer actors, first-wins- HTTP requires a
byclause (bynk.actor.missing_by_on_http); a public route writesby v: Visitor. The internal protocols default:call→Caller, cron →Scheduler, queue →Producer. - Multi-actor sums resolve first-wins; the body
matches the resolved actor. Peers are distinguished by scheme (no duplicates), aNonecatch-all is last, refinements are not members, and total failure is401. Oncall, a sum admits a Bynk context (Caller) and declared foreign callers, each told apart by name:by who: Caller | Legacy | Ledger. SeveralForeignpeers may share a sum; each has its own secret. - Refinement actors (
<Admin> = <User> where <pred>) add an authorisation invariant over aBearerbase: scheme failure is401, predicate failure is403. The predicate is a closed set —hasClaim("n"),claimEquals("n","v"), composed with&&/||/!. AnAdminis usable wherever its base is. Caller(onon call) yields the calling context’s qualified name asCallerId. It admits Bynk contexts only: a foreign caller is admitted by its own actor, never byCaller, so a handler withoutbyrefuses one (403).
Prelude actors
Section titled “Prelude actors”| Name | Scheme | Identity | For |
|---|---|---|---|
Visitor | None | () | anonymous HTTP routes |
Caller | Internal | CallerId | the calling context (on call) |
Scheduler | Internal | () | cron |
Producer | Internal | () | queue |
Diagnostics
Section titled “Diagnostics”All bynk.actor.* codes are in the diagnostic index — among
them missing_by_on_http, scheme_not_admissible, signature_requires_body,
sum_requires_binder, duplicate_sum_scheme, unreachable_sum_arm,
refinement_base_unsupported, refinement_predicate_unsupported,
oidc_missing_issuer, oidc_missing_audience, oidc_missing_jwks,
oidc_identity_not_string_constructible, oidc_not_in_sum,
foreign_missing_secret, foreign_reserved_secret,
foreign_name_is_context_name, foreign_identity_unsupported,
foreign_secret_not_string, and foreign_duplicate_secret.
Who, not whose
Section titled “Who, not whose”An actor answers who is at the boundary — it authenticates a party and seals
its identity. It deliberately does not answer whose a given object is:
object-level authorisation (may this user read this record?) is domain logic
in the handler body, by design. A where-clause invariant narrows who (a claim
the party carries), never whose.
The normative rules are Specification §5.7a; the emitted verification is §7.3.4a.