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
13 changes: 13 additions & 0 deletions hub/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,16 @@ DATABASE_URL=postgresql://replica:replica@127.0.0.1:5432/replica

# Only database-enabled tests require this URL and CREATEDB permission.
TEST_DATABASE_URL=postgresql://replica:replica@127.0.0.1:5432/postgres

# Select the GitHub source credential provider. See docs/identity.md.
HUB_GITHUB_CREDENTIAL_PROVIDER=database

# Generate separate values outside the database. See docs/identity.md.
# HUB_GITHUB_CREDENTIAL_KEY=<openssl rand -hex 32>

# The environment provider is read-only and requires every GitHub integration setting below.
# GITHUB_APP_ID=
# GITHUB_INSTALLATION_ID=
# GITHUB_CLIENT_ID=
# GITHUB_CLIENT_SECRET=
# GITHUB_PRIVATE_KEY=
1 change: 1 addition & 0 deletions hub/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Hub is being rebuilt. The [Hub overview](../README.md) describes what the baseli
- [Database](database.md): Prisma schema conventions, migrations, and the shared client.
- [Logging](logging.md): application events, automatic request records, and payload safety.
- [Background jobs](background-jobs.md): PostgreSQL queues, workers, schedules, setup, and test isolation.
- [Identity](identity.md): credential storage and providers.
- [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: 1 addition & 1 deletion hub/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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 no product models yet. 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 [identity foundation](identity.md) separates Hub users, external identities, installation membership, authentication provider configuration, sessions, authorization attempts, security records, and GitHub source credentials. Sign-in routes, deployment, and product APIs have not been built. This app cannot yet authenticate users, evaluate prototype access, find repositories, import Builds, host previews, save product data, or support reviews.

## Backend

Expand Down
10 changes: 10 additions & 0 deletions hub/docs/backend-architecture.md
Comment thread
alliscode marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ Export route handlers by HTTP method, such as `get()`, and alias them to descrip

Add directories when their functions are needed. Do not add empty implementations just to fill out the structure.

Keep function implementations flat. A composition factory may bind imported functions to dependencies, but must not contain their implementations. Put schemas and constants in a colocated `constants.ts`, and types in `types.ts`.

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.

## 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).

Compose repository functions in domains. The identity store binds standalone queries to a database client or transaction there; a repository must not import its neighboring queries to build that store.

`staticIdentityStoreRecordId` identifies the row that serializes identity store transactions. It is not a user, provider configuration, or GitHub installation. Keep it with the identity store state repository.

GitHub source credentials use a separate fixed credentials row. Authentication provider configuration must not read or write those credentials.
6 changes: 5 additions & 1 deletion hub/docs/coding-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ Follow the [database conventions](database.md#schema-conventions) for Prisma mod
- Use TypeScript for new application code and tests.
- Put each standalone function or component in a kebab-case directory with `index.ts` or `index.tsx`, colocated tests, types, and styles.
- Export at most one function per file and import exact directories rather than sibling-export barrels.
- Keep helpers in their own function directories. Put schemas and constants in `constants.ts` and types in `types.ts`.
- Group package, alias, and relative imports separately.
- Omit `/index.ts` and `/index.tsx` from imports in frontend code and Vitest tests. Keep explicit `.ts` file paths in server runtime and Node-loaded tooling, including Storybook configuration. Native Node does not resolve directory imports; the server compiler rewrites its paths for production.
- Use `@/` for browser code and `#server/` for API code. Import function directories without `/index.ts`, such as `#server/utils/digest-secret`. Import colocated constants and types from their named files.
- [Package imports](../package.json) resolve `#server/` to source with Node's `development` condition and to compiled JavaScript otherwise. `npm run dev:api` selects that condition; `npm start` uses compiled files. TypeScript and Vitest resolve source through the same mappings. Native Node package aliases must start with `#`; `@/` remains browser-only.
- Keep explicit `.ts` paths for relative imports in native Node modules and tooling, including Storybook configuration. The server compiler rewrites relative extensions for the build.
- Prefer named alias imports such as `#server/integrations/github/identify-user/constants` over relative paths when a mapping exists. Vite resolves extensionless relative paths, but native Node does not. TypeScript `paths` alone cannot make `@/server/...` work in Node or rewrite it in emitted JavaScript.
- Keep HTTP parsing and responses outside domains. Validate input and return explicit errors when introducing real API behavior.

## Frontend layers
Expand Down
2 changes: 1 addition & 1 deletion hub/docs/database.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hub database

Hub uses PostgreSQL and Prisma. Product models and migrations have not been added.
Hub uses PostgreSQL and Prisma. The initial migration stores the provider-neutral [identity foundation](identity.md); product models have not been added.

[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
36 changes: 36 additions & 0 deletions hub/docs/identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Hub identity

Hub persists a provider-neutral identity foundation. User authentication proves an external identity. Hub installation membership records whether that user belongs to this Hub and whether they are an administrator. This persistence layer does not define prototype access rules.

## Persisted boundaries

- `User` is the Hub-owned account. It does not contain a provider identifier or administrator flag.
- `ExternalIdentity` links a provider, issuer, and subject to one Hub user. One user may link several identities. The issuer is part of uniqueness so separate OIDC tenants may reuse subjects safely.
- `InstallationMembership` stores a user's status, role, and source for this Hub installation.
- `AuthenticationProvider` and its versioned revisions store authentication configuration without tying it to one provider. An OIDC configuration does not require GitHub source credentials.
- `Session` records the Hub user, external identity, and authentication provider revision that established it. Rotating GitHub source credentials does not change session identity context.
- `AuthorizationAttempt` stores digested state and browser bindings, a PKCE verifier, an optional OIDC nonce, provider revision, purpose, optional originating session, and expiry. Consumption deletes the record in the same transaction so concurrent callbacks cannot reuse it.
- `SecurityAudit` stores the event and minimal optional identity context.

Queries are available without a lock. Mutations run through one database transaction that locks a fixed store row. Related records and audit events therefore commit or roll back together, and overlapping mutations observe committed state.

Security audit queries return at most 100 records from the previous 90 days. Each new audit write deletes records older than 90 days. Operators who require a longer security history must export records before that limit.

## GitHub source credentials

GitHub source access is an optional integration boundary, not the global identity boundary. Set `HUB_GITHUB_CREDENTIAL_PROVIDER` in the [environment example](../.env.example):

| Provider | Credentials |
| --- | --- |
| `database` (default) | Generate `HUB_GITHUB_CREDENTIAL_KEY` with `openssl rand -hex 32`. The provider encrypts GitHub integration credentials before storage and authenticates their context when reading. |
| `environment` | Set every `GITHUB_*` value, including `GITHUB_INSTALLATION_ID`. This provider is read-only and ignores database credentials. |

Both providers currently validate an App ID, installation ID, client ID, client secret, and RSA private key. IDs are positive decimal strings. The key must contain at least 2048 bits.

Database credentials use AES-256-GCM with contextual additional authenticated data. Keep the encryption key outside the database and back it up separately. Losing or replacing it makes saved credentials unreadable. Invalid provider settings or unreadable ciphertext cause errors. Hub never falls back to another provider.

## Foundation scope

This layer provides persistence only. It does not add HTTP sign-in routes, production OIDC or SCIM flows, prototype or product models, an authorization evaluator, GitHub permission synchronization, artifact ingestion, or UI. GitHub authentication configuration and GitHub source credentials remain separate so either capability can change without redefining Hub identity.

Apply the rewritten initial migration with `npm run db:deploy`. See [Database](database.md) for setup and schema rules.
8 changes: 5 additions & 3 deletions hub/docs/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ Each request produces one final record with `reqId`, `method`, `route`, `statusC

Automatic records exclude raw URLs, headers, cookies, uploads, response bodies, and error details. JSON parsing uses Express's default 100 KiB limit; rejected bodies are not logged. The Express error handler returns generic JSON errors, or closes the connection if headers were sent, without exposing raw errors.

For private free text or other sensitive payloads, exclude all payload fields before sending a response:
Authentication routers should install `omitRequestLogPayload` before their handlers so callback payloads are excluded by default:

```ts
response.locals.omitLogPayload = true
import { omitRequestLogPayload } from "../integrations/pino/omit-request-log-payload/index.ts"

router.use(omitRequestLogPayload)
```

This omits `params`, `query`, and `body` while keeping request metadata.
Expand All @@ -42,6 +44,6 @@ This omits `params`, `query`, and `body` while keeping request metadata.

Application fields, `request.log` fields, and automatic payloads share the same sanitizer. It copies data without changing the request or invoking getters or custom serialization methods.

Keys containing password, passwd, passphrase, secret, token, authorization, cookie, API key, private key, or session ID become `[REDACTED]`. Matching ignores case and punctuation and includes nested objects and arrays. Exclude payloads whose sensitive fields do not match these rules.
Keys containing password, passwd, passphrase, secret, token, authorization, cookie, API key, private key, or session ID become `[REDACTED]`. Automatic request payloads also redact OAuth and OIDC `code`, `state`, `nonce`, and `code_verifier` fields. Matching ignores case and punctuation and includes nested objects and arrays. Exclude payloads whose sensitive fields do not match these rules.

Structured fields have [size, depth, and entry limits](../server/utils/logging/sanitize-log-value/constants.ts). Markers replace truncated data, cycles, getters, and unsupported values. These limits do not apply to messages or custom bindings and do not replace endpoint validation or privacy decisions.
6 changes: 4 additions & 2 deletions hub/docs/testing-and-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Arrange each case's inputs and records inside that case. Act through its public

Tests must release resources they create, such as servers, files, sockets, and event listeners. Use `try/finally` or register disposal with Vitest's `onTestFinished` when creating the resource. Clearing the module cache only affects subsequent imports. Reset any deliberately shared fixture state before each case.

Keep project-wide test helpers in the appropriate `test/` directory and function-specific helpers beside their owning function. TypeScript checks both, but the API build excludes them.

## Parallel database tests

Each test run uses separate databases so parallel tests cannot change another worker's records or clear the development database. Vitest runs files in parallel in [forked worker processes](https://vitest.dev/config/pool). Cases within a worker run sequentially, including cases marked concurrent, so database resets cannot erase another case's records. Use `--maxWorkers` to set file-level parallelism.
Expand All @@ -51,15 +53,15 @@ Use `.test.ts` or `.test.tsx` for ordinary cases. Use `.db.test.ts` or `.db.test

Start the PostgreSQL service as described in [Development](development.md) and set `TEST_DATABASE_URL` using `.env.example`. The devcontainer supplies this variable. Use a local database user with permission to create and drop databases, never a production connection.

The database command uses `--passWithNoTests` so an empty selection exits successfully. It still fails when tests fail. The job integration cases exercise real PostgreSQL queues, schedules, and database resets.
The database command uses `--passWithNoTests` so an empty selection exits successfully. It still fails when tests fail. Identity repository cases exercise external identity linking and uniqueness, installation membership, provider-specific authorization attempts, session context, independent GitHub credentials, transaction rollback, and concurrent store locks. Job integration cases exercise real PostgreSQL queues, schedules, and database resets.

Database-enabled jsdom tests use the browser setup and database hooks together. They can arrange records through server code, but frontend code must not import server modules. The setup does not start an API or connect frontend requests to one.

The database setup follows this lifecycle:

1. Before workers start, setup creates a template database with a unique name for each project and run. It applies the current Prisma schema with `prisma db push` and BullMQ's migrations, closes those setup connections, then copies the template for every worker slot.
2. Each worker receives its own `DATABASE_URL` before application modules load. Setup uses Vitest's [`VITEST_POOL_ID`](https://vitest.dev/guide/migration/jest#envs), which stays within the configured worker limit.
3. Before every case, setup empties application and job tables across user schemas and resets generated identity counters. It preserves `public._prisma_migrations` and `bullmq.migration`. Each case creates its own records; there is no shared seed data.
3. Before every case, setup empties application and job tables across user schemas. It preserves `public._prisma_migrations` and `bullmq.migration`. Each case creates its own records; there is no shared seed data.
4. After the run, teardown drops only the databases it created, even if tests left connections open.

Unique database names also isolate simultaneous runs. Before clearing tables, cleanup checks that the database name belongs to the test namespace and matches the connected database.
Expand Down
16 changes: 15 additions & 1 deletion hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
"version": "0.1.0",
"private": true,
"type": "module",
"imports": {
"#server/*/constants": {
"development": "./server/*/constants.ts",
"default": "./dist/server/*/constants.js"
},
"#server/*/types": {
"development": "./server/*/types.ts",
"default": "./dist/server/*/types.js"
},
"#server/*": {
"development": "./server/*/index.ts",
"default": "./dist/server/*/index.js"
}
},
"engines": {
"node": "^24.15.0 || ^26.0.0"
},
Expand Down Expand Up @@ -30,7 +44,7 @@
"build": "npm run typecheck && vite build && tsc --project tsconfig.server.json",
"dev": "npm run dev:client",
"dev:client": "vite --host 127.0.0.1",
"dev:api": "node --watch server/index.ts",
"dev:api": "node --conditions=development --watch server/index.ts",
"start": "node dist/server/index.js",
"preview": "vite preview --host 127.0.0.1",
"extension:assets:check": "node ../.github/extensions/replica-prototype/check-assets/index.mjs",
Expand Down
23 changes: 23 additions & 0 deletions hub/schema/authentication-provider-revision.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
model AuthenticationProviderRevision {
// ID columns
id String @id @default(uuid()) @db.Uuid
authenticationProviderId String @map("authentication_provider_id") @db.Uuid

// Relations
authenticationProvider AuthenticationProvider @relation(fields: [authenticationProviderId], references: [id], onDelete: Cascade)
authorizationAttempts AuthorizationAttempt[]
sessions Session[]

// Attribute columns
revision Int
issuer String
configuration Json

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

@@unique([authenticationProviderId, revision])
@@index([issuer])
@@map("authentication_provider_revisions")
}
18 changes: 18 additions & 0 deletions hub/schema/authentication-provider.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
model AuthenticationProvider {
// ID columns
id String @id @default(uuid()) @db.Uuid

// Relations
revisions AuthenticationProviderRevision[]

// Attribute columns
key String @unique
type String
enabled Boolean @default(true)

// 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_providers")
}
27 changes: 27 additions & 0 deletions hub/schema/authorization-attempt.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
model AuthorizationAttempt {
// ID columns
id String @id @default(uuid()) @db.Uuid
authenticationProviderRevisionId String @map("authentication_provider_revision_id") @db.Uuid
originatingSessionId String? @map("originating_session_id") @db.Uuid

// Relations
authenticationProviderRevision AuthenticationProviderRevision @relation(fields: [authenticationProviderRevisionId], references: [id], onDelete: Cascade)
originatingSession Session? @relation("OriginatingSession", fields: [originatingSessionId], references: [id], onDelete: Cascade)

// Attribute columns
stateDigest String @unique @map("state_digest")
browserDigest String @map("browser_digest")
pkceVerifier String @map("pkce_verifier")
nonce String?
purpose String
expiresAt DateTime @map("expires_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([authenticationProviderRevisionId])
@@index([originatingSessionId])
@@map("authorization_attempts")
}
25 changes: 25 additions & 0 deletions hub/schema/external-identity.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
model ExternalIdentity {
// ID columns
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid

// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
sessions Session[]

// Attribute columns
provider String
issuer String
subject String
username String?
displayName String? @map("display_name")
avatarUrl String? @map("avatar_url")

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

@@unique([provider, issuer, subject])
@@index([userId])
@@map("external_identities")
}
Loading
Loading