Skip to content

fix(contrail): make community.provision work end-to-end (env-gate + resolver + bridge body)#586

Merged
tompscanlan merged 5 commits into
mainfrom
feat/contrail-community-provision
May 23, 2026
Merged

fix(contrail): make community.provision work end-to-end (env-gate + resolver + bridge body)#586
tompscanlan merged 5 commits into
mainfrom
feat/contrail-community-provision

Conversation

@tompscanlan

Copy link
Copy Markdown
Contributor

What

Three openmeet-api fixes that, together, make POST /xrpc/net.openmeet.community.provision work end-to-end against a live PDS+PLC. Discovered while rehearsing Track D Phase 1 Step 3 (one-shot LKF community provision) on the local devnet. The prior plan assumed Step 3 needed only the env-gate; driving it end-to-end surfaced two more real bugs.

  1. Env-gate allowProvisioning (d909095) — CONTRAIL_ALLOW_PROVISIONING === 'true' lifts the operator gate (was hardcoded false). Default-deny.
  2. Bridge body forwarding (c4d4be5) — the /xrpc middleware in main.ts runs before Nest's body parsers and never next()s for net.openmeet.*, so req is an unconsumed stream. It forwarded method+headers but no body, so every POST XRPC (provision, putRecord, space.grant, …) reached the Hono handler empty and failed required-fields validation. GET (getHealth) has no body, so the Step-2 GET-only e2e never caught it. Fix buffers the stream for non-GET/HEAD and forwards it as body (dropping content-length so undici recomputes). Unblocks all of atmo's future write path, not just provision.
  3. Resolver wiring (3e91643) — the spaces-authority verifier (buildVerifier in contrail-base) and community identity paths default to a public plc.directory resolver, so caller DIDs minted on a non-public PLC fail to resolve → auth 401 "failed to retrieve did document". Spreads the internal-PLC resolver into spaces.authority + community when CONTRAIL_PLC_URL is set. Also injects the CONTRAIL_* vars from .env via env_file in dev compose (was ${VAR:-} overrides that blanked them).

Test plan

  • New e2e regression guard (test/contrail/contrail-xrpc.e2e-spec.ts): an authenticated community.provision call with a full body + bad invite must reach createAccount (502 ProvisioningFailed), not the empty-body "...required" 400. Reaching createAccount proves all five fields crossed the bridge; the bad invite fails before any PLC op, so no community is provisioned (repeatable, no accumulating state). Gated on the community env + a live PDS (PDS_URL + PDS_INVITE_CODE); skips cleanly otherwise.
  • Manual rehearsal on local devnet: provision → HTTP 200 {status:"activated"}, communityDid did:plc:rdbq2wjapfjmiznsklldbwev; PLC log last-op rotationKeys[0] == the caller's did:key (sovereignty invariant holds, Contrail's subordinate keys at [1]/[2]).
  • contrail-xrpc e2e suite 6/6 green; lint clean.

Notes

  • All three fixes are openmeet-api only — no @atmo-dev/contrail* fork change needed.
  • Follow-on (separate session): the real dev provision of LKF (rotation key → 1Password, pdsEndpoint = public PDS hostname per Decision 11).

…ision

allowProvisioning was hardcoded false; gate it on
CONTRAIL_ALLOW_PROVISIONING === 'true' so the Step-3 one-shot provision
window can open without a code edit, then close by unsetting it. Strict
equality keeps the route's default-deny posture (403 ProvisioningDisabled)
for any non-'true' value. Tests cover unset, "true", and a non-'true' value.
The /xrpc middleware in main.ts runs before Nest's body parsers and never
calls next() for net.openmeet.*, so the Express req is an unconsumed
readable stream. It built the forwarded fetch Request with method + headers
but no body, so every POST XRPC reached the Contrail Hono handler empty and
failed required-fields validation. GET (getHealth) has no body, which is why
the Step-2 GET-only e2e never caught it.

Buffer the raw req stream for non-GET/HEAD and forward it as the body
(dropping content-length so undici recomputes it). Unblocks all POST XRPC —
provision, putRecord, space.grant — not just provision.

Adds an e2e regression guard: an authenticated community.provision call with
a full body + bad invite must reach createAccount (502 ProvisioningFailed),
not the empty-body "...required" 400. Verified it fails without the fix.
The spaces-authority verifier (buildVerifier in contrail-base) and the
community adopt/identity paths default to a public plc.directory resolver, so
caller DIDs minted on a non-public PLC (the devnet PLC) fail to resolve and
auth 401s with "failed to retrieve did document". Spread the internal-PLC
resolver into spaces.authority and community when CONTRAIL_PLC_URL is set;
the vendor default (public PLC) holds otherwise.

Also inject the CONTRAIL_* community/provision vars straight from .env via
env_file in docker-compose-dev.yml instead of `environment:` ${VAR:-}
overrides that blanked them when absent from the shell.
…ars)

The body-forwarding e2e 401'd in CI because the caller DID (minted on the CI
devnet PLC) couldn't resolve — CONTRAIL_PLC_URL was unset, so the resolver
defaulted to public plc.directory. Provisioning was also disabled, so even
past auth it would 403 before reaching createAccount.

Set the three provision-window vars in env-example-relational-ci
(CONTRAIL_PLC_URL=http://plc:2582, CONTRAIL_ALLOWED_PDS_ENDPOINTS=http://pds:3000,
CONTRAIL_ALLOW_PROVISIONING=true) so the test runs end-to-end in CI rather
than skipping. aud alignment is automatic: OM's did.json `id` and contrail's
verifier serviceDid both default to did:web:api.openmeet.net when SERVICE_DID
is unset (as in CI). The test uses a bad invite, so it fails at createAccount
and provisions nothing — no accumulating CI state.

Also widen the test's gate to require those vars (the real preconditions) so
it skips cleanly anywhere the provision window is closed instead of failing
with a 401/403.
"Window" implied a time-bounded open/close; these are just config flags that
enable provisioning. Rename describeIfProvisionWindow →
describeIfProvisioningEnabled and reword the comments/env accordingly. No
behavior change.
@tompscanlan tompscanlan merged commit 90987b9 into main May 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant