Skip to content

feat(#58): provision per-install GitHub App via Manifest flow#64

Merged
G0maa merged 20 commits into
mainfrom
feature/GH-58-github-app-provisioning-manifest-flow
Jun 11, 2026
Merged

feat(#58): provision per-install GitHub App via Manifest flow#64
G0maa merged 20 commits into
mainfrom
feature/GH-58-github-app-provisioning-manifest-flow

Conversation

@G0maa

@G0maa G0maa commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Per-install GitHub App provisioning via the Manifest flow — each Marsa install creates its own GitHub App (a single central App can't serve operator-chosen domains; see AgDR-0005-github-app-integration-model). New github-app API slice exposes GET /api/v1/github-app/manifest (builds the manifest + an HMAC-signed CSRF state) and POST /api/v1/github-app/conversions (verifies state, exchanges GitHub's one-time code, then encrypts + persists the returned credentials).
  • Secrets encrypted at rest — a cross-cutting SecretCipher (src/modules/crypto/) encrypts client_secret, webhook_secret, and the private-key PEM with AES-256-GCM (Node built-in, zero new deps; key from APP_SECRETS_ENCRYPTION_KEY). Stored as base64 iv‖authTag‖ciphertext. Reused by GitHub Auth #23 to mint installation tokens. Rationale + alternatives in AgDR-0006-github-app-credential-storage.
  • First DB entity + migration — adds the github_app table (encrypted *_enc columns), removes MikroORM's empty-schema flag, and ships the generated migration. Schema change tracked on this ticket via the migration label + AgDR-0007-migration-github-app-table (rollback = DROP TABLE, zero blast radius).
  • FE drives the flow — the operator hits the web origin, so /setup/github renders the auto-submitting manifest form to GitHub and /setup/github/callback completes the conversion and surfaces the install link. Responses are Zod-validated at the boundary per the web↔api contract.
  • Added @mikro-orm/cli — the repo's migration:* scripts referenced the mikro-orm bin but the CLI dep was never installed (this is the first migration, so nobody had hit it). Added to the catalog + api devDeps.

Decision records

  • AgDR-0006-github-app-credential-storage — encryption at rest + Manifest-flow implementation choices
  • AgDR-0007-migration-github-app-table — the schema migration (rollback, blast radius, observability)

Testing

  1. pnpm --filter api test — 27 tests, coverage 91% lines / 81% branches / 94% funcs (gates 80/80/90). Covers the cipher (round-trip, tamper, wrong/missing key), state sign/verify (valid/expired/tampered/malformed), manifest builder, the convert service (encrypted persistence, invalid-state + missing-code + GitHub-failure paths), and both endpoints e2e.
  2. pnpm --filter web test — 32 tests, coverage 90% lines / 100% branches. Covers the provisioning composable (fetch/convert/contract-violation), the manifest form builder, and both pages (render, success, error states).
  3. pnpm format:check && pnpm lint && pnpm build — all green; openapi.json regenerated and committed (CI drift-checked); web client regenerated from it.
  4. Manual happy-path needs a public tunnel (ngrok/cloudflared) so GitHub redirects reach the install — out of scope for CI.

Out of scope (follow-ups)

  • Installation-token minting, webhook receiver, repo selection → GitHub Auth #23.
  • Operator login / first-admin allowlist → Marsa Auth #22.
  • Chart wiring of APP_SECRETS_ENCRYPTION_KEY, MARSA_WEB_URL, MARSA_API_PUBLIC_URLmarsa-charts follow-up.

Refs #58

Glossary

Term Definition
GitHub App Manifest flow GitHub mechanism to register an App from a JSON manifest the browser POSTs to GitHub; GitHub returns a one-time code that's exchanged for the App's credentials.
Conversion POST https://api.github.com/app-manifests/{code}/conversions — exchanges the one-time code for the App id, client_id, client_secret, webhook_secret, and private-key pem.
AES-256-GCM Authenticated symmetric encryption; the GCM auth tag makes tampering or a wrong key fail loudly on decrypt rather than returning garbage.
HMAC-signed state A stateless CSRF token (base64url(payload).hmac, payload = nonce + expiry) that travels to GitHub and back; verified for signature + freshness on the callback.
*_enc column A column storing AES-256-GCM ciphertext rather than plaintext.
Installation token A short-lived (~1h) token minted from the App's private key to access repos — used by #23, not this PR.

Summary by CodeRabbit

  • New Features

    • GitHub App provisioning: UI pages to start/connect and complete installation, plus API endpoints to fetch a manifest and convert/finish installation.
    • Client-side helpers to build/submit the GitHub manifest form.
  • Security / Data

    • Secure credential storage: AES-256-GCM encryption for app secrets with startup key validation and global crypto service.
    • DB-backed single-use manifest state (CSRF) for the manifest flow.
  • Documentation

    • ADRs covering credential storage, migrations, config, and request validation.
  • Tests & Chores

    • Extensive unit/e2e tests added and workspace/dev deps updated; test env vars for the feature included.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements GitHub App provisioning: manifest issuance and conversion endpoints, DB-backed single-use state, AES-256-GCM secret encryption, GitHub manifest client, entity persistence/migrations, frontend form flow and pages, OpenAPI/types/Zod, global request validation, tests, tooling, and docs.

Changes

GitHub App Manifest Provisioning

Layer / File(s) Summary
Encryption and single-use state primitives
apps/api/src/modules/crypto/*, apps/api/src/app/github-app/manifest-state/*, apps/api/src/app/github-app/entities/manifest-state.entity.ts
SecretCipherService implements AES-256-GCM encryption/decryption with random IV and auth-tag verification. ManifestState entity and ManifestStateService replace stateless HMAC with atomic delete-on-consume DB-backed tokens. Comprehensive unit and DB-integration tests cover encryption round-trips, tamper detection, expired tokens, and malformed inputs.
GitHub app entity, builder, manifest state entity, and database migrations
apps/api/src/app/github-app/entities/*, apps/api/src/sql/migrations/Migration*.ts, apps/api/src/sql/mikro-orm.config.ts
GitHubApp entity maps to github_app table with encrypted secret columns, unique constraints on github_app_id and slug, and timestamps. GitHubAppBuilder provides fluent construction API. ManifestState entity persists single-use tokens. Migrations create the tables and add unique constraints; ORM discovery config updated to recognize entities.
Shared GitHub config, manifest types, and GitHub client integration
apps/api/src/app/github-app/github-app.config.ts, apps/api/src/app/github-app/github-app.types.ts, apps/api/src/modules/github-client/*
githubAppConfig factory validates MARSA_WEB_URL and MARSA_API_PUBLIC_URL via Joi at startup, derives webhook/redirect/OAuth URLs. ManifestDto and HookAttributesDto model GitHub manifest payload. GitHubManifestClient calls GitHub’s conversion endpoint and maps responses to normalized credentials. Unit tests validate config, mapping, and HTTP error handling.
Manifest retrieval endpoint
apps/api/src/app/github-app/use-cases/get-manifest/*
GetManifestController exposes GET /api/v1/github-app/manifest. GetManifestUseCase issues a single-use state via ManifestStateService, builds the manifest DTO from config and returns GetManifestResponse with formAction and state. Unit and e2e tests verify manifest structure and state behavior.
Manifest conversion and credential persistence endpoint
apps/api/src/app/github-app/use-cases/convert-manifest/*
ConvertManifestController exposes POST /api/v1/github-app/convert-manifest. ConvertManifestUseCase.execute() consumes the state, exchanges code with GitHub via GitHubManifestClient, encrypts secrets with SecretCipherService, and performs idempotent upsert of GitHubApp (handles unique-constraint races). Returns sanitized ConvertManifestResponse. Tests cover validation, error mapping, idempotency, and encryption round-trips.
API composition, global validation, and contract publication
apps/api/src/app.module.ts, apps/api/src/modules/api/api.module.ts, apps/api/src/entrypoints/api.ts, apps/api/src/test/setup/test-bench.ts, apps/api/openapi.json, apps/web/app/api/*
CryptoModule and ConfigModule wired into AppModule; GitHubAppModule imported by ApiModule. Global ValidationPipe configured for whitelist/transform/forbidNonWhitelisted in bootstrap and tests. OpenAPI spec adds the new endpoints and schemas; web types/Zod regenerated and exported for client usage.
Frontend provisioning composable and setup pages
apps/web/app/composables/useGithubProvisioning.ts, apps/web/app/pages/setup/github/*
useGithubProvisioning() composable calls manifest/conversion endpoints and validates responses with Zod. buildManifestForm() creates the POST form embedding the manifest, submitManifestForm() submits it to GitHub. Index and callback pages implement the client flow and UI messages. Tests validate form construction, missing params, success, and failure paths.
Tooling updates, test configuration, and architecture docs
apps/api/eslint.config.mjs, apps/api/package.json, apps/api/.env.test, pnpm-workspace.yaml, apps/api/.claude/CLAUDE.md, docs/agdr/AgDR-*.md
ESLint override added for migration files; test coverage excludes migrations; workspace catalogs add @mikro-orm/cli, @nestjs/config, joi, class-validator, class-transformer. Test env includes dummy encryption key and URL fixtures. CLAUDE.md and ADRs document entity conventions, encryption-at-rest, config validation, request validation, and migration/state decisions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • marsa-cloud/marsa#33: Regenerates OpenAPI→web types/Zod and adds an API contract similar to how /api/v1/status was previously added.

Poem

🐰 I nibbled keys and hid them tight,

States that blossom, single-flight,
Manifests jump from form to site,
Secrets sealed in GCM's light,
Hops of tests and docs tonight.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/GH-58-github-app-provisioning-manifest-flow

@G0maa G0maa left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Review: PR #64

Commit: 727f247878801614907a738921115720e9a19427

Summary

Reviewed the GitHub App Manifest provisioning slice with a focus on the four credential-handling primitives: the AES-256-GCM SecretCipherService, the HMAC-signed CSRF state, the convert-manifest conversion endpoint, and secret persistence. The cryptographic core is well-built — correct AEAD construction, fresh random IV per encryption, constant-time signature comparison, fail-fast key validation, and ciphertext-only persistence. No CRITICAL or HIGH issues. The findings below are MEDIUM-and-below, and the documented v0.1 tradeoffs in AgDR-0006 are acceptable for a one-time, pre-auth, first-run provisioning flow.

Checklist Results

  • Secrets & Credentials: Pass — only ciphertext persisted; .env.test key is a verified dummy (see below)
  • Injection Prevention: Pass — MikroORM parameterised writes; code URL-encoded into the GitHub path
  • XSS Prevention: Pass — no v-html, no secret reflection; Vue escapes interpolation
  • Auth & Authorisation: Pass (with documented v0.1 caveat) — endpoint is intentionally unauthenticated pre-#22; CSRF state is the only guard
  • Data Protection: Pass
  • API Security: Partial — no input-validation pipe, no rate limiting (see MEDIUM-1, MEDIUM-2)

Crypto core — verified correct

SecretCipherService (src/modules/crypto/secret-cipher.service.ts)

  • AES-256-GCM via Node built-in; 32-byte key enforced at construction (fail-fast on missing/wrong length) — good.
  • Fresh 12-byte random IV per encrypt() call (randomBytes(IV_LENGTH)) — IV uniqueness is correct; no nonce reuse risk.
  • Auth tag captured via getAuthTag() and verified via setAuthTag() on decrypt; decipher.final() throws on tamper/wrong-key — correct AEAD failure mode.
  • Length guard (< IV + TAG) before slicing prevents a malformed-token panic — good.

StateSigner (src/app/github-app/state-signer.ts)

  • HMAC-SHA256 with a domain-separated subkey derived from the root key (HMAC(root, "github-app-state-v1")) — correct separation from the encryption key.
  • Signature compared with timingSafeEqual behind a length pre-check — constant-time, correct.
  • Expiry enforced (payload.exp > now); malformed/garbage payload caught and rejected. Solid.

Security Issues Found

MEDIUM-1 — No input-validation pipe on the conversion endpoint. There is no global ValidationPipe, no class-validator decorators on ConvertManifestRequest, and forbidNonWhitelisted is not set. code/state are typed but not validated at the framework boundary — the only guards are the manual if (!request.code) and stateSigner.verify(request.state) inside the service. verify() does call String.split / Buffer.from on state, so a non-string state (e.g. {state: {}} or an array) would reach those calls and throw an unhandled TypeError → 500 rather than a clean 400. Impact is low (DoS-resistant, no bypass), but the boundary should reject malformed input deterministically.
Remediation: register a global ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }) and add @IsString() @IsNotEmpty() to both DTO fields. This also hardens every future endpoint.

MEDIUM-2 — No rate limiting on GET /manifest or POST /conversions. Both endpoints are unauthenticated (by design, pre-#22). POST /conversions accepts a code + state; an attacker who obtains a valid unexpired state (it travels through the browser and GitHub) can replay it freely within the 10-min window, and GET /manifest can be hammered to mint unlimited state tokens. No DB-backed nonce store means there is no server-side single-use enforcement.
Remediation: add @nestjs/throttler on these routes. Acceptable to defer to the #22 follow-up if explicitly tracked, but it should be noted as a known gap, not silent.

LOW-1 — State is not single-use (replay within TTL). The nonce in the payload is generated but never persisted, so the state token is replayable until exp. AgDR-0006 documents the not-session-bound limitation and defers binding to #22, which covers the cross-browser CSRF angle. The single-use angle is a narrower instance of the same class. Acceptable for v0.1 one-time provisioning; flag it in the #22 scope so the session-bound rewrite also makes state single-use (e.g. consume-on-verify against a store or session).

LOW-2 — Upstream GitHub error body is propagated to the client. GitHubManifestClient.convertManifest builds the thrown message from response.status + body (raw GitHub response text), and ConvertManifestService rewraps it verbatim in BadGatewayException(error.message). GitHub's manifest-conversion error bodies don't contain Marsa secrets, but echoing an upstream provider's raw response to the caller is an information-disclosure smell and can leak internal detail if the upstream contract changes. The FE already shows a generic message, so the detailed body only helps an attacker probing the endpoint.
Remediation: log the upstream body server-side; return a generic 502 message to the client.

LOW-3 — Conversion code and decrypted secrets rely on absence-of-logging rather than explicit redaction. The service never logs creds and never returns secrets in the response (verified: response is appSlug/appName/htmlUrl/installUrl only) — good. But there's no structural guard (e.g. a toJSON/redaction wrapper) preventing a future logger.debug(creds) or an exception serializer from spilling clientSecret/webhookSecret/pem. Given the value of these secrets, consider wrapping them in a redacting type or at least an inline comment at the credential boundary. Informational.


Notes that are NOT issues (verified)

  • .env.test key is safe. APP_SECRETS_ENCRYPTION_KEY=Kioq…Kio= decodes to exactly 32 bytes, all 0x2A (*) — a verified dummy fixture, not a real secret. No concern. Keep the inline (NOT real secrets) comment.
  • Only ciphertext persisted. GitHubApp entity stores clientSecretEnc / webhookSecretEnc / privateKeyPemEnc as text; client_id / app id / slug / name / htmlUrl / owner are non-secret and stored plaintext, consistent with AgDR-0006. Confirmed no plaintext secret column.
  • code handled safely. encodeURIComponent(code) into the GitHub path prevents path injection; the code is short-lived (~1h) and never persisted or logged.
  • No injection / XSS surface. Writes go through MikroORM (parameterised); FE uses interpolation only, no v-html, no secret reflection.

Assessment of AgDR-0006 tradeoffs (v0.1)

Acceptable. The choices — AES-256-GCM with zero new deps, env-var key with operator-owned key management, stateless HMAC state instead of session-bound, built-in fetch over Octokit — are sound and explicitly scoped to a one-time first-run flow with the session-binding gap consciously deferred to #22. The AgDR correctly calls out that pre-auth signed-state is weaker than a session and that key rotation is not implemented. Recommend the #22 follow-up also (a) bind state to the admin session AND (b) make it single-use, and that MEDIUM-1/MEDIUM-2 are tracked rather than silently inherited.

Verdict

COMMENT — no blocking (CRITICAL/HIGH) findings. The cryptographic core is correct and secrets are handled properly. MEDIUM-1 (validation pipe) and MEDIUM-2 (rate limiting) should be addressed or explicitly tracked before this surface is reachable in production; LOW items are hardening. Safe to proceed for v0.1 with those follow-ups recorded.


🛡️ Reviewed by Hakim (Security Auditor)
📌 Reviewed commit: 727f247878801614907a738921115720e9a19427

@G0maa G0maa left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #64

Commit: 727f247878801614907a738921115720e9a19427

Posted as a comment because GitHub blocks request-changes on one's own PR. Verdict is CHANGES REQUESTED — treat this as blocking; the Rex approval marker has NOT been written.

Summary

Implements per-install GitHub App provisioning via the Manifest flow (#58): a github-app NestJS vertical slice (get-manifest + convert-manifest use-cases), a cross-cutting AES-256-GCM SecretCipher in src/modules/crypto, the project's first MikroORM entity + migration (github_app), and two Nuxt pages (/setup/github + callback). Architecture, AgDR linkage, glossary, and test design are all strong. The blocker is red CI, not code quality.

Checklist Results

  • Architecture & Design: Pass
  • Code Quality: Pass
  • Testing: Pass
  • Security: Pass (with one CI-surfaced item, below)
  • Performance: Pass
  • PR Description & Glossary: Pass
  • Summary Bullet Narrative: Pass (narrative bullets with rationale)
  • Technical Decisions (AgDR):Pass — AgDR-0006 (credential storage) + AgDR-0007 (migration) linked and cover the diff
  • Adopter Handbooks: Pass (Migration Safety blocking handbook: N/A — purely additive table)

Issues Found

⛔ BLOCKER — CI is red. Gate 5 (workflow-gates.md) + "No Red CI Before Merge" (pr-quality.md) cannot pass. Two checks fail:

  1. SAST (Semgrep) — blocking finding introduced by THIS PR.
    apps/api/src/modules/crypto/secret-cipher.service.ts:57 — rule javascript.node-crypto.security.gcm-no-tag-length. createDecipheriv('aes-256-gcm', key, iv) is called without an explicit { authTagLength: 16 }. Semgrep flags this as blocking because an unconstrained GCM tag length can, in general, let an attacker present a truncated tag.

    Your code already slices the tag to a fixed 16 bytes and setAuthTag rejects a wrong-length buffer, so real-world forgery risk is low — but it is a blocking SAST finding that fails CI and must be cleared. One-line fix:
    ```ts
    const decipher = createDecipheriv(ALGORITHM, this.key, iv, { authTagLength: AUTH_TAG_LENGTH })
    ```
    Pass { authTagLength: AUTH_TAG_LENGTH } to createCipheriv too for symmetry. Prefer the code fix over a // nosemgrep suppression.

  2. Dependency audit (pnpm) — pre-existing critical advisory, unrelated to this PR.
    pnpm audit --audit-level=high fails on GHSA-5xrq-8626-4rwp (vitest <4.1.0, critical) via apps/web's vitest@3.2.4 (catalog ^3.0.0 on main). Not introduced here — it pre-dates the branch. But a pre-existing red check still blocks merge: bump vitest + @vitest/coverage-v8 catalog entries to >=4.1.0 in a separate commit/PR, re-verify the web suite, rebase this PR onto green, then merge.

Handbook Findings

Migration Safety (blocking)handbooks/architecture/migration-safety.mdN/A. Migration20260607130731.ts only runs create table "github_app" (additive, first table) with a drop table if exists down(). No DROP/RENAME/narrowing on existing data. AgDR-0007 documents rollback, zero blast radius, sole-consumer analysis. No violation.

Clean Architecture Layershandbooks/architecture/clean-architecture-layers.md — no violations. HTTP/DI/persistence sit in the right places; SecretCipher/StateSigner are cross-cutting infra; the entity is feature-owned per the api CLAUDE.md vertical-slice convention.

TypeScript Strict Modehandbooks/language/typescript/strict-mode.md — no violations. No bare any in hand-written code; test doubles use as unknown as over runtime-shaped objects (accepted pattern). Generated app/api/*.gen.ts is out of scope.

Suggestions (non-blocking)

  • convert-manifest.service.ts:28let creds is untyped; annotate let creds: GitHubAppCredentials for readability.
  • convert-manifest.controller.tsConvertManifestRequest carries only @ApiProperty, no class-validator decorators, and no global ValidationPipe is wired. The service defensively handles empty code (400) and bad/missing state (400 via verify), so it's adequately guarded today. If a ValidationPipe lands later, add @IsString()/@IsNotEmpty() here.
  • github-manifest.client.ts:51 — GitHub's conversion response is cast as ConversionResponse with no runtime shape check. Low risk (trusted endpoint), but a small zod/guard on data.id/data.pem would fail loudly if GitHub's shape drifts. Optional.

Verdict

CHANGES REQUESTED — code and design are approve-quality; the gate is the red CI. (1) Add { authTagLength: 16 } to the GCM cipher/decipher calls to clear the blocking Semgrep finding. (2) Get the pre-existing critical vitest advisory green (separate bump + rebase). Re-request review once CI is green on the same HEAD.


🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: `727f247878801614907a738921115720e9a19427`

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
apps/api/src/app/github-app/github-app.config.ts (1)

3-9: 💤 Low value

Consider trimming whitespace in addition to trailing slashes.

The required() helper strips trailing slashes but does not trim leading/trailing whitespace. If an operator accidentally includes whitespace in the environment variable (e.g., " https://example.com "), it could cause subtle URL construction issues.

🔧 Proposed enhancement
 function required(name: string): string {
   const value = process.env[name]
   if (!value) {
     throw new Error(`${name} is not set`)
   }
-  return value.replace(/\/+$/, '')
+  return value.trim().replace(/\/+$/, '')
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/app/github-app/github-app.config.ts` around lines 3 - 9, The
helper function required(name: string) currently strips trailing slashes but not
surrounding whitespace; update it so after reading process.env[name] you call
.trim() to remove leading/trailing whitespace and then remove trailing slashes
(e.g., operate on value.trim() before replace) so the returned string has no
surrounding spaces and no trailing slashes; keep the same error throw when value
is missing and preserve return type for required().
apps/api/src/app/github-app/github-manifest.client.ts (1)

37-44: ⚡ Quick win

Consider adding a timeout to the GitHub API request.

The fetch call has no configured timeout, so if GitHub's manifest conversion endpoint is slow or unresponsive, the request could hang indefinitely, tying up resources and degrading user experience.

⏱️ Proposed timeout configuration
   async convertManifest(code: string): Promise<GitHubAppCredentials> {
+    const controller = new AbortController()
+    const timeoutId = setTimeout(() => controller.abort(), 30000) // 30s timeout
+
     const response = await fetch(
       `${GITHUB_API}/app-manifests/${encodeURIComponent(code)}/conversions`,
       {
         method: 'POST',
         headers: { Accept: 'application/vnd.github+json' },
+        signal: controller.signal,
       },
     )
+      .finally(() => clearTimeout(timeoutId))

     if (!response.ok) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/app/github-app/github-manifest.client.ts` around lines 37 - 44,
The convertManifest function’s fetch call lacks a timeout and can hang; add an
AbortController with a configurable timeout (e.g., 10s) and pass
controller.signal into the fetch in convertManifest, start a setTimeout that
calls controller.abort after the timeout, and clear that timer once the fetch
completes; also ensure the existing response/error handling accounts for aborted
requests (propagate a clear timeout error or wrap the abort in a descriptive
error message) so convertManifest fails fast on GitHub endpoint slowness.
apps/api/src/app/github-app/use-cases/get-manifest/tests/get-manifest.e2e.test.ts (1)

26-26: ⚡ Quick win

Strengthen the state assertion to catch empty-token regressions.

typeof === 'string' still passes for ''; asserting non-empty improves this e2e signal with almost no cost.

Proposed test tweak
-    expect(typeof response.body.state).toBe('string')
+    expect(response.body.state).toEqual(expect.any(String))
+    expect(response.body.state.length).toBeGreaterThan(0)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/api/src/app/github-app/use-cases/get-manifest/tests/get-manifest.e2e.test.ts`
at line 26, The test currently only asserts typeof response.body.state is
'string' which permits empty strings; update the assertion in
get-manifest.e2e.test.ts to also assert the state is non-empty (e.g., assert
response.body.state.length > 0 or match a non-empty regex) so
response.body.state is both a string and contains at least one character.
apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.request.ts (1)

4-8: Reconsider DTO runtime validation—code/state are already validated in the service

@ApiProperty only documents types, but ConvertManifestService.execute() already rejects invalid inputs at runtime (BadRequestException when code isn’t a non-empty string or when state fails stateSigner.verify()), with unit + e2e tests covering these failure paths.

Adding class-validator decorators to ConvertManifestRequest alone won’t help here unless this repo also enables ValidationPipe (no ValidationPipe/class-validator usage found repo-wide). If you want additional constraints (e.g., length limits) or consistent DTO-level enforcement, extend the existing service checks or introduce ValidationPipe + class-validator across the API.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.request.ts`
around lines 4 - 8, The DTO ConvertManifestRequest currently only uses
`@ApiProperty` for docs while runtime validation for code and state lives in
ConvertManifestService.execute(); either remove/add no-op class-validator
decorators (to avoid misleading reviewers) or explicitly implement validation
consistently: if you want DTO-level validation, enable ValidationPipe and add
class-validator decorators (e.g., IsString/IsNotEmpty) on
ConvertManifestRequest, otherwise leave only `@ApiProperty` and keep the existing
service checks in ConvertManifestService.execute() (which already throws
BadRequestException and verifies state via stateSigner.verify()); update tests
or documentation accordingly to reflect where validation occurs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/api/src/app/github-app/github-app.config.ts`:
- Around line 3-9: The helper function required(name: string) currently strips
trailing slashes but not surrounding whitespace; update it so after reading
process.env[name] you call .trim() to remove leading/trailing whitespace and
then remove trailing slashes (e.g., operate on value.trim() before replace) so
the returned string has no surrounding spaces and no trailing slashes; keep the
same error throw when value is missing and preserve return type for required().

In `@apps/api/src/app/github-app/github-manifest.client.ts`:
- Around line 37-44: The convertManifest function’s fetch call lacks a timeout
and can hang; add an AbortController with a configurable timeout (e.g., 10s) and
pass controller.signal into the fetch in convertManifest, start a setTimeout
that calls controller.abort after the timeout, and clear that timer once the
fetch completes; also ensure the existing response/error handling accounts for
aborted requests (propagate a clear timeout error or wrap the abort in a
descriptive error message) so convertManifest fails fast on GitHub endpoint
slowness.

In
`@apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.request.ts`:
- Around line 4-8: The DTO ConvertManifestRequest currently only uses
`@ApiProperty` for docs while runtime validation for code and state lives in
ConvertManifestService.execute(); either remove/add no-op class-validator
decorators (to avoid misleading reviewers) or explicitly implement validation
consistently: if you want DTO-level validation, enable ValidationPipe and add
class-validator decorators (e.g., IsString/IsNotEmpty) on
ConvertManifestRequest, otherwise leave only `@ApiProperty` and keep the existing
service checks in ConvertManifestService.execute() (which already throws
BadRequestException and verifies state via stateSigner.verify()); update tests
or documentation accordingly to reflect where validation occurs.

In
`@apps/api/src/app/github-app/use-cases/get-manifest/tests/get-manifest.e2e.test.ts`:
- Line 26: The test currently only asserts typeof response.body.state is
'string' which permits empty strings; update the assertion in
get-manifest.e2e.test.ts to also assert the state is non-empty (e.g., assert
response.body.state.length > 0 or match a non-empty regex) so
response.body.state is both a string and contains at least one character.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ddb3ae0-9bf7-49d3-bdf3-6a12c9723055

📥 Commits

Reviewing files that changed from the base of the PR and between 5745481 and 727f247.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (46)
  • .prettierignore
  • apps/api/.env.test
  • apps/api/eslint.config.mjs
  • apps/api/openapi.json
  • apps/api/package.json
  • apps/api/src/app.module.ts
  • apps/api/src/app/github-app/entities/github-app.entity.ts
  • apps/api/src/app/github-app/github-app-shared.module.ts
  • apps/api/src/app/github-app/github-app.config.ts
  • apps/api/src/app/github-app/github-app.module.ts
  • apps/api/src/app/github-app/github-manifest.client.ts
  • apps/api/src/app/github-app/state-signer.ts
  • apps/api/src/app/github-app/tests/github-app.config.unit.test.ts
  • apps/api/src/app/github-app/tests/github-manifest.client.unit.test.ts
  • apps/api/src/app/github-app/tests/state-signer.unit.test.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.controller.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.module.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.request.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.response.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.service.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.e2e.test.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.service.unit.test.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.controller.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.module.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.response.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.service.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/tests/get-manifest.e2e.test.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/tests/get-manifest.service.unit.test.ts
  • apps/api/src/modules/api/api.module.ts
  • apps/api/src/modules/crypto/crypto.module.ts
  • apps/api/src/modules/crypto/secret-cipher.service.ts
  • apps/api/src/modules/crypto/tests/secret-cipher.service.unit.test.ts
  • apps/api/src/sql/migrations/Migration20260607130731.ts
  • apps/api/src/sql/mikro-orm.config.ts
  • apps/web/app/api/index.ts
  • apps/web/app/api/types.gen.ts
  • apps/web/app/api/zod.gen.ts
  • apps/web/app/composables/__tests__/useGithubProvisioning.spec.ts
  • apps/web/app/composables/useGithubProvisioning.ts
  • apps/web/app/pages/setup/github/__tests__/callback.spec.ts
  • apps/web/app/pages/setup/github/__tests__/index.spec.ts
  • apps/web/app/pages/setup/github/callback.vue
  • apps/web/app/pages/setup/github/index.vue
  • docs/agdr/AgDR-0006-github-app-credential-storage.md
  • docs/agdr/AgDR-0007-migration-github-app-table.md
  • pnpm-workspace.yaml
💤 Files with no reviewable changes (1)
  • apps/api/src/sql/mikro-orm.config.ts

@G0maa

G0maa commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Status: code complete + reviewed; parked on a pre-existing, unrelated CI failure.

  • ci (lint, typecheck, build, api+web tests), ✅ SAST (Semgrep), ✅ Secret scan all green at HEAD.
  • ✅ Rex approved the code; ✅ Hakim (security) found no blocking issues.
  • Dependency audit (pnpm) is red on GHSA-5xrq-8626-4rwp (vitest <4.1.0) — this is pre-existing on main and unrelated to Implement GitHub App provisioning via Manifest flow #58. It is not introduced by this PR (no vitest change here).

Per the "No Red CI Before Merge" rule, this PR will not be merged over that red check. The vitest CVE fix is a coordinated major upgrade (vitest 3→4 + @nuxt/test-utils 3→4 + happy-dom), so it's being handled in a separate PR. Once that lands, this branch will be rebased onto green and re-reviewed before merge.

Comment thread apps/api/src/app/github-app/entities/github-app.entity.ts Outdated
Comment thread apps/api/src/app/github-app/entities/github-app.entity.ts
Comment thread apps/api/src/app/github-app/entities/github-app.entity.ts Outdated
Comment thread apps/api/src/app/github-app/entities/github-app.entity.ts
Comment thread apps/api/src/app/github-app/entities/github-app.entity.ts
Comment thread apps/api/src/app/github-app/entities/github-app.entity.ts
Comment thread apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.controller.ts Outdated
Comment thread apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.service.ts Outdated
Comment thread apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.use-case.ts Outdated
Comment thread apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.service.ts Outdated
Comment thread apps/api/src/app/github-app/github-app-shared.module.ts Outdated
Comment thread apps/api/src/app/github-app/github-app.config.ts Outdated
Comment thread apps/api/src/app/github-app/github-app.config.ts
Comment thread apps/api/src/app/github-app/github-app.module.ts
Comment thread apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.module.ts Outdated
Comment thread apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.module.ts Outdated
Comment thread apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.response.ts Outdated
Comment thread apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.use-case.ts Outdated
Comment thread apps/api/src/app/github-app/github-app.config.ts Outdated
Comment thread apps/api/src/modules/github-client/tests/github-manifest.client.unit.test.ts Outdated
Comment thread apps/api/src/modules/github-client/github-client.module.ts
Comment thread apps/api/src/modules/github-client/github-manifest.client.ts Outdated
…ifest state

- Add ManifestState entity + ManifestStateService (issue/consume via an atomic
  conditional nativeDelete) in a shared ManifestStateModule both use-cases import
- Delete state-signer.ts (stateless HMAC) per the second-review ruling; this
  supersedes the `state` decision in AgDR-0006 (recorded in AgDR-0010)
- get-manifest use-case is now async — it issues a DB-backed token
- Add UNIQUE(github_app_id, slug) + an idempotent persist so re-provisioning
  updates the existing row instead of inserting a duplicate App (CodeRabbit Major)
- Migration20260610100415: create github_app_manifest_state + add the constraints
- Update unit/e2e tests, add ManifestStateService db tests, regenerate openapi.json

Refs #58

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.use-case.ts (1)

36-43: 💤 Low value

Consider adding defensive validation in appName helper.

The appName function doesn't explicitly handle edge cases like empty strings or malformed URLs (e.g., "" or "https://" would produce "marsa-" after all transformations). While the upstream GitHubAppConfig Joi validation likely prevents these cases, adding an explicit guard would make this function more robust and self-documenting.

🛡️ Optional defensive check
 function appName(webUrl: string): string {
+  if (!webUrl || !webUrl.includes('.')) {
+    throw new Error('Invalid webUrl for app name generation')
+  }
   const host = webUrl
     .replace(/^https?:\/\//, '')
     .replace(/[^a-zA-Z0-9]+/g, '-')
     .replace(/^-+|-+$/g, '')
   return `marsa-${host}`.slice(0, 34).replace(/-+$/g, '')
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.use-case.ts`
around lines 36 - 43, appName currently transforms webUrl without guarding
against empty or malformed input (e.g., "" or "https://") which yields invalid
or trailing-only names; update the appName(webUrl: string) helper to defensively
validate webUrl after trimming protocol and stripping non-alphanumerics: ensure
the resulting host segment is non-empty, and if it is either throw a clear Error
or return a safe fallback (e.g., throw new Error("invalid webUrl for appName")
or return a deterministic default name), keeping the final truncation and
dash-trimming logic intact so callers like get-manifest.use-case get a
well-formed GitHub App name.
apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.ts (1)

25-27: Manual input validation present despite class-validator setup.

Lines 25-27 manually validate the code field, which duplicates validation that should be handled by class-validator decorators on ConvertManifestCommand. Past comments noted this redundancy and suggested relying solely on the declarative validation layer (per AgDR-0009).

Additionally, line 38 throws BadGatewayException, but past comments indicated that all thrown exceptions should be documented in the controller's OpenAPI decorators to keep the contract complete.

Also applies to: 36-38

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.ts`
around lines 25 - 27, Remove the manual runtime checks for command.code in the
ConvertManifestUseCase and rely on the existing class-validator decorators on
ConvertManifestCommand to validate inputs; delete the typeof/length guard and
let validation middleware raise BadRequestException. Also keep the
BadGatewayException thrown in the use-case (the error path in the
ConvertManifestUseCase/execute flow) but update the corresponding controller's
OpenAPI decorators to explicitly document that BadGatewayException as a possible
response so the API contract remains complete. Ensure references to
ConvertManifestCommand and the use-case method where the exception is thrown are
updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.ts`:
- Around line 54-73: The current findOne → conditional persistAndFlush flow on
em.fork() for GitHubApp is vulnerable to a TOCTOU race causing unique constraint
errors; update convert-manifest.use-case.ts to wrap the insert path (the branch
that calls em.persistAndFlush(app)) in a try/catch that catches
UniqueConstraintViolationException (import from `@mikro-orm/core`), and on
catching it re-query the existing GitHubApp via em.findOne({ githubAppId }) and
then em.assign(existing, { slug, name, htmlUrl, ownerLogin, clientId,
clientSecretEnc, webhookSecretEnc, privateKeyPemEnc }) followed by em.flush()
(or alternatively retry the upsert once), so concurrent inserts are handled
defensively instead of erroring the request.

In `@docs/agdr/AgDR-0010-migration-manifest-state-db-backed.md`:
- Line 108: Update the "Commits / PRs" line to remove the placeholder
parenthetical; change the text "Commits / PRs: PR `#64` (filled in as the
migration ships)" to simply "Commits / PRs: PR `#64`" (or replace with the commit
SHA if your docs convention requires SHAs) in
AgDR-0010-migration-manifest-state-db-backed.md so the PR reference is no longer
a placeholder.

---

Nitpick comments:
In
`@apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.ts`:
- Around line 25-27: Remove the manual runtime checks for command.code in the
ConvertManifestUseCase and rely on the existing class-validator decorators on
ConvertManifestCommand to validate inputs; delete the typeof/length guard and
let validation middleware raise BadRequestException. Also keep the
BadGatewayException thrown in the use-case (the error path in the
ConvertManifestUseCase/execute flow) but update the corresponding controller's
OpenAPI decorators to explicitly document that BadGatewayException as a possible
response so the API contract remains complete. Ensure references to
ConvertManifestCommand and the use-case method where the exception is thrown are
updated accordingly.

In `@apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.use-case.ts`:
- Around line 36-43: appName currently transforms webUrl without guarding
against empty or malformed input (e.g., "" or "https://") which yields invalid
or trailing-only names; update the appName(webUrl: string) helper to defensively
validate webUrl after trimming protocol and stripping non-alphanumerics: ensure
the resulting host segment is non-empty, and if it is either throw a clear Error
or return a safe fallback (e.g., throw new Error("invalid webUrl for appName")
or return a deterministic default name), keeping the final truncation and
dash-trimming logic intact so callers like get-manifest.use-case get a
well-formed GitHub App name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: de814455-3d10-4565-8c40-5c925306f136

📥 Commits

Reviewing files that changed from the base of the PR and between 4ac711a and 87517fc.

📒 Files selected for processing (17)
  • apps/api/openapi.json
  • apps/api/src/app/github-app/entities/github-app.entity.ts
  • apps/api/src/app/github-app/entities/manifest-state.entity.ts
  • apps/api/src/app/github-app/manifest-state/manifest-state.module.ts
  • apps/api/src/app/github-app/manifest-state/manifest-state.service.ts
  • apps/api/src/app/github-app/manifest-state/tests/manifest-state.service.db.test.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.module.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.e2e.test.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.use-case.unit.test.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.controller.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.module.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.response.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.use-case.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/tests/get-manifest.use-case.unit.test.ts
  • apps/api/src/sql/migrations/Migration20260610100415.ts
  • docs/agdr/AgDR-0010-migration-manifest-state-db-backed.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/api/src/app/github-app/entities/github-app.entity.ts
  • apps/api/openapi.json
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.e2e.test.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.use-case.unit.test.ts

Comment thread docs/agdr/AgDR-0010-migration-manifest-state-db-backed.md Outdated
G0maa and others added 2 commits June 10, 2026 13:54
CLAUDE.md (apps/api) conventions from the PR #64 second review:
- Build *every* entity via a <Entity>Builder (drop the ≈5+ threshold) — add
  ManifestStateBuilder so the new entity complies
- Commands stay class-validator DTOs; add a test-side <Action>CommandBuilder
  (ConvertManifestCommandBuilder) and use it in the convert-manifest tests
- Validate at the DTO boundary, not manually in the use-case
- Document every thrown error on the controller via @Api*Response
- Share a service across use-cases via its own module (providers + exports)
- Place util functions by reach (module-local utils/ vs src/utils)
- Comments explain non-obvious *why*, not *what* — trim github-app.builder.ts
  and manifest-state doc comments accordingly
- Responses take the entity/type (nested object → its own @ApiProperty class)

Refs #58

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ConvertManifestUseCase: the findOne→insert path had a TOCTOU race — two
  concurrent conversions of the same github_app_id could both miss and both
  insert, surfacing an uncaught unique-violation (CodeRabbit Major). Catch
  UniqueConstraintViolationException on insert and re-resolve as an update;
  add a unit test for the lost-race path.
- AgDR-0010: drop the "(filled in as the migration ships)" placeholder.

Refs #58

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.command.builder.ts (1)

4-19: ⚡ Quick win

Builder returns same object reference on repeated build() calls.

The current implementation creates a single ConvertManifestCommand instance (line 5) and returns that same instance from build() (line 18). If the builder instance is reused across multiple build() calls, all callers receive references to the same object:

const builder = new ConvertManifestCommandBuilder().withCode('x')
const cmd1 = builder.build()
const cmd2 = builder.withCode('y').build()
// cmd1.code is now 'y' because cmd1 and cmd2 reference the same object

Consider creating a fresh instance in build() to prevent aliasing bugs:

♻️ Proposed fix to return fresh instance
  build(): ConvertManifestCommand {
-   return this.command
+   const result = new ConvertManifestCommand()
+   result.code = this.command.code
+   result.state = this.command.state
+   return result
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.command.builder.ts`
around lines 4 - 19, ConvertManifestCommandBuilder currently reuses a single
ConvertManifestCommand instance (private readonly command) so repeated build()
calls return the same object; change build() to create and return a fresh
ConvertManifestCommand (copying current code and state from the builder) and
then reset the builder's internal command (or set a new instance) so subsequent
withCode()/withState() and build() produce independent objects; update
ConvertManifestCommandBuilder.build(), and ensure withCode and withState
continue to set values on the builder state (not on a single shared instance).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.command.builder.ts`:
- Around line 4-19: ConvertManifestCommandBuilder currently reuses a single
ConvertManifestCommand instance (private readonly command) so repeated build()
calls return the same object; change build() to create and return a fresh
ConvertManifestCommand (copying current code and state from the builder) and
then reset the builder's internal command (or set a new instance) so subsequent
withCode()/withState() and build() produce independent objects; update
ConvertManifestCommandBuilder.build(), and ensure withCode and withState
continue to set values on the builder state (not on a single shared instance).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f1863607-f46f-4eae-b63c-94d7bb65f6ed

📥 Commits

Reviewing files that changed from the base of the PR and between 87517fc and ffac2d8.

📒 Files selected for processing (10)
  • apps/api/.claude/CLAUDE.md
  • apps/api/src/app/github-app/entities/github-app.builder.ts
  • apps/api/src/app/github-app/entities/manifest-state.builder.ts
  • apps/api/src/app/github-app/entities/manifest-state.entity.ts
  • apps/api/src/app/github-app/manifest-state/manifest-state.service.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.command.builder.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.e2e.test.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.use-case.unit.test.ts
  • docs/agdr/AgDR-0010-migration-manifest-state-db-backed.md
✅ Files skipped from review due to trivial changes (3)
  • apps/api/src/app/github-app/entities/manifest-state.builder.ts
  • apps/api/.claude/CLAUDE.md
  • docs/agdr/AgDR-0010-migration-manifest-state-db-backed.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/api/src/app/github-app/entities/github-app.builder.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.e2e.test.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.use-case.unit.test.ts
  • apps/api/src/app/github-app/manifest-state/manifest-state.service.ts

G0maa and others added 3 commits June 10, 2026 16:52
…rs, responses, utils)

Applies the conventions codified earlier in PR #64 to the code that prompted them:

- Drop the redundant manual code/state type checks in ConvertManifestUseCase —
  class-validator on ConvertManifestCommand already enforces them; only the
  business rule (state token valid + unconsumed) stays. Removed the now-moot
  unit test (the empty-code path is covered by the e2e via ValidationPipe).
- Document the thrown errors on the convert controller: @ApiBadRequestResponse
  (400) + @apiresponse(502), so openapi.json reflects the real response set.
- Responses take their type, not loose fields: ConvertManifestResponse takes the
  GitHubApp entity and derives installUrl; GetManifestResponse.manifest is now a
  proper ManifestDto class (+ HookAttributesDto) instead of an interface typed
  with additionalProperties:true — the web now gets a real schema.
- Move util functions by reach: appName → github-app/utils/app-name.ts
  (feature-local); stripTrailingSlash → src/utils/ (cross-cutting).
- Regenerated openapi.json.

Refs #58

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Move the inline `ConversionResponse` interface out of github-manifest.client.ts
  into github-client.types.ts as `GitHubManifestConversionResponse` (the raw
  snake_case GitHub shape, alongside the normalised `GitHubAppCredentials`).
- Type the client unit-test payload fixtures with it.

Refs #58

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The endpoint path read as "conversions of what?"; rename to convert-manifest
  to match the use-case. operationId (convertGithubAppManifestV1) is unchanged,
  so the generated type names stay stable.
- api: controller path + e2e paths; regenerated openapi.json.
- web: updated the hard-coded path in useGithubProvisioning, regenerated the
  client (app/api/*). This regen also folds in the ManifestDto schema from the
  earlier Group-A commit (api openapi had changed without a web regen) — the
  tightened manifest schema required completing the manifest fixtures in both
  useGithubProvisioning specs.

Refs #58

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/openapi.json (1)

1-267: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Regenerate both OpenAPI spec and web client to resolve CI failure.

Both apps/api/openapi.json and apps/web/app/api/index.ts are generated artifacts that are out of sync with the backend source, causing the CI pipeline to fail. The root cause is that the generation step was not run (or not committed) after the latest backend changes. Run the following command to regenerate both files consistently, then commit the result:

pnpm --filter api generate:openapi && pnpm --filter web generate:api
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/openapi.json` around lines 1 - 267, Generated artifacts for the
OpenAPI spec and web client are out of sync (e.g., operationId getApiInfoV1,
getGithubAppManifestV1 and schemas like ConvertManifestResponse/ManifestDto no
longer match the backend); run the generation commands to rebuild the OpenAPI
spec and the web client (pnpm --filter api generate:openapi && pnpm --filter web
generate:api), verify the regenerated openapi.json includes the expected
operationIds/schemas (e.g., ConvertManifestCommand/ConvertManifestResponse,
ManifestDto), and commit the regenerated artifacts so CI passes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/api/openapi.json`:
- Around line 1-267: Generated artifacts for the OpenAPI spec and web client are
out of sync (e.g., operationId getApiInfoV1, getGithubAppManifestV1 and schemas
like ConvertManifestResponse/ManifestDto no longer match the backend); run the
generation commands to rebuild the OpenAPI spec and the web client (pnpm
--filter api generate:openapi && pnpm --filter web generate:api), verify the
regenerated openapi.json includes the expected operationIds/schemas (e.g.,
ConvertManifestCommand/ConvertManifestResponse, ManifestDto), and commit the
regenerated artifacts so CI passes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d2f4059-4bf5-43b2-9290-2fdc53ca4f0d

📥 Commits

Reviewing files that changed from the base of the PR and between ffac2d8 and 97a420e.

📒 Files selected for processing (21)
  • apps/api/openapi.json
  • apps/api/src/app/github-app/github-app.config.ts
  • apps/api/src/app/github-app/github-app.types.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.controller.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.response.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.e2e.test.ts
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.use-case.unit.test.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.response.ts
  • apps/api/src/app/github-app/use-cases/get-manifest/get-manifest.use-case.ts
  • apps/api/src/app/github-app/utils/app-name.ts
  • apps/api/src/modules/github-client/github-client.types.ts
  • apps/api/src/modules/github-client/github-manifest.client.ts
  • apps/api/src/modules/github-client/tests/github-manifest.client.unit.test.ts
  • apps/api/src/utils/strip-trailing-slash.ts
  • apps/web/app/api/index.ts
  • apps/web/app/api/types.gen.ts
  • apps/web/app/api/zod.gen.ts
  • apps/web/app/composables/__tests__/useGithubProvisioning.nuxt.spec.ts
  • apps/web/app/composables/__tests__/useGithubProvisioning.spec.ts
  • apps/web/app/composables/useGithubProvisioning.ts
💤 Files with no reviewable changes (1)
  • apps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.use-case.unit.test.ts
✅ Files skipped from review due to trivial changes (3)
  • apps/api/src/utils/strip-trailing-slash.ts
  • apps/api/src/app/github-app/utils/app-name.ts
  • apps/web/app/api/types.gen.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/web/app/composables/tests/useGithubProvisioning.spec.ts
  • apps/api/src/modules/github-client/github-manifest.client.ts
  • apps/api/src/app/github-app/github-app.config.ts
  • apps/web/app/api/zod.gen.ts
  • apps/web/app/composables/tests/useGithubProvisioning.nuxt.spec.ts
  • apps/web/app/composables/useGithubProvisioning.ts
  • apps/api/src/modules/github-client/tests/github-manifest.client.unit.test.ts

G0maa and others added 3 commits June 10, 2026 18:21
- Move GitHubApp persistence (idempotent upsert + unique-violation race recovery)
  out of ConvertManifestUseCase into a co-located ConvertManifestRepository that
  wraps a forked EM. The use-case now injects the repository, not the raw EM —
  trivially mockable, persistence concerns behind a seam (AgDR-0011).
- Repository is use-case-scoped (vertical-slice), not a feature-wide aggregate
  repo; deliberately a plain injectable rather than MikroORM @InjectRepository
  (which binds to the root EM and would negate the request-isolated fork).
- Move the persist/idempotent/race tests to convert-manifest.repository.unit.test;
  the use-case test now stubs the repository.
- Codify: a use-case's DB access goes through its own <use-case>.repository.ts —
  CLAUDE.md bullet + handbook use-case-repository.md.

Refs #58

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reflow the option tables in AgDR-0009 and AgDR-0010 to prettier's
  column widths so `format:check` passes in CI (was the only red in
  the `ci` job; tracked docs, content unchanged)

Refs #58
- A freshly-published advisory (GHSA-w7jw-789q-3m8p) flagged
  shell-quote@1.8.3, pulled in transitively via
  nuxt -> @nuxt/devtools -> launch-editor (dev-only). The dependency
  audit CI job blocks on critical/high, so the pre-existing tree
  turned the gate red.
- Add a pnpm override pinning shell-quote to the patched >=1.8.4 line;
  pnpm audit now reports only low/moderate, clearing the gate.

Refs #58
@G0maa G0maa merged commit 98b7698 into main Jun 11, 2026
5 checks passed
@G0maa G0maa deleted the feature/GH-58-github-app-provisioning-manifest-flow branch June 11, 2026 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant