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 @@ -6,3 +6,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 credential provider. See docs/identity.md.
HUB_CREDENTIAL_PROVIDER=database

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

# The environment provider is read-only and requires every GitHub 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 @@ -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.
- [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, background jobs, 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. Identity models store credentials, users, sessions, sign-in attempts, and security records. [Credential providers](identity.md) use encrypted database values or read-only environment settings. Sign-in routes, deployment, background jobs, and product APIs have not been built. This app cannot yet find repositories, import Builds, host previews, save product data, or support reviews.

## Backend

Expand Down
8 changes: 8 additions & 0 deletions hub/docs/backend-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ Put each function in a directory named with lowercase words and hyphens, such as

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.

`staticSettingsRecordId` identifies Hub's single settings row, not a person or GitHub installation. Its fixed UUID lets every process lock the same row. Keep it with the identity-state repository.
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 [identity boundary](identity.md); product models have not been added.

[Compose](../.devcontainer/compose.yaml) pins the development database image. [package.json](../package.json) pins the Prisma packages to the same version. Dependency updates must follow the [seven-day dependency policy](../../docs/testing-and-review.md#update-dependencies).

Expand Down
24 changes: 24 additions & 0 deletions hub/docs/identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Hub identity

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

## Credential providers

Set `HUB_CREDENTIAL_PROVIDER` in the [environment example](../.env.example):

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

Both providers require 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.

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. Never commit credentials.

## Saved records

Apply the committed migration with `npm run db:deploy`. See [Database](database.md) for setup and schema rules.

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.
4 changes: 3 additions & 1 deletion 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 shared server test helpers under the owning function's `test/` directory. TypeScript checks them, 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,7 +53,7 @@ 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. There are no database test cases yet; an empty run does not prepare or exercise the databases.
The database command uses `--passWithNoTests` so an empty selection exits successfully. It still fails when tests fail. Identity repository cases exercise persistence, one-use attempts, transaction rollback, and concurrent revocation locks.

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.

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 All @@ -22,7 +36,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
14 changes: 14 additions & 0 deletions hub/schema/identity-state.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
model IdentityState {
// ID columns
id String @id @db.Uuid

// Attribute columns
generation Int @default(0)
encryptedCredentials String? @map("encrypted_credentials")

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

@@map("identity_states")
}
89 changes: 89 additions & 0 deletions hub/schema/migrations/20261026195151_identity/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
-- CreateTable
CREATE TABLE "identity_states" (
"id" UUID NOT NULL,
"generation" INTEGER NOT NULL DEFAULT 0,
"encrypted_credentials" TEXT,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "identity_states_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "oauth_attempts" (
"id" UUID NOT NULL,
"session_id" UUID,
"state_digest" TEXT NOT NULL,
"browser_digest" TEXT NOT NULL,
"verifier" TEXT NOT NULL,
"generation" INTEGER NOT NULL,
"credential_revision" TEXT NOT NULL,
"purpose" TEXT NOT NULL,
"expires_at" TIMESTAMPTZ(3) NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "oauth_attempts_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "security_audits" (
"id" UUID NOT NULL,
"actor_id" UUID,
"session_id" UUID,
"event" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "security_audits_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "sessions" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"token_digest" TEXT NOT NULL,
"signed_in_at" TIMESTAMPTZ(3) NOT NULL,
"fresh_authorized_at" TIMESTAMPTZ(3),
"credential_revision" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "users" (
"id" UUID NOT NULL,
"provider" TEXT NOT NULL DEFAULT 'github',
"provider_id" TEXT NOT NULL,
"login" TEXT NOT NULL,
"name" TEXT,
"avatar_url" TEXT,
"administrator" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "oauth_attempts_state_digest_key" ON "oauth_attempts"("state_digest");

-- CreateIndex
CREATE INDEX "oauth_attempts_expires_at_idx" ON "oauth_attempts"("expires_at");

-- CreateIndex
CREATE INDEX "security_audits_created_at_idx" ON "security_audits"("created_at");

-- CreateIndex
CREATE UNIQUE INDEX "sessions_token_digest_key" ON "sessions"("token_digest");

-- CreateIndex
CREATE INDEX "sessions_user_id_idx" ON "sessions"("user_id");

-- CreateIndex
CREATE UNIQUE INDEX "users_provider_provider_id_key" ON "users"("provider", "provider_id");

-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
3 changes: 3 additions & 0 deletions hub/schema/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
21 changes: 21 additions & 0 deletions hub/schema/oauth-attempt.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
model OAuthAttempt {
// ID columns
id String @id @default(uuid()) @db.Uuid
sessionId String? @map("session_id") @db.Uuid

// Attribute columns
stateDigest String @unique @map("state_digest")
browserDigest String @map("browser_digest")
verifier String
generation Int
credentialRevision String @map("credential_revision")
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])
@@map("oauth_attempts")
}
16 changes: 16 additions & 0 deletions hub/schema/security-audit.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
model SecurityAudit {
// ID columns
id String @id @default(uuid()) @db.Uuid
actorId String? @map("actor_id") @db.Uuid
sessionId String? @map("session_id") @db.Uuid

// Attribute columns
event String

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

@@index([createdAt])
@@map("security_audits")
}
21 changes: 21 additions & 0 deletions hub/schema/session.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
model Session {
// 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)

// Attribute columns
tokenDigest String @unique @map("token_digest")
signedInAt DateTime @map("signed_in_at") @db.Timestamptz(3)
freshAuthorizedAt DateTime? @map("fresh_authorized_at") @db.Timestamptz(3)
credentialRevision String @map("credential_revision")

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

@@index([userId])
@@map("sessions")
}
22 changes: 22 additions & 0 deletions hub/schema/user.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
model User {
// ID columns
id String @id @default(uuid()) @db.Uuid

// Relations
sessions Session[]

// Attribute columns
provider String @default("github")
providerId String @map("provider_id")
login String
name String?
avatarUrl String? @map("avatar_url")
administrator Boolean @default(false)

// 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, providerId])
@@map("users")
}
Loading