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-docsrather than re-derived.
Verified 2026-08-27 against iec-janus, iec-relay, iec-koko, iec-builder, the live cluster, and relay-prod message data.
Every internal call carries three distinct identities. Two are separated correctly; the third is misnamed and that misnaming has caused real bugs.
| Axis | Header | Actually carries | Answers |
|---|---|---|---|
| Service | X-Service-ID | OAuth client_id (e.g. policysign-prod) | Who is calling; who gets a callback |
| Billing org | X-Caller-Org | orgSlug, after X-Forward-User resolution | Who pays |
| Caller org | X-Program-ID | orgSlug — despite the name | Which org the calling pod belongs to |
WARNING:
X-Program-IDdoes not carry a program or service id. Janus setsauth.programId = orgSlugininternalProxyAuthMiddleware. 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.
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:
resolveCallerOrg() — billing org from the JWKS-verified service token, or a JWKS-verified X-Forward-User for pass-through billinginternalProxyGasCheckMiddleware — derives wallet-{orgSlug}, checks balance; platform org bypasses, /internal/* is free infrastructuregasSettleMiddleware — debits 1 token on a 2xx only; non-2xx and timeouts are not chargedThis 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.serviceScopesrequireScope bypasses whenever serviceScopes is undefinedAnd 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.
They are a deploy-time gate, and a real one:
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
scopesis enforced byauthzMiddlewarenormally. The gap is specific to/i/.
The word is overloaded across two systems that share no data and behave differently. Verified against koko-prod contents:
| Koko scopes | Bio-ID scopes | |
|---|---|---|
| Declared in | spec.scopes (owner) | spec.modules[].scopes (owner), spec.dependencies[].scopes (consumer) |
| Stored in | Koko available_scopes / scope_grants | Bio-ID |
| Governs | Database access — resourceType: mongodb / redis / neo4j / users | API calls |
| Real effect | A connection string env var is injected. Genuinely enforced. | Deploy-time approval gate + client-credentials provisioning |
Runtime enforcement on /i/ | n/a | None |
| Live count (prod) | 3 available, 2 grants — all database | Only relay publishes any: relay:send, relay:templates, relay:admin |
WARNING: Koko's
available_scopeshas nothing to do with API scopes. Every row is a database grant. Do not read it expecting to findrelay: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.
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.
[] trap, if anyone does turn enforcement onserviceScopes = 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.
An earlier draft of this review recommended it, on the reasoning that Janus already authenticated the caller. It would have 401'd three services.
| Service | What it does with the inbound token |
|---|---|
iec-docman | bio-auth.ts requires Bearer, verifies via Koko then Bio-ID, populates req.user (org, roles, email) |
iec-bio-vault | requireAuth rejects any request without a valid Bearer token |
iec-raterspot | Resolves 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.
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.
| Check | Result |
|---|---|
| Live webhook subscriptions | 3 — policysign-prod, ballantyne-prod, portal-prod; all keyed on a service client_id |
orgSlug on those subscriptions | insureco on all three, stored separately |
| Messages stamped with an org slug | 0 of 10,751 — the collision is real in code but has never fired |
| Messages with no caller identity | 938, 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.
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-docman→docman,iec-relay→relay,iec-koko→koko, butiec-wallet,iec-cronandiec-pulsekeep their prefix. Always check the Koko registry, never infer from the repo.
Tracked as iec-builder#80.
Read from koko-prod on 2026-08-27.
| Registry | Count |
|---|---|
| iec-builder service list | 108 |
Koko services | 97 |
| In both | 96 |
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.
Every dependency in every catalog, checked against the registry: 62 resolve, 5 do not.
| Catalog | Declares | Reality |
|---|---|---|
byte-mga | iec-bio-vault | Should 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-mga | iec-storage | No such service. Object storage is spec.storage (MinIO + Vault), which byte-mga already declares — the dependency is a category error |
policyeco-web | jci-brain | Not a Tawa service; JCI_BRAIN_URL must come from tawa config set |
iec-books-2025/iec-ledger | iec-relay | Stale tree; the live iec-ledger already fixed this |
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.
Checked against this review; flagged for anyone maintaining tawa-docs:
| Claim | Status |
|---|---|
"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: true | False, 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/ |
iec-builder#8 — service classification in Koko, default-deny egressiec-builder#80 — make unresolved dependencies loudtawa-cli#79 — preflight sourcing schema from the builderLast updated: August 28, 2026