Repository navigation
Add an AuthFX + ConnectFX co-embedding smoke test #18
Description
One process, one Postgres: AuthFX and ConnectFX compose side by side the way Plus-One will run them
Context
Plus-One will embed both frameworks in the same application against the same Postgres database: AuthFX for login/sessions, ConnectFX for operational provider connections, bridged only by consumer policy (D8). Nothing in either backlog exercises that co-embedding before the production migration issues (#8, In-Time-Tec/connectfx#16) — which is exactly when surprises are most expensive. Concrete failure classes this smoke exists to catch early: migrator collisions (authfx_migrations vs connectfx_migrations on one database), both frameworks sharing one SqlClient/PgClient layer (D2 forbids private pools), effect version skew across the two published dependency trees, tag-string collisions, and the Identity → opaque Subject handoff being representable without either framework importing the other.
This issue adds a non-published example app plus an integration test in the authfx repo (the last mover per D8 ordering) that composes @authfx/{core,store-sql,http,microsoft} from the workspace with published @connectfx/{core,store-sql,testing} and drives one end-to-end flow.
Spec references: docs/spec/13-plusone-migration.md (deployment shape), docs/spec/11-sdk-and-layer-composition.md; connectfx docs/spec/15-plusone-migration.md, docs/spec/13-sdk-and-layer-composition.md.
Governing decisions
- D8 (foundations) — neither framework imports or calls the other; the consumer app is the only bridge; the app verifies a session with AuthFX, applies its own policy, derives an opaque Subject, and invokes ConnectFX.
- D7 (foundations) — AuthFX is subject-producing (SubjectResolver), ConnectFX is Subject-scoped with explicit parameters; the smoke proves the bridge is consumer-owned code only.
- D2 (foundations) — one shared Effect-SQL client per host process; the smoke provides exactly one
PgClient.layerConfigto both store bundles. - D9 (foundations) — table prefixes
authfx_/connectfx_and migrator tablesauthfx_migrations/connectfx_migrationsmust coexist on one database; the smoke asserts both migrator tables exist after startup. - Arch authfx §10.1 and arch connectfx §12.1 — the two production composition roots this app merges.
- D3 (foundations) — no shared standards package appears; the app's dependency tree must not introduce cross-framework imports.
Deliverables
authfx/
apps/coembed-smoke/
package.json tsconfig.json AGENTS.md
src/main.ts
src/host-policy.ts
src/bridge.ts
test/coembed.test.ts
The app is private: true, never published, excluded from the release workflow.
Contracts
The consumer-owned bridge (the only place both frameworks' types meet; exact service shapes come from the shipped barrels — AuthFX SessionService.verify output and ConnectFX Subject schema):
import { Effect } from "effect"
import { SessionService } from "@authfx/core"
import { Subject } from "@connectfx/core"
export const subjectForVerifiedSession = (verified: SessionService.VerifiedSession) =>
Effect.succeed(Subject.Subject.make({ type: "app-user", id: verified.identity.id }))Composition shape (merging arch authfx §10.1 with arch connectfx §12.1; one pg layer):
const pgLayer = PgClient.layerConfig({
url: Config.redacted("DATABASE_URL"),
maxConnections: Config.int("SMOKE_SQL_MAX_CONNECTIONS").pipe(Config.withDefault(8)),
})
const authLayer = authfxCoreLayer.pipe(
Layer.provideMerge(authfxStoreLayer),
Layer.provideMerge(AuthfxMigrations.layer),
)
const connectLayer = connectfxCoreServicesLayer.pipe(
Layer.provideMerge(StoreLayers.sqlStoreLayer),
Layer.provideMerge(Migrator.layer),
)
const appLayer = Layer.mergeAll(authLayer, connectLayer).pipe(Layer.provideMerge(pgLayer))The smoke flow the test drives (pg-gated variant) and the memory variant (always-on, using @authfx/testing deterministic crypto + fixture authority and @connectfx/testing FakeProvider + MemoryVault):
- AuthFX:
begin → fixture.issueLogin → complete → verifymints a session and a verified Identity. - Bridge: derive
Subjectfrom the verified session inhost-policy.ts. - ConnectFX: create a pending Connection for that Subject, complete a fake-provider authorization flow, assert the Connection is
activeand Subject-isolated (a second Subject cannot read it).
Implementation steps
- Create
apps/coembed-smokewith the dependency split:@authfx/*viaworkspace:*,@connectfx/{core,store-sql,testing}via published versions (from Publish @connectfx packages for consumer adoption connectfx#21). Assert inAGENTS.mdthat this app is the only sanctioned place both scopes appear together in this repo. - Write
src/bridge.tsandsrc/host-policy.ts(SubjectResolver + LifecycleHooks implementations on the AuthFX side, Subject derivation for ConnectFX) — consumer-policy code only, no framework imports of each other. - Write
src/main.tscomposing both production roots over onepgLayeras specified. - Write
test/coembed.test.tsmemory variant: full flow 1–3 over memory layers, deterministic crypto, fake provider; runs in the default suite. - Add the pg-gated variant (
AUTHFX_PG_TEST=1): bootappLayeragainst live Postgres, assert both migrator tables exist (select from authfx_migrations/connectfx_migrations), run flow 1–3, assert zero table-name collisions (information_schema scan: every framework table starts with exactly one of the two prefixes). - Add a dependency-tree guard test:
bun pm lsoutput contains no@authfx/*package depending on@connectfx/*or vice versa, and exactly oneeffectversion resolves. - Wire the app into turbo (build/typecheck/test) and into the
pg-conformanceCI job from Wire CI Postgres conformance and security-catalog gates #15.
Testing
bun run --cwd apps/coembed-smoke test— memory-variant flow green in the default suite.AUTHFX_PG_TEST=1 DATABASE_URL=... bun run test:pg— pg variant green: both migrators applied, flow completes, prefix scan clean.bun run typecheck && bun run build— app compiles against both dependency trees with a singleeffect@4.0.0-beta.93resolution.- Cross-Subject denial assertion reuses the connectfx conformance expectation (S3): second Subject's
getanswersConnectionNotFound.
Acceptance criteria
- Memory-variant smoke (login → bridge → connection activation) runs in default CI with no I/O.
- Pg-variant proves both migration trees apply to one database without collision and the flow completes.
- Exactly one
PgClient/SqlClientlayer is constructed; grep confirms no second pool configuration. - No
@authfx/*↔@connectfx/*import exists outsideapps/coembed-smoke; the dependency-tree guard test enforces it. - Single
effectversion resolves across both trees; the guard test enforces it. - The bridge module is the only file constructing a ConnectFX
Subjectfrom AuthFX output, and it lives in app code (D8 proven by construction).
Dependencies
- Blocked by: Phase 5: implement Effect SQL Postgres stores #9 (pg stores), Phase 3: implement Microsoft Entra OIDC authenticator #11 + Phase 3: build Microsoft OIDC mock and protocol fixture kit #5 (fixture-backed login flow), Phase 2: implement authentication transaction service #12 (transaction service), connectfx Publish @connectfx packages for consumer adoption connectfx#21 (installable
@connectfx/{core,store-sql,testing}), Phase 2: implement authorization transactions and installation lifecycle connectfx#13 + Phase 4: implement RFC 8628 device authorization #10 + Phase 1: scaffold workspace packages and @authfx/testing foundations #14 (flow/lifecycle/pg surface the smoke drives). - Blocks: Phase 6: create Plus-One AuthFX migration adapter #8 (migration readiness — the smoke is the rehearsal of Plus-One's deployment shape), Epic: implement AuthFX production foundation #3 epic close-out.
- Cross-repo constraint: runs against published connectfx artifacts, so it also regression-tests the connectfx publish pipeline.
Out of scope
- Any Plus-One vocabulary or actual Plus-One code — the bridge uses neutral
app-usersubjects (D7; Plus-One's real mapping lives in its own repo at Phase 6: create Plus-One AuthFX migration adapter #8). - Microsoft live-tenant traffic — fixture authority and fake provider only; live tests are opt-in per D10 and belong to the migration issues.
- A shared example published to consumers — this is an internal proof; docs quickstarts stay single-framework.
- ConnectFX runtime/workflow coverage — the smoke exercises core-only flows (D12's light path); crash-replay is Phase 3: implement durable Operation runtime connectfx#9's domain.
Activity