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
2 changes: 1 addition & 1 deletion hub/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +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.
- [Identity](identity.md): credential storage and providers.
- [Identity](identity.md): credential storage, sign-in rules, and session access.
- [Testing and operations](testing-and-operations.md): baseline checks and their limits.
- [Storybook](storybook.md): retained presentation, fixtures, and screenshot evidence.
- [Prototype canvas](prototype-canvas.md): the separately retained extension.
Expand Down
2 changes: 2 additions & 0 deletions hub/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ The only route is `GET /api/health`, which returns `{ "status": "ok" }`. Every o

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.

The [identity service](identity.md) implements GitHub sign-in, sessions, repository permission checks, and administration rules. Its domain contracts and errors stay separate from the GitHub integration and do not assign HTTP statuses.

## Frontend

```mermaid
Expand Down
2 changes: 2 additions & 0 deletions hub/docs/backend-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Keep function implementations flat. A composition factory may bind imported func

Keep domain contracts with their domains, GitHub types and errors with that integration, and database inputs with their repositories. Keep only general helpers in utilities.

Domain and GitHub errors contain a code and a safe message, not an HTTP status. Keep Hub access and CSRF policy in domains. GitHub credential validation and encryption belong in `integrations/github/`.

## Database access

The [shared Prisma client](database.md#shared-client) lives in `server/integrations/prisma/index.ts`. Domains may pass it to repositories for database queries without letting repositories import integrations. Keep model definitions, naming, and migrations consistent with the [database conventions](database.md#schema-conventions).
Expand Down
13 changes: 13 additions & 0 deletions hub/docs/identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Hub stores GitHub credentials, users, sessions, one-use sign-in attempts, and security records in PostgreSQL. Credential providers use one GitHub App installation.

The identity service implements setup, sign-in, session verification, repository access, credential rotation, and revocation without depending on HTTP.

## Credential providers

Set `HUB_CREDENTIAL_PROVIDER` in the [environment example](../.env.example):
Expand All @@ -22,3 +24,14 @@ Apply the committed migration with `npm run db:deploy`. See [Database](database.
Domain functions bind repository queries to a client or transaction. A fixed settings row lets overlapping changes lock the same record. Credential updates, session changes, and their audit records can commit together or roll back together.

User records use permanent GitHub IDs, not names or emails. Repositories consume each sign-in attempt once. Audit reads return the latest 100 records within 90 days; later writes remove older records.

## Sign-in and access

- Sign-in attempts expire in 15 minutes and work once in the same browser. Hub discards GitHub user tokens after identification.
- Ordinary sign-in needs read access to a repo in the configured installation. Enrolled administrators may sign in without repo access, but cannot bypass repository checks.
- Repository checks use current GitHub permissions and only the configured installation. GitHub failures deny access.
- Initial setup and administrator enrollment require a deployment bootstrap secret. Setup works only once; the environment provider cannot be changed through the service.
- Rotation and revocation require an administrator, CSRF proof, and a GitHub check in that same live session less than five minutes old. Revocation also requires explicit confirmation.
- Rotation keeps the same App, client, and installation IDs. It preserves sessions but cancels pending sign-ins and fresh verification.
- Revocation ends every session and cancels pending sign-ins. Callbacks already in progress cannot restore access.
- Completed identity changes and their audit records commit together. Rejected operations create no audit records. Only administrators can read them.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { timingSafeEqual } from "node:crypto"

import { finishGitHubAuthorization } from "#server/domains/identity/finish-github-authorization"
import { getIdentityConfiguration } from "#server/domains/identity/get-identity-configuration"
import { digestSecret } from "#server/utils/digest-secret"
import type { IdentityDependencies } from "#server/domains/identity/types"
import { IdentityError } from "#server/domains/identity/identity-error"

export async function completeGitHubAuthorization(
dependencies: IdentityDependencies,
code: string,
state: string,
browserToken: string,
) {
const { store, github, now } = dependencies

const attempt = await store.lock((records) => records.consumeAttempt(digestSecret(state)))
if (
!attempt ||
attempt.expiresAt <= now() ||
!timingSafeEqual(Buffer.from(digestSecret(browserToken)), Buffer.from(attempt.browserDigest))
) {
throw new IdentityError(
"invalid_oauth_state",
"This GitHub sign-in expired or does not belong to this browser. Start again.",
)
}
const configuration = await getIdentityConfiguration(dependencies)
if (
attempt.credentialRevision !== configuration.revision ||
attempt.generation !== configuration.state.generation
) {
throw new IdentityError(
"sign_in_cancelled",
"Hub credentials or sessions changed. Start a new sign-in.",
)
}
const profile = await github.identify(configuration.credentials, code, attempt.verifier)
const verifiedAt = now()
const existingUser = await store.read.githubUser(profile.providerId)
const admitted =
attempt.purpose === "sign-in" && !existingUser?.administrator
? await github.admitted(configuration.credentials, profile)
: false
return store.lock(
finishGitHubAuthorization.bind(null, dependencies, { attempt, profile, verifiedAt, admitted }),
)
}
5 changes: 5 additions & 0 deletions hub/server/domains/identity/create-csrf-token/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { digestSecret } from "#server/utils/digest-secret"

export function createCsrfToken(token: string): string {
return digestSecret(`csrf:${token}`)
}
Loading