From d909095b17da24d4aea9de2ee018864bfb812376 Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Fri, 22 May 2026 09:02:29 -0400 Subject: [PATCH 1/5] feat(contrail): env-gate community provisioning for one-shot LKF provision 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. --- src/contrail/contrail.config.spec.ts | 24 +++++++++++++++++++++++- src/contrail/contrail.config.ts | 6 +++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/contrail/contrail.config.spec.ts b/src/contrail/contrail.config.spec.ts index 009efd8f..247ab2d2 100644 --- a/src/contrail/contrail.config.spec.ts +++ b/src/contrail/contrail.config.spec.ts @@ -39,12 +39,13 @@ describe('buildContrailConfig community/spaces gating', () => { expect(config.spaces?.authority?.signing).toBeDefined(); }); - it('should add community with default-deny provisioning when BOTH keys are set', async () => { + it('should add community with default-deny provisioning when BOTH keys are set and CONTRAIL_ALLOW_PROVISIONING is unset', async () => { // Community needs the authority to sign/verify the service-auth credentials // its routes depend on, so it only assembles when spaces.authority is also // present (i.e. the authority signing key is configured too). process.env.CONTRAIL_COMMUNITY_ENCRYPTION_KEY = FAKE_ENCRYPTION_KEY; process.env.CONTRAIL_AUTHORITY_SIGNING_KEY = FAKE_SIGNING; + delete process.env.CONTRAIL_ALLOW_PROVISIONING; const config = await buildContrailConfig(); const community = config.community as Record; // `masterKey` is the vendor (contrail-community) config field name. @@ -52,6 +53,27 @@ describe('buildContrailConfig community/spaces gating', () => { expect(community.allowProvisioning).toBe(false); }); + it('should enable provisioning when CONTRAIL_ALLOW_PROVISIONING is exactly "true"', async () => { + // The Step-3 one-shot provision window opens by setting this env var, with + // no code edit. Strict `=== 'true'` so the route's default-deny posture is + // only lifted by an explicit, deliberate value. + process.env.CONTRAIL_COMMUNITY_ENCRYPTION_KEY = FAKE_ENCRYPTION_KEY; + process.env.CONTRAIL_AUTHORITY_SIGNING_KEY = FAKE_SIGNING; + process.env.CONTRAIL_ALLOW_PROVISIONING = 'true'; + const config = await buildContrailConfig(); + const community = config.community as Record; + expect(community.allowProvisioning).toBe(true); + }); + + it('should keep provisioning disabled for a non-"true" CONTRAIL_ALLOW_PROVISIONING value', async () => { + process.env.CONTRAIL_COMMUNITY_ENCRYPTION_KEY = FAKE_ENCRYPTION_KEY; + process.env.CONTRAIL_AUTHORITY_SIGNING_KEY = FAKE_SIGNING; + process.env.CONTRAIL_ALLOW_PROVISIONING = '1'; + const config = await buildContrailConfig(); + const community = config.community as Record; + expect(community.allowProvisioning).toBe(false); + }); + it('should drop community (and warn) when the encryption key is set without the authority signing key', async () => { process.env.CONTRAIL_COMMUNITY_ENCRYPTION_KEY = FAKE_ENCRYPTION_KEY; delete process.env.CONTRAIL_AUTHORITY_SIGNING_KEY; diff --git a/src/contrail/contrail.config.ts b/src/contrail/contrail.config.ts index b430b411..f50a58f2 100644 --- a/src/contrail/contrail.config.ts +++ b/src/contrail/contrail.config.ts @@ -76,7 +76,11 @@ export async function buildContrailConfig(): Promise { ? { masterKey: encryptionKey, plcDirectory: plcUrl, - allowProvisioning: false, // default-deny until Step 3 + // Default-deny: the one-shot Step-3 provision window opens by setting + // CONTRAIL_ALLOW_PROVISIONING=true (no code edit), then unsetting it. + // Strict `=== 'true'` so only a deliberate value lifts the route's + // default-deny posture (router returns 403 ProvisioningDisabled). + allowProvisioning: process.env.CONTRAIL_ALLOW_PROVISIONING === 'true', allowedPdsEndpoints: process.env.CONTRAIL_ALLOWED_PDS_ENDPOINTS?.split(',') || undefined, } From c4d4be5897ef328c12108262b82123985ca08409 Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Fri, 22 May 2026 15:49:04 -0400 Subject: [PATCH 2/5] =?UTF-8?q?fix(contrail):=20forward=20request=20body?= =?UTF-8?q?=20in=20nest=E2=86=92Hono=20XRPC=20bridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/main.ts | 15 +++ test/contrail/contrail-xrpc.e2e-spec.ts | 121 +++++++++++++++++++++++- 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index f05a1f81..298aff1c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -112,9 +112,24 @@ async function bootstrap() { if (typeof v === 'string') headers[k] = v; else if (Array.isArray(v)) headers[k] = v.join(', '); } + // This middleware runs before Nest's body parsers and never calls + // next() for net.openmeet.*, so req is an unconsumed readable stream. + // Buffer it and forward as the request body — without this, every + // POST XRPC (provision, putRecord, space.grant, …) reaches the Hono + // handler empty and fails its required-fields validation. + let body: Buffer | undefined; + const method = req.method.toUpperCase(); + if (method !== 'GET' && method !== 'HEAD') { + const chunks: Buffer[] = []; + for await (const chunk of req) chunks.push(chunk as Buffer); + body = Buffer.concat(chunks); + } + // Drop content-length so undici recomputes it for the buffered body. + delete headers['content-length']; const fetchRequest = new globalThis.Request(url.toString(), { method: req.method, headers, + ...(body && body.length ? { body } : {}), }); const fetchResponse = await contrailProvider.handle(fetchRequest); res.status(fetchResponse.status); diff --git a/test/contrail/contrail-xrpc.e2e-spec.ts b/test/contrail/contrail-xrpc.e2e-spec.ts index 287260a7..4ab317fb 100644 --- a/test/contrail/contrail-xrpc.e2e-spec.ts +++ b/test/contrail/contrail-xrpc.e2e-spec.ts @@ -1,5 +1,10 @@ import request from 'supertest'; -import { TESTING_APP_URL } from '../utils/constants'; +import { + TESTING_APP_URL, + TESTING_PDS_URL, + TESTING_PDS_HANDLE_DOMAIN, + TESTING_PDS_INVITE_CODE, +} from '../utils/constants'; /** * Curl-level proof that OM API's /xrpc/net.openmeet.* mount works. @@ -92,3 +97,117 @@ describeIfCommunity('Contrail community XRPC mount (e2e)', () => { expect(res.status).toBe(401); }); }); + +/** + * Regression guard for the nest→Hono bridge request-body forwarding + * (src/main.ts /xrpc middleware). That middleware runs before Nest's body + * parsers and never calls next() for net.openmeet.*, so `req` is an + * unconsumed readable stream that must be buffered and forwarded — otherwise + * every POST XRPC reaches the Hono handler with an empty body and fails its + * required-fields validation. GET routes have no body, which is why the + * getHealth probe above never caught this. + * + * The discriminator: with the body DROPPED, an authenticated provision call + * returns 400 InvalidRequest "...required". With the body FORWARDED, the same + * call carries handle/email/password/pdsEndpoint/rotationKey past that gate + * into the orchestrator, which reaches createAccount and fails only on the + * deliberately-invalid invite code (502 ProvisioningFailed). Reaching + * createAccount is positive proof the body crossed the bridge. A bad invite + * fails before any PLC op, so no community is provisioned — the test is + * repeatable with no accumulating PDS/PLC state. + * + * Needs a live PDS (account creation + service-auth mint) on top of the + * community env, so it skips unless PDS_URL + PDS_INVITE_CODE are also set. + */ +const describeIfCommunityProvision = + process.env.CONTRAIL_DATABASE_URL && + process.env.CONTRAIL_COMMUNITY_ENCRYPTION_KEY && + process.env.CONTRAIL_AUTHORITY_SIGNING_KEY && + process.env.PDS_URL && + process.env.PDS_INVITE_CODE + ? describe + : describe.skip; + +describeIfCommunityProvision( + 'Contrail community POST body forwarding (e2e)', + () => { + const app = TESTING_APP_URL; + const pdsUrl = TESTING_PDS_URL; + // Endpoint the OM API server (not this test process) uses to reach the PDS. + // On devnet that's the in-cluster address; CONTRAIL_ALLOWED_PDS_ENDPOINTS + // gates it, so it must match one of the allowed values. + const inContainerPdsEndpoint = + process.env.CONTRAIL_ALLOWED_PDS_ENDPOINTS?.split(',')[0].trim() || + 'http://pds:3000'; + + const shortId = () => Math.random().toString(36).substring(2, 8); + + /** Create a throwaway PDS account and mint a community.provision-scoped + * service-auth JWT (aud = OM's SERVICE_DID from did.json). */ + async function mintProvisionCaller(): Promise { + const id = shortId(); + // Use the domain the PDS actually serves (.env's PDS_SERVICE_HANDLE_DOMAINS + // can drift from the devnet PDS config — e.g. .pds.test vs .devnet.test). + const describe = await request(pdsUrl) + .get('/xrpc/com.atproto.server.describeServer') + .expect(200); + const handleDomain: string = + describe.body.availableUserDomains?.[0] ?? TESTING_PDS_HANDLE_DOMAIN; + + const create = await request(pdsUrl) + .post('/xrpc/com.atproto.server.createAccount') + .set('Content-Type', 'application/json') + .send({ + email: `bodyfwd-${id}@test.invalid`, + handle: `bodyfwd${id}${handleDomain}`, + password: 'test-password-123', + inviteCode: TESTING_PDS_INVITE_CODE, + }) + .expect(200); + const { accessJwt } = create.body; + + const didJson = await request(app) + .get('/.well-known/did.json') + .expect(200); + const serviceDid = didJson.body.id; + + const svc = await request(pdsUrl) + .get('/xrpc/com.atproto.server.getServiceAuth') + .query({ aud: serviceDid, lxm: 'net.openmeet.community.provision' }) + .set('Authorization', `Bearer ${accessJwt}`) + .expect(200); + return svc.body.token; + } + + it('should forward the POST body across the bridge (provision reaches createAccount, not the empty-body 400)', async () => { + const token = await mintProvisionCaller(); + const id = shortId(); + + const res = await request(app) + .post('/xrpc/net.openmeet.community.provision') + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .send({ + handle: `bodyfwd-tgt-${id}.devnet.test`, + email: `bodyfwd-tgt-${id}@test.invalid`, + password: `pw-${id}`, + pdsEndpoint: inContainerPdsEndpoint, + rotationKey: + 'did:key:zDnaeWVHmxYg3V8sBygjT64PRcQzCVjePBhmPrjEPngAvatvi', + inviteCode: 'deliberately-invalid-invite', + }); + + // The empty-body bug returned 400 "...required". The body crossed if we + // do NOT see that rejection. + const message: string = res.body?.message ?? ''; + expect(message).not.toMatch(/required/i); + expect(res.status).not.toBe(400); + + // Positive proof: the orchestrator received all fields and got as far as + // createAccount, failing only on the bad invite. + expect(res.status).toBe(502); + expect(res.body?.error).toBe('ProvisioningFailed'); + expect(message).toMatch(/createAccount/i); + }); + }, +); From 3e9164329a0c6caea6c4cec3d5c05e86335b8bc5 Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Fri, 22 May 2026 15:49:13 -0400 Subject: [PATCH 3/5] fix(contrail): resolve caller DIDs against the configured internal PLC 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. --- docker-compose-dev.yml | 15 +++++++++------ src/contrail/contrail.config.ts | 17 ++++++++++++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index a9ce88c7..08f71e5a 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -222,12 +222,15 @@ services: # uninitialized and /xrpc/net.openmeet.* returns 503. - CONTRAIL_DATABASE_URL=postgres://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@postgres:5432/${DATABASE_NAME} - CONTRAIL_SCHEMA=contrail - # Community routes mount only when BOTH keys are present (see - # env-example-relational). Pass them at `up` time, e.g.: - # CONTRAIL_COMMUNITY_ENCRYPTION_KEY=... CONTRAIL_AUTHORITY_SIGNING_KEY=... \ - # docker compose -f docker-compose-dev.yml up -d api - - CONTRAIL_COMMUNITY_ENCRYPTION_KEY=${CONTRAIL_COMMUNITY_ENCRYPTION_KEY:-} - - CONTRAIL_AUTHORITY_SIGNING_KEY=${CONTRAIL_AUTHORITY_SIGNING_KEY:-} + # Community keys + the one-shot Step-3 provision window + # (CONTRAIL_COMMUNITY_ENCRYPTION_KEY, CONTRAIL_AUTHORITY_SIGNING_KEY, + # CONTRAIL_PLC_URL, CONTRAIL_ALLOWED_PDS_ENDPOINTS, + # CONTRAIL_ALLOW_PROVISIONING) are injected straight from `.env` via + # env_file below — no `environment:` override here, so they don't get + # blanked when absent from the shell. Community routes mount only when + # BOTH keys are present; provisioning stays default-deny unless + # CONTRAIL_ALLOW_PROVISIONING=true. Close the window by removing those + # three lines from `.env` and recreating. env_file: - .env networks: diff --git a/src/contrail/contrail.config.ts b/src/contrail/contrail.config.ts index f50a58f2..1679aba0 100644 --- a/src/contrail/contrail.config.ts +++ b/src/contrail/contrail.config.ts @@ -19,7 +19,11 @@ function parseAuthoritySigningKey( export async function buildContrailConfig(): Promise { const plcUrl = process.env.CONTRAIL_PLC_URL; - let resolver: unknown | undefined; + // Typed `unknown` because the ESM-only @atcute resolver type isn't cleanly + // importable under this project's moduleResolution; it's attached to the + // config below via spread (which skips excess-property checks) and consumed + // structurally at runtime by contrail-base's buildVerifier. + let resolver: unknown; if (plcUrl) { const ir = await esmImport( '@atcute/identity-resolver', @@ -51,6 +55,13 @@ export async function buildContrailConfig(): Promise { type: process.env.CONTRAIL_SPACE_TYPE ?? 'tools.atmo.event.space', serviceDid: process.env.SERVICE_DID ?? 'did:web:api.openmeet.net', signing: authoritySigningKey, + // Without this the spaces-authority verifier (buildVerifier in + // contrail-base) defaults to a public plc.directory resolver and + // can't resolve caller DIDs minted on a non-public PLC — the auth + // middleware then 401s with "failed to retrieve did document". + // Only set when CONTRAIL_PLC_URL is configured; otherwise the vendor + // default (public PLC) is correct. + ...(resolver ? { resolver } : {}), }, } : undefined; @@ -76,6 +87,10 @@ export async function buildContrailConfig(): Promise { ? { masterKey: encryptionKey, plcDirectory: plcUrl, + // Same rationale as spaces.authority.resolver above: the community + // adopt/identity-resolution paths use cfg.resolver; without it they + // fall back to public PLC and can't see non-public-PLC DIDs. + ...(resolver ? { resolver } : {}), // Default-deny: the one-shot Step-3 provision window opens by setting // CONTRAIL_ALLOW_PROVISIONING=true (no code edit), then unsetting it. // Strict `=== 'true'` so only a deliberate value lifts the route's From f3b281f39ead4e65113519297ce0ff46ef3c980b Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Fri, 22 May 2026 18:37:31 -0400 Subject: [PATCH 4/5] test(contrail): run the body-forwarding e2e in CI (provision window vars) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- env-example-relational-ci | 11 +++++++++ test/contrail/contrail-xrpc.e2e-spec.ts | 30 ++++++++++++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/env-example-relational-ci b/env-example-relational-ci index 263edc09..cfcedf35 100644 --- a/env-example-relational-ci +++ b/env-example-relational-ci @@ -206,3 +206,14 @@ CONTRAIL_SCHEMA=contrail # from this file. The committed P-256 private key here signs nothing of value. CONTRAIL_COMMUNITY_ENCRYPTION_KEY=BBiI4DJaQ+AsoTJuKvMv06dNEFL+NprY9atylYM4Eds= CONTRAIL_AUTHORITY_SIGNING_KEY=eyJwcml2YXRlS2V5Ijp7ImtleV9vcHMiOlsic2lnbiJdLCJleHQiOnRydWUsImt0eSI6IkVDIiwieCI6ImM5ZnhwRWlQdktXd2g5eEk5OGFmUTVucVBYcjI5WU03NHkxOG1qeks0V2siLCJ5IjoiNmF1c1c5MVgyS21XMEJYXzFuRDlBZmtHRDh2ZldNNE0yU29kN0dLMkZpRSIsImNydiI6IlAtMjU2IiwiZCI6Ik9tMzM4Q0NCRmhMUzFON1JmYjZESnJ5NlpnYTdJakx1c3JpM2ExcGdvX3MifSwicHVibGljS2V5Ijp7ImtleV9vcHMiOlsidmVyaWZ5Il0sImV4dCI6dHJ1ZSwia3R5IjoiRUMiLCJ4IjoiYzlmeHBFaVB2S1d3aDl4STk4YWZRNW5xUFhyMjlZTTc0eTE4bWp6SzRXayIsInkiOiI2YXVzVzkxWDJLbVcwQlhfMW5EOUFma0dEOHZmV000TTJTb2Q3R0syRmlFIiwiY3J2IjoiUC0yNTYifX0= +# Provision-window vars — enable the community.provision body-forwarding e2e +# to actually RUN in CI (not skip). The resolver must point at the CI devnet +# PLC so caller DIDs minted there resolve (else auth 401s before the body is +# read); provisioning must be enabled to reach createAccount; the PDS endpoint +# must be allow-listed. aud alignment is automatic — both OM's did.json `id` +# and contrail's verifier serviceDid default to did:web:api.openmeet.net when +# SERVICE_DID is unset (as it is here). The e2e uses a bad invite, so it fails +# at createAccount and provisions nothing — no accumulating CI state. +CONTRAIL_PLC_URL=http://plc:2582 +CONTRAIL_ALLOWED_PDS_ENDPOINTS=http://pds:3000 +CONTRAIL_ALLOW_PROVISIONING=true diff --git a/test/contrail/contrail-xrpc.e2e-spec.ts b/test/contrail/contrail-xrpc.e2e-spec.ts index 4ab317fb..b5bb4def 100644 --- a/test/contrail/contrail-xrpc.e2e-spec.ts +++ b/test/contrail/contrail-xrpc.e2e-spec.ts @@ -116,19 +116,39 @@ describeIfCommunity('Contrail community XRPC mount (e2e)', () => { * fails before any PLC op, so no community is provisioned — the test is * repeatable with no accumulating PDS/PLC state. * - * Needs a live PDS (account creation + service-auth mint) on top of the - * community env, so it skips unless PDS_URL + PDS_INVITE_CODE are also set. + * Preconditions (skips cleanly unless ALL are met — these are the operator + * "provision window", not the default config): + * - community env (mount): CONTRAIL_COMMUNITY_ENCRYPTION_KEY + + * CONTRAIL_AUTHORITY_SIGNING_KEY + CONTRAIL_DATABASE_URL + * - live PDS for account creation + service-auth mint: PDS_URL + + * PDS_INVITE_CODE + * - CONTRAIL_PLC_URL: the internal-PLC resolver must be wired, else the + * caller DID (minted on that PLC) can't resolve and auth 401s before the + * body is ever read. (This is also what fix #2 in this PR addresses.) + * - CONTRAIL_ALLOW_PROVISIONING === 'true': otherwise the route 403s at the + * gate before reaching createAccount, so we'd never observe the body. + * + * These are set in CI (env-example-relational-ci) so the test RUNS there — + * the resolver points at the CI devnet PLC and provisioning is enabled. aud + * alignment is automatic: both OM's did.json `id` and contrail's verifier + * serviceDid default to did:web:api.openmeet.net when SERVICE_DID is unset. + * The gate just makes the test skip cleanly anywhere the window is closed + * (e.g. a plain local run) rather than failing with a 401/403. It first + * caught this bridge regression during the local Step-3 rehearsal (verified + * RED without the main.ts fix). */ -const describeIfCommunityProvision = +const describeIfProvisionWindow = process.env.CONTRAIL_DATABASE_URL && process.env.CONTRAIL_COMMUNITY_ENCRYPTION_KEY && process.env.CONTRAIL_AUTHORITY_SIGNING_KEY && process.env.PDS_URL && - process.env.PDS_INVITE_CODE + process.env.PDS_INVITE_CODE && + process.env.CONTRAIL_PLC_URL && + process.env.CONTRAIL_ALLOW_PROVISIONING === 'true' ? describe : describe.skip; -describeIfCommunityProvision( +describeIfProvisionWindow( 'Contrail community POST body forwarding (e2e)', () => { const app = TESTING_APP_URL; From 2fa9420c257e5a99e13844a59d9cadcd97e5698a Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Fri, 22 May 2026 18:53:42 -0400 Subject: [PATCH 5/5] refactor(contrail): drop "provision window" framing for the e2e gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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. --- env-example-relational-ci | 2 +- test/contrail/contrail-xrpc.e2e-spec.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/env-example-relational-ci b/env-example-relational-ci index cfcedf35..5382bdd1 100644 --- a/env-example-relational-ci +++ b/env-example-relational-ci @@ -206,7 +206,7 @@ CONTRAIL_SCHEMA=contrail # from this file. The committed P-256 private key here signs nothing of value. CONTRAIL_COMMUNITY_ENCRYPTION_KEY=BBiI4DJaQ+AsoTJuKvMv06dNEFL+NprY9atylYM4Eds= CONTRAIL_AUTHORITY_SIGNING_KEY=eyJwcml2YXRlS2V5Ijp7ImtleV9vcHMiOlsic2lnbiJdLCJleHQiOnRydWUsImt0eSI6IkVDIiwieCI6ImM5ZnhwRWlQdktXd2g5eEk5OGFmUTVucVBYcjI5WU03NHkxOG1qeks0V2siLCJ5IjoiNmF1c1c5MVgyS21XMEJYXzFuRDlBZmtHRDh2ZldNNE0yU29kN0dLMkZpRSIsImNydiI6IlAtMjU2IiwiZCI6Ik9tMzM4Q0NCRmhMUzFON1JmYjZESnJ5NlpnYTdJakx1c3JpM2ExcGdvX3MifSwicHVibGljS2V5Ijp7ImtleV9vcHMiOlsidmVyaWZ5Il0sImV4dCI6dHJ1ZSwia3R5IjoiRUMiLCJ4IjoiYzlmeHBFaVB2S1d3aDl4STk4YWZRNW5xUFhyMjlZTTc0eTE4bWp6SzRXayIsInkiOiI2YXVzVzkxWDJLbVcwQlhfMW5EOUFma0dEOHZmV000TTJTb2Q3R0syRmlFIiwiY3J2IjoiUC0yNTYifX0= -# Provision-window vars — enable the community.provision body-forwarding e2e +# Provisioning config — enables the community.provision body-forwarding e2e # to actually RUN in CI (not skip). The resolver must point at the CI devnet # PLC so caller DIDs minted there resolve (else auth 401s before the body is # read); provisioning must be enabled to reach createAccount; the PDS endpoint diff --git a/test/contrail/contrail-xrpc.e2e-spec.ts b/test/contrail/contrail-xrpc.e2e-spec.ts index b5bb4def..6deda1af 100644 --- a/test/contrail/contrail-xrpc.e2e-spec.ts +++ b/test/contrail/contrail-xrpc.e2e-spec.ts @@ -116,8 +116,8 @@ describeIfCommunity('Contrail community XRPC mount (e2e)', () => { * fails before any PLC op, so no community is provisioned — the test is * repeatable with no accumulating PDS/PLC state. * - * Preconditions (skips cleanly unless ALL are met — these are the operator - * "provision window", not the default config): + * Preconditions (skips cleanly unless ALL are met — provisioning has to be + * configured and enabled, which is not the default config): * - community env (mount): CONTRAIL_COMMUNITY_ENCRYPTION_KEY + * CONTRAIL_AUTHORITY_SIGNING_KEY + CONTRAIL_DATABASE_URL * - live PDS for account creation + service-auth mint: PDS_URL + @@ -132,12 +132,12 @@ describeIfCommunity('Contrail community XRPC mount (e2e)', () => { * the resolver points at the CI devnet PLC and provisioning is enabled. aud * alignment is automatic: both OM's did.json `id` and contrail's verifier * serviceDid default to did:web:api.openmeet.net when SERVICE_DID is unset. - * The gate just makes the test skip cleanly anywhere the window is closed + * The gate just makes the test skip cleanly where provisioning isn't enabled * (e.g. a plain local run) rather than failing with a 401/403. It first * caught this bridge regression during the local Step-3 rehearsal (verified * RED without the main.ts fix). */ -const describeIfProvisionWindow = +const describeIfProvisioningEnabled = process.env.CONTRAIL_DATABASE_URL && process.env.CONTRAIL_COMMUNITY_ENCRYPTION_KEY && process.env.CONTRAIL_AUTHORITY_SIGNING_KEY && @@ -148,7 +148,7 @@ const describeIfProvisionWindow = ? describe : describe.skip; -describeIfProvisionWindow( +describeIfProvisioningEnabled( 'Contrail community POST body forwarding (e2e)', () => { const app = TESTING_APP_URL;