Reference · Jobs and services
accept_calls
accept_calls(map) -> nullWhat it does
At the top level of a service, says which calls from other services it takes and how
each proves who makes it. Written as a declaration, accept_calls { ... }, it takes:
keys: a key for each caller, by name,{ shop: env("SHOP_KEY") }. A caller that signs with its own key (service billing = "billing.qwe" { auth: { key, as: "shop" } }) is known by that name, and no other service can sign as it. A list of keys,[new, old], takes either while one is rotated; answers are signed back with the key the call was signed with.shared:falserefuses calls signed with the sharedQUILLO_SERVICE_SECRET, which every service holding it can sign as any other. Taken by default.jwt: bearer tokens, as an identity provider issues them for services (OAuth 2.0's client credentials).{ issuer, audience, jwks }checks the token's signature against the provider's keys (RS256, ES256, EdDSA; a URL, kept ten minutes), orsecretfor HS256.audienceis required, and so isexpin the token. A token a user signed in for, whosesubis the user and not the client, is refused (since 0.7): a service calls with its own, from the client-credentials grant. The same rules check a visitor's token that a caller passes on withforward, asrequest.user.client_cert: { header: "x-forwarded-client-cert" }: the certificate a proxy checked (mutual TLS ended at the proxy), read from its header only when the request comes from an address--trust-proxynames (an option ofquillo run:quillo run billing.qwe --trust-proxy 10.0.0.1; see the options). The name is the SPIFFE id's last part, or the subject's CN.check: (caller) => ...: anything else, in Quillo, run before the arguments are read. Onlytrueor a map accepts the call, and a map becomes the caller from then on. Anything else refuses it with a 403,falseand null among them (since 0.7: null, anifwith noelse, refused no one); an error value refuses it with its text, andreplyanswers the call itself.
Who called is request.caller in the server fn: { name, by }, by being
"quillo", "key", "jwt" or "mtls", with scopes and claims for a token and
subject and uri for a certificate. server fn refund(...) for ["shop"] takes calls
from those callers only (403 otherwise), and none from pages: /_quillo/rpc/refund
answers 404, and a page's script has no server.refund (since 0.7: anyone could call
it there). It gives null.
Examples
// billing.qweaccept_calls { keys: { shop: [env("SHOP_KEY"), env("SHOP_KEY_OLD")], notify: env("NOTIFY_KEY") }, shared: false, jwt: { issuer: env("IDP"), audience: "billing", jwks: env("IDP") + "/.well-known/jwks.json" }, client_cert: { header: "x-forwarded-client-cert" }, check: (caller) => caller.name != "mallory",}server fn refund(account: int, cents: int) for ["shop"] { { refunded: cents, by: request.caller.name }}server fn whoami() = { caller: request.caller, user: request.user, forwarded: request.forwarded }Both sides, with a key for one caller: billing takes calls signed with the shop's key and no others, and the shop declares the service with that key and calls it as a function of its own:
// billing.qweaccept_calls { keys: { shop: env("SHOP_KEY") }, shared: false }server fn refund(account: int, cents: int) for ["shop"] { { refunded: cents, account, by: request.caller.name }}// shop.qweservice billing = "billing.qwe" { auth: { key: env("SHOP_KEY"), as: "shop" } }route POST /orders/:id/refund { let r = billing.refund(int(id), 1200) ?? reply 502 "Billing refused the refund" <p>Refunded {r.refunded} cents to account {r.account}, as {r.by}</p>}SHOP_KEY=... quillo run billing.qwe --port 8081SHOP_KEY=... quillo run shop.qwe --service-url billing=http://localhost:8081A post to the shop's /orders/42/refund answers Refunded 1200 cents to account 42, as shop. A program that declares billing with no auth, and so signs with the shared
secret, gets billing's 401 as an error value (service billing: 401 this service takes no calls signed with the shared secret (accept_calls { shared: false })), which its
visitor sees as a 500 unless the route answers it. Run in one process (quillo run shop.qwe alone), the call is a function's call: nothing is checked, and
request.caller is null.
print(accept_calls({ keys: { shop: "shop-key-0123456789abcdef0123456789" } }))nullEdge cases
- With neither
QUILLO_SERVICE_SECRETnoraccept_calls, a service takes no calls (403). Withaccept_callsand no secret, only what it names proves a caller. - A call signed with the shared secret names its caller from version 3 of the
signature on; one of version 2 has an empty name, so
for [...]refuses it. - A token without
exp, for another audience or issuer, expired, or not good yet (nbf) is refused with a 401 that says which. - The header named in
client_certsent by anyone else is refused, not believed. - A
checkwritten as an allow-list with noelserefuses everyone it does not name:(c) => { if c.name == "shop" { return true } }gives null for the rest. - A call sent again is answered as it was only when it is the same caller's, to the same function, for the same tenant, with the same arguments (since 0.7: by its id alone).
print(accept_calls({ shared: false }))nullErrors
An option it does not take, an empty key (an environment variable not set), a jwt
without audience or without jwks or secret, or accept_calls with nothing in it,
is a runtime error as the program loads.
accept_calls { jwt: { secret: "s" } }line 1: `accept_calls`: `jwt` needs `audience`: a token for another service must not be taken hereaccept_calls { keys: { shop: "" } }line 1: `accept_calls`: the key of `shop` is empty (is its environment variable set?)accept_calls {}line 1: `accept_calls` with nothing in it takes what it took before: give keys, jwt, client_cert, check or shared: falseChaining
It is a declaration, written once at the top level of the service, not piped into or out of. Its keys come from the environment:
accept_calls { keys: { shop: split(env("SHOP_KEYS") ?? "", ",") } }See also
later.status, ready, publish, env
Thread 0 posts
On accept_calls(): questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.
Edit this page on GitHub · every example's output was made by running it (tools/refcheck.mjs) · questions on Discord or r/quillo