Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions hub/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ LOG_LEVEL=info
OTEL_TRACES_EXPORTER=none
# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1:4318/v1/traces

# Trust forwarded client addresses only behind this exact protected proxy-chain length.
HUB_TRUSTED_PROXY_HOPS=0

# GitHub authentication proof is disabled until every setting below is uncommented.
# AUTHENTICATION_PROVIDER=github
# HUB_APP_ORIGIN=http://127.0.0.1:5173
# AUTHENTICATION_SECURE_COOKIES=false
# AUTHENTICATION_ENCRYPTION_KEY=replace-with-base64url-encoded-32-byte-key
# GITHUB_AUTHENTICATION_REVISION=1
# GITHUB_CLIENT_ID=replace-with-oauth-app-client-id
# GITHUB_CLIENT_SECRET=replace-with-oauth-app-client-secret
# GITHUB_CALLBACK_URI=http://127.0.0.1:3001/api/auth/github/callback
# GITHUB_AUTHORIZATION_ENDPOINT=https://github.com/login/oauth/authorize
# GITHUB_TOKEN_ENDPOINT=https://github.com/login/oauth/access_token
# GITHUB_USER_ENDPOINT=https://api.github.com/user

# Independent worker concurrency. Both default to one (supported range: 1-100).
ON_DEMAND_JOB_CONCURRENCY=1
SCHEDULED_JOB_CONCURRENCY=1
Expand Down
1 change: 1 addition & 0 deletions hub/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Hub is being rebuilt. The [Hub overview](../README.md) describes what the baseli
- [Coding conventions](coding-conventions.md): frontend layers and interface rules.
- [Development](development.md): local API and frontend setup.
- [Database](database.md): Prisma schema conventions, migrations, and the shared client.
- [Authentication proof](authentication.md): GitHub identity proof, one-use attempts, and the non-admitted handoff boundary.
- [Logging](logging.md): application events, automatic request records, and payload safety.
- [Telemetry](telemetry.md): explicit OpenTelemetry spans, safe attributes, and process lifetime.
- [Background jobs](background-jobs.md): PostgreSQL queues, workers, schedules, setup, and test isolation.
Expand Down
6 changes: 3 additions & 3 deletions hub/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ The team has chosen TypeScript, Express for the API, React and Vite for the fron

The API responds to a health check, and the frontend displays the logo with light and dark themes. Storybook keeps the existing components and pages available for reuse.

Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The database has a provider-neutral identity transaction foundation, but it does not issue sessions or admit users to Hub. Sign-in, deployment, and product APIs have not been built. This app cannot yet find repositories, import Builds, host previews, save product data, or support reviews.
Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The database has a provider-neutral identity transaction foundation and a GitHub authentication proof flow. The proof produces a short-lived non-admitted handoff; it does not issue a usable session or admit users to Hub. Deployment and product APIs have not been built. This app cannot yet find repositories, import Builds, host previews, save product data, or support reviews.

## Backend

API code lives under `server/`. As features are added, route handlers will read requests, call domains, and return responses. The [code conventions](backend-architecture.md) explain repositories, integrations, utilities, domains, and which layers may import others. Put these directories under `server/` when they are needed; do not add empty implementations.

The only route is `GET /api/health`, which returns `{ "status": "ok" }`. Unknown paths return a JSON 404 response; invalid JSON is rejected before routing. `createApp()` builds the Express app without starting a server. `server/index.ts` starts it at a local address.
`GET /api/health` returns `{ "status": "ok" }`. Explicit GitHub configuration enables `POST /api/auth/github/start` and `GET /api/auth/github/callback` for authentication proof only. Unknown paths return a JSON 404 response; invalid JSON is rejected before routing. `createApp()` builds the Express app without starting a server. `server/index.ts` starts it at a local address.

The [database guide](database.md) defines the multi-file Prisma schema and shared client in integrations. The health route remains a process check, not a database readiness check.

Expand All @@ -30,7 +30,7 @@ The identity foundation stores Hub users, immutable external identity mappings,

Domain operations receive only the transaction-scoped repositories they need. A successful state change and its success audit commit together. The kernel applies one fixed bounded retry policy to expected serializable and aggregate-creation races so consumers cannot disable convergence. A rejected database operation rolls back before its final denial or failure audit is written. External provider-validation failures write one typed audit after validation returns and before the operation fails. Audit callers select a typed kernel event; the repository builds the stored action and allowlisted context.

This foundation does not prove a provider assertion, create a usable Hub session, assign membership, or authorize product access.
The [authentication proof layer](authentication.md) uses this foundation to validate explicit GitHub configuration, complete OAuth user authentication, resolve the immutable external identity, and create a browser-bound handoff. It performs provider network work outside identity transactions. It does not create a usable Hub session, assign membership, or authorize product access.

## Frontend

Expand Down
29 changes: 29 additions & 0 deletions hub/docs/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Authentication proof

Hub can prove a GitHub user identity without admitting that user to Hub. The proof flow creates a short-lived admission handoff for the next identity layer. It does not create a usable session, membership, role, invitation, or product authority.

## Current flow

`POST /api/auth/github/start` accepts requests only from the configured Hub origin. Hub reserves capacity in a database-backed rate-limit window for the trusted request address, selects the configured GitHub provider revision, stores a one-use authorization attempt, sets an HTTP-only browser-binding cookie, and redirects to GitHub with state and PKCE. The callback reserves its separate database-backed budget before parsing callback values, so every Hub replica enforces the same limits.

GitHub returns to `GET /api/auth/github/callback`. Hub consumes the attempt before exchanging the bounded authorization code. It uses GitHub's current-user endpoint and binds the immutable external identity to the numeric GitHub user ID. Login, name, avatar URL, and email remain display attributes.

A successful callback returns `authentication_proved` with `admission: "pending"` and a short-lived opaque handoff token. The handoff is bound to the browser, selected provider revision, and global revocation generation. It is not a Hub session and cannot access product routes.

## Stored data

Hub stores SHA-256 digests of state, browser binding, request address, and handoff token. The PKCE verifier is encrypted with the configured authentication encryption key. Provider access tokens are used only for the current-user lookup and are never saved, logged, or audited.

Authorization attempts and admission handoffs can transition from unconsumed to consumed once. Their identity, provider revision, expiry, and evidence cannot change. Admission-handoff consumption uses PostgreSQL's current clock after acquiring its transaction locks, so callers cannot choose the expiry-check time. The global revocation generation can only increase.

The C1 identity kernel continues to own users, immutable external identity mappings, provider revision digests, and security audits. Provider requests and validation run outside C1 database transactions.

## Configuration

Authentication routes are disabled unless `AUTHENTICATION_PROVIDER=github` is set. When enabled, startup requires the GitHub client, endpoint, callback, revision, cookie, origin, and encryption settings listed in [.env.example](../.env.example). Hub validates the strict public configuration allowlist and selects its explicit revision through the identity kernel. Unknown fields and revision drift fail closed; Hub never falls back to another provider.

Set `HUB_TRUSTED_PROXY_HOPS` only to the exact number of protected proxies in front of Hub. The default is `0`, so forwarded client addresses are ignored.

## Not implemented

This layer does not consume a handoff through an HTTP route or issue a usable Hub session. It does not evaluate installation membership, invitations, bootstrap or recovery state, roles, repository permissions, or product authorization. Entra OIDC, account linking, synchronized access, SCIM, publishing, and migration from old Hub instances are also not implemented.
4 changes: 3 additions & 1 deletion hub/docs/database.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Hub database

Hub uses PostgreSQL and Prisma.
Hub uses PostgreSQL and Prisma. The schema contains the provider-neutral identity transaction foundation, one-use authorization attempts, short-lived admission handoffs, and a global authentication revocation generation. Authentication provider revisions store a validated-configuration digest, not provider configuration, and every committed provider must select one of its own revisions. Hub sessions, memberships, invitations, source integrations, and product records are not part of this foundation.

An external identity's provider, issuer, and subject share a 2,000-byte UTF-8 limit, including two separator bytes. Runtime parsing and the committed database constraint enforce the same conservative bound so the composite unique index remains below PostgreSQL's B-tree item limit.

[Background jobs](background-jobs.md) use the same database with BullMQ-owned tables and functions in the `bullmq` schema. Prisma does not own those objects. `npm run setup` applies Prisma migrations before BullMQ setup and schedule registration.

Expand Down
8 changes: 5 additions & 3 deletions hub/docs/development.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hub development

Use the Hub devcontainer for Node.js and PostgreSQL, or run Node.js locally with the same database service. Sign-in and production deployment are not configured.
Use the Hub devcontainer for Node.js and PostgreSQL, or run Node.js locally with the same database service. GitHub authentication proof can be configured locally, but it does not admit users or create Hub sessions. Production deployment is not configured.

## Devcontainer

Expand All @@ -20,7 +20,7 @@ cp .env.example .env
npm ci --engine-strict
```

The example URL connects to PostgreSQL on `127.0.0.1:5432`. To use another development database, set `DATABASE_URL` in `.env` instead. Do not commit credentials. The existing health and frontend routes do not query the database.
The example URL connects to PostgreSQL on `127.0.0.1:5432`. To use another development database, set `DATABASE_URL` in `.env` instead. Do not commit credentials. The health and frontend routes do not query the database. Authentication routes require the database and remain disabled unless an authentication provider is selected.

## Start the app

Expand All @@ -36,7 +36,9 @@ In a second terminal, also from `hub/`, start Vite:
npm run dev
```

Open <http://127.0.0.1:5173>. Vite serves the frontend and forwards `/api` requests to Express at <http://127.0.0.1:3001>. `GET /api/health` returns JSON; other API routes have not been built. Both servers accept connections only from this machine. Stop each with Ctrl+C.
Open <http://127.0.0.1:5173>. Vite serves the frontend and forwards `/api` requests to Express at <http://127.0.0.1:3001>. `GET /api/health` returns JSON. When GitHub authentication is configured, the API also provides the proof-only start and callback routes described in [Authentication proof](authentication.md).

To enable the GitHub proof flow, copy the commented authentication settings from `.env.example`, register the exact callback URI in a GitHub OAuth App, and generate the local encryption key with `node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"`. Keep the client secret and encryption key out of source control. Use insecure cookies only for loopback HTTP development. Both servers accept connections only from this machine. Stop each with Ctrl+C.

Vite also forwards WebSocket upgrades under `/api`. The `/api/realtime` endpoint rejects connections until application access checks are connected; see [WebSocket updates](websockets.md). Stopping the API closes its WebSocket connections as well.

Expand Down
26 changes: 26 additions & 0 deletions hub/schema/admission-handoff.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
model AdmissionHandoff {
// ID columns
id String @id @default(uuid()) @db.Uuid
externalIdentityId String @map("external_identity_id") @db.Uuid
providerRevisionId String @map("provider_revision_id") @db.Uuid

// Relations
externalIdentity ExternalIdentity @relation(fields: [externalIdentityId], references: [id], onDelete: Restrict)
providerRevision AuthenticationProviderRevision @relation(fields: [providerRevisionId], references: [id], onDelete: Restrict)

// Attribute columns
tokenDigest String @unique @map("token_digest") @db.VarChar(64)
browserBindingDigest String @map("browser_binding_digest") @db.VarChar(64)
revocationGeneration BigInt @map("revocation_generation")
displayAttributes Json @map("display_attributes") @db.JsonB
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
consumedAt DateTime? @map("consumed_at") @db.Timestamptz(3)

// Timestamp columns
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)

@@index([expiresAt])
@@index([externalIdentityId])
@@map("admission_handoffs")
}
6 changes: 4 additions & 2 deletions hub/schema/authentication-provider-revision.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ model AuthenticationProviderRevision {
providerId String @map("provider_id") @db.Uuid

// Relations
provider AuthenticationProvider @relation("AuthenticationProviderRevisions", fields: [providerId], references: [id], onDelete: Restrict)
selectedBy AuthenticationProvider? @relation("SelectedAuthenticationProviderRevision")
provider AuthenticationProvider @relation("AuthenticationProviderRevisions", fields: [providerId], references: [id], onDelete: Restrict)
selectedBy AuthenticationProvider? @relation("SelectedAuthenticationProviderRevision")
authorizationAttempts AuthorizationAttempt[]
admissionHandoffs AdmissionHandoff[]

// Attribute columns
revision Int
Expand Down
18 changes: 18 additions & 0 deletions hub/schema/authentication-rate-limit.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
model AuthenticationRateLimit {
// ID columns
id String @id @default(uuid()) @db.Uuid

// Attribute columns
scope String @db.VarChar(32)
keyDigest String @map("key_digest") @db.VarChar(64)
windowStartedAt DateTime @map("window_started_at") @db.Timestamptz(3)
reservationCount Int @map("reservation_count")

// Timestamp columns
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)

@@unique([scope, keyDigest])
@@index([windowStartedAt])
@@map("authentication_rate_limits")
}
14 changes: 14 additions & 0 deletions hub/schema/authentication-revocation-generation.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
model AuthenticationRevocationGeneration {
// ID columns
id String @id @default(uuid()) @db.Uuid

// Attribute columns
key String @unique @db.VarChar(32)
generation BigInt @default(1)

// Timestamp columns
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)

@@map("authentication_revocation_generations")
}
25 changes: 25 additions & 0 deletions hub/schema/authorization-attempt.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
model AuthorizationAttempt {
// ID columns
id String @id @default(uuid()) @db.Uuid
providerRevisionId String @map("provider_revision_id") @db.Uuid

// Relations
providerRevision AuthenticationProviderRevision @relation(fields: [providerRevisionId], references: [id], onDelete: Restrict)

// Attribute columns
stateDigest String @unique @map("state_digest") @db.VarChar(64)
browserBindingDigest String @map("browser_binding_digest") @db.VarChar(64)
codeChallenge String @map("code_challenge") @db.VarChar(43)
codeVerifierCiphertext String @map("code_verifier_ciphertext") @db.VarChar(500)
requestIpDigest String @map("request_ip_digest") @db.VarChar(64)
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
consumedAt DateTime? @map("consumed_at") @db.Timestamptz(3)

// Timestamp columns
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)

@@index([expiresAt])
@@index([requestIpDigest, createdAt])
@@map("authorization_attempts")
}
3 changes: 2 additions & 1 deletion hub/schema/external-identity.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ model ExternalIdentity {
userId String @map("user_id") @db.Uuid

// Relations
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
admissionHandoffs AdmissionHandoff[]

// Attribute columns
provider String @db.VarChar(100)
Expand Down
Loading
Loading