Why this document exists

Everything below was read against source and production data, not against documentation. That distinction earned its keep: in the course of this review, two code comments and one set of acceptance criteria were confidently wrong, and one "obvious simplification" would have taken down three services.

IMPORTANT: Where docs, code comments and the running system disagree, the running system is the authority. Anything learned here should be written back to tawa-docs rather than re-derived.

Verified 2026-08-27 against iec-janus, iec-relay, iec-koko, iec-builder, the live cluster, and relay-prod message data.

The three identity axes

Every internal call carries three distinct identities. Two are separated correctly; the third is misnamed and that misnaming has caused real bugs.

AxisHeaderActually carriesAnswers
ServiceX-Service-IDOAuth client_id (e.g. policysign-prod)Who is calling; who gets a callback
Billing orgX-Caller-OrgorgSlug, after X-Forward-User resolutionWho pays
Caller orgX-Program-IDorgSlug — despite the nameWhich org the calling pod belongs to

WARNING: X-Program-ID does not carry a program or service id. Janus sets auth.programId = orgSlug in internalProxyAuthMiddleware. Anything using it as a service identifier is reading the wrong namespace.

Wallets are org-level. deriveWalletId() is literally wallet-{orgSlug} — services do not have wallets, orgs do. That is why billing resolves to an org, and it is correct.

Rule of thumb: bill the org, because that holds the wallet. Call back the service, because that is what registered. Never let one substitute for the other because a header was missing.

Gas is resolved entirely inside Janus

A recurring wrong guess is that downstream services re-verify tokens for billing. They do not. iec-relay contains no gas, wallet or billing code of any kind.

The whole path runs before the proxy hop, from the identity established by the first verification:

  1. resolveCallerOrg() — billing org from the JWKS-verified service token, or a JWKS-verified X-Forward-User for pass-through billing
  2. internalProxyGasCheckMiddleware — derives wallet-{orgSlug}, checks balance; platform org bypasses, /internal/* is free infrastructure
  3. gasSettleMiddleware — debits 1 token on a 2xx only; non-2xx and timeouts are not charged

Scopes are NOT enforced at runtime on internal calls

This is the most important correction in this document.

iec-relay guards /send with requireScope('relay:send'). That check never fires on a normal call:

  • internalAuth sees Janus's X-Program-ID / X-Service-ID and returns next() without setting res.locals.serviceScopes
  • requireScope bypasses whenever serviceScopes is undefined

And Janus does not enforce scopes on /i/ either. Its middleware stack is auth → sandbox-relay-block → internal-path-block → gas-check → service-context → gas-settle → proxy. Its app-level authzMiddleware only acts when route.scopes is non-empty, and the /i/ route synthesizes routeConfig = { owner: serviceName } with no scopes.

The relay comment says "scopes enforced at the Janus layer." That delegation was never built.

So what do scope grants actually do?

They are a deploy-time gate, and a real one:

  • The builder blocks a cross-org deploy until the target org approves the grant
  • A scoped dependency provisions OAuth client-credentials (BIO_CLIENT_ID / BIO_CLIENT_SECRET)

They do not gate a request at runtime. Answering the common question directly: no, a service does not need relay:send in order to send. Every send routes through Janus, and the Janus path bypasses the check.

NOTE: Public routes are different. A route registered in Koko with declared scopes is enforced by authzMiddleware normally. The gap is specific to /i/.

"Scopes" means two unrelated things — this is the biggest trap in the system

The word is overloaded across two systems that share no data and behave differently. Verified against koko-prod contents:

Koko scopesBio-ID scopes
Declared inspec.scopes (owner)spec.modules[].scopes (owner), spec.dependencies[].scopes (consumer)
Stored inKoko available_scopes / scope_grantsBio-ID
GovernsDatabase accessresourceType: mongodb / redis / neo4j / usersAPI calls
Real effectA connection string env var is injected. Genuinely enforced.Deploy-time approval gate + client-credentials provisioning
Runtime enforcement on /i/n/aNone
Live count (prod)3 available, 2 grants — all databaseOnly relay publishes any: relay:send, relay:templates, relay:admin

WARNING: Koko's available_scopes has nothing to do with API scopes. Every row is a database grant. Do not read it expecting to find relay:send.

Only relay publishes API scopes. docman, bio-vault and septor publish none — so a catalog declaring scopes: [docman:generate] or [septor:emit] is naming something that exists nowhere. Those declarations are inert and nothing warns about them. byte-mga currently declares five API scopes of which exactly one (relay:send) is real.

Why real enforcement is not a small change

There is no data anywhere mapping a path on a service to a required scope. ServiceRoute.scopes covers public routes only. Nothing says "POST /send on relay requires relay:send" except a hard-coded line inside relay.

Enforcement is therefore a data-model change, not a middleware. Until that is decided, the honest move is to make requireScope log-only or remove it — unreachable code that reads as a security control is worse than no control.

The [] trap, if anyone does turn enforcement on

serviceScopes = result.scopes ?? [], and [] is truthy in JavaScript. A token that introspects successfully but carries no scopes does not hit the bypass — it falls to [].includes('relay:send') and 403s. Combined with the documented fire-and-forget .catch(logger.warn) pattern, that 403 is swallowed and the email silently never arrives. Roll out report-only first.

Do NOT strip the forwarded Authorization header

An earlier draft of this review recommended it, on the reasoning that Janus already authenticated the caller. It would have 401'd three services.

ServiceWhat it does with the inbound token
iec-docmanbio-auth.ts requires Bearer, verifies via Koko then Bio-ID, populates req.user (org, roles, email)
iec-bio-vaultrequireAuth rejects any request without a valid Bearer token
iec-raterspotResolves a RestPrincipal from the Bearer token or an X-API-Key fallback

The division of labour is deliberate and sound: Janus authenticates the calling service; the forwarded token lets the target service authorize the end user. Two different questions, both needing an answer.

The genuinely redundant piece is narrow — relay uses its Bearer branch to derive a service identity that Janus already passed in a header. That branch alone can go, inside relay.

Webhook callback routing

IWebhookSubscription stores ownerKey (routing) and an optional orgSlug (context) separately — the model already supports both axes.

callerIdentity() returns serviceClientId ?? X-Service-ID ?? X-Program-ID. The first two are a client_id; the third is an orgSlug. That fallback crosses namespaces and is an accident, not a feature — it does not let a caller choose org-scoped delivery, it swaps the namespace based on which token type was presented.

What production contains (relay-prod, 2026-08-27)

CheckResult
Live webhook subscriptions3 — policysign-prod, ballantyne-prod, portal-prod; all keyed on a service client_id
orgSlug on those subscriptionsinsureco on all three, stored separately
Messages stamped with an org slug0 of 10,751 — the collision is real in code but has never fired
Messages with no caller identity938, all 2026-06-01 → 06-23; historical, closed since June

Consequence: fixing ownerKey to be service-identity-only is not a migration. Every live subscription already uses that form.

Silent failure mode worth knowing

A dependency naming a service that is not in Koko logs a server-side warning at dependency-resolver.ts:88, skips it, and deploys anyway — no {SERVICE}_URL injected, nothing visible to the developer.

This bit iec-ledger (#282): it declared iec-relay, but the Koko service name is relay, so RELAY_URL was never injected and invite emails silently failed.

WARNING: Repo name ≠ service name, with no rule. iec-docmandocman, iec-relayrelay, iec-kokokoko, but iec-wallet, iec-cron and iec-pulse keep their prefix. Always check the Koko registry, never infer from the repo.

Tracked as iec-builder#80.

Koko registry — what the data actually looks like

Read from koko-prod on 2026-08-27.

Two registries that disagree

RegistryCount
iec-builder service list108
Koko services97
In both96

12 services exist in the builder but not in Koko. Most are disposable (my-api, test-service, test-sample-api, oauth-demo, e2e-test-site, my-san-site), but some are not: byte-mga-api, byte-mga-web, checkpoint-api, developer-portal, bio-samples, iec-builder. Anything declaring a dependency on one of those gets no URL injected and no error.

One stale entry sits only in Koko: ask-jefferson, from the retired Jefferson server.

Service records themselves are healthy — all 97 are active, none missing upstream, healthEndpoint or owner; 44 carry routes and 81 carry directDeps.

Live unresolvable dependencies

Every dependency in every catalog, checked against the registry: 62 resolve, 5 do not.

CatalogDeclaresReality
byte-mgaiec-bio-vaultShould be bio-vault. Its code reads IEC_BIO_VAULT_URL, which is never injected — the vault integration is dormant and returns IEC_BIO_VAULT_URL not configured
byte-mgaiec-storageNo such service. Object storage is spec.storage (MinIO + Vault), which byte-mga already declares — the dependency is a category error
policyeco-webjci-brainNot a Tawa service; JCI_BRAIN_URL must come from tawa config set
iec-books-2025/iec-ledgeriec-relayStale tree; the live iec-ledger already fixed this

Eleven of twenty-five collections are empty

domains, service_roles, deployments, plugin_enables, flags, promotions, plugin_docs, onboarding_specs, config_history, platformVersions, service_deploy_manifests.

Several correspond to features that exist in code and docs — promotion, deployment gates, the plugin store, onboarding specs, custom domains. Built, never populated. A future agent reading the code will reasonably assume these are load-bearing; they are not, and should be labelled aspirational or removed.

Doc claims to keep honest

Checked against this review; flagged for anyone maintaining tawa-docs:

ClaimStatus
"Every dependency goes through an approval gate" (reference/scopes)True at deploy time; the doc is silent on runtime, which is where readers assume enforcement
"Declare relay with scopes: [relay:send] … provisions the BIO credentials the SDK needs"True only for a service without auth: mode: sso; with SSO the credentials arrive anyway
branding: true maps to includeBranding: trueFalse, retracted 2026-08-27. Never implemented. Corrected in all four doc locations
"scopes enforced at the Janus layer" (relay code comment)False. No scope enforcement exists on /i/

Related

  • Full review artifact with diagrams and risk-ordered next steps
  • iec-builder#8 — service classification in Koko, default-deny egress
  • iec-builder#80 — make unresolved dependencies loud
  • tawa-cli#79 — preflight sourcing schema from the builder

Last updated: August 28, 2026