feat(#58): provision per-install GitHub App via Manifest flow#64
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughImplements 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. ChangesGitHub App Manifest Provisioning
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
G0maa
left a comment
There was a problem hiding this comment.
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.testkey is a verified dummy (see below) - Injection Prevention: Pass — MikroORM parameterised writes;
codeURL-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
stateis 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 viasetAuthTag()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
timingSafeEqualbehind 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.testkey is safe.APP_SECRETS_ENCRYPTION_KEY=Kioq…Kio=decodes to exactly 32 bytes, all0x2A(*) — a verified dummy fixture, not a real secret. No concern. Keep the inline(NOT real secrets)comment.- Only ciphertext persisted.
GitHubAppentity storesclientSecretEnc/webhookSecretEnc/privateKeyPemEncastext;client_id/app id/slug/name/htmlUrl/ownerare non-secret and stored plaintext, consistent with AgDR-0006. Confirmed no plaintext secret column. codehandled safely.encodeURIComponent(code)into the GitHub path prevents path injection; thecodeis 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
left a comment
There was a problem hiding this comment.
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:
-
SAST (Semgrep) — blocking finding introduced by THIS PR.
apps/api/src/modules/crypto/secret-cipher.service.ts:57— rulejavascript.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
setAuthTagrejects 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 }tocreateCipherivtoo for symmetry. Prefer the code fix over a// nosemgrepsuppression. -
Dependency audit (pnpm) — pre-existing critical advisory, unrelated to this PR.
pnpm audit --audit-level=highfails on GHSA-5xrq-8626-4rwp (vitest <4.1.0, critical) viaapps/web'svitest@3.2.4(catalog^3.0.0onmain). Not introduced here — it pre-dates the branch. But a pre-existing red check still blocks merge: bumpvitest+@vitest/coverage-v8catalog entries to>=4.1.0in 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.md — N/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 Layers — handbooks/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 Mode — handbooks/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:28—let credsis untyped; annotatelet creds: GitHubAppCredentialsfor readability.convert-manifest.controller.ts—ConvertManifestRequestcarries only@ApiProperty, no class-validator decorators, and no globalValidationPipeis wired. The service defensively handles emptycode(400) and bad/missingstate(400 viaverify), so it's adequately guarded today. If aValidationPipelands later, add@IsString()/@IsNotEmpty()here.github-manifest.client.ts:51— GitHub's conversion response is castas ConversionResponsewith no runtime shape check. Low risk (trusted endpoint), but a small zod/guard ondata.id/data.pemwould 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`
There was a problem hiding this comment.
🧹 Nitpick comments (4)
apps/api/src/app/github-app/github-app.config.ts (1)
3-9: 💤 Low valueConsider 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 winConsider 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 winStrengthen the
stateassertion 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/stateare already validated in the service
@ApiPropertyonly documents types, butConvertManifestService.execute()already rejects invalid inputs at runtime (BadRequestExceptionwhencodeisn’t a non-empty string or whenstatefailsstateSigner.verify()), with unit + e2e tests covering these failure paths.Adding
class-validatordecorators toConvertManifestRequestalone won’t help here unless this repo also enablesValidationPipe(noValidationPipe/class-validatorusage found repo-wide). If you want additional constraints (e.g., length limits) or consistent DTO-level enforcement, extend the existing service checks or introduceValidationPipe+class-validatoracross 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (46)
.prettierignoreapps/api/.env.testapps/api/eslint.config.mjsapps/api/openapi.jsonapps/api/package.jsonapps/api/src/app.module.tsapps/api/src/app/github-app/entities/github-app.entity.tsapps/api/src/app/github-app/github-app-shared.module.tsapps/api/src/app/github-app/github-app.config.tsapps/api/src/app/github-app/github-app.module.tsapps/api/src/app/github-app/github-manifest.client.tsapps/api/src/app/github-app/state-signer.tsapps/api/src/app/github-app/tests/github-app.config.unit.test.tsapps/api/src/app/github-app/tests/github-manifest.client.unit.test.tsapps/api/src/app/github-app/tests/state-signer.unit.test.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.controller.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.module.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.request.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.response.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.service.tsapps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.e2e.test.tsapps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.service.unit.test.tsapps/api/src/app/github-app/use-cases/get-manifest/get-manifest.controller.tsapps/api/src/app/github-app/use-cases/get-manifest/get-manifest.module.tsapps/api/src/app/github-app/use-cases/get-manifest/get-manifest.response.tsapps/api/src/app/github-app/use-cases/get-manifest/get-manifest.service.tsapps/api/src/app/github-app/use-cases/get-manifest/tests/get-manifest.e2e.test.tsapps/api/src/app/github-app/use-cases/get-manifest/tests/get-manifest.service.unit.test.tsapps/api/src/modules/api/api.module.tsapps/api/src/modules/crypto/crypto.module.tsapps/api/src/modules/crypto/secret-cipher.service.tsapps/api/src/modules/crypto/tests/secret-cipher.service.unit.test.tsapps/api/src/sql/migrations/Migration20260607130731.tsapps/api/src/sql/mikro-orm.config.tsapps/web/app/api/index.tsapps/web/app/api/types.gen.tsapps/web/app/api/zod.gen.tsapps/web/app/composables/__tests__/useGithubProvisioning.spec.tsapps/web/app/composables/useGithubProvisioning.tsapps/web/app/pages/setup/github/__tests__/callback.spec.tsapps/web/app/pages/setup/github/__tests__/index.spec.tsapps/web/app/pages/setup/github/callback.vueapps/web/app/pages/setup/github/index.vuedocs/agdr/AgDR-0006-github-app-credential-storage.mddocs/agdr/AgDR-0007-migration-github-app-table.mdpnpm-workspace.yaml
💤 Files with no reviewable changes (1)
- apps/api/src/sql/mikro-orm.config.ts
|
Status: code complete + reviewed; parked on a pre-existing, unrelated CI failure.
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 + |
…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>
There was a problem hiding this comment.
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 valueConsider adding defensive validation in
appNamehelper.The
appNamefunction doesn't explicitly handle edge cases like empty strings or malformed URLs (e.g.,""or"https://"would produce"marsa-"after all transformations). While the upstreamGitHubAppConfigJoi 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
codefield, which duplicates validation that should be handled byclass-validatordecorators onConvertManifestCommand. 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
📒 Files selected for processing (17)
apps/api/openapi.jsonapps/api/src/app/github-app/entities/github-app.entity.tsapps/api/src/app/github-app/entities/manifest-state.entity.tsapps/api/src/app/github-app/manifest-state/manifest-state.module.tsapps/api/src/app/github-app/manifest-state/manifest-state.service.tsapps/api/src/app/github-app/manifest-state/tests/manifest-state.service.db.test.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.module.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.tsapps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.e2e.test.tsapps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.use-case.unit.test.tsapps/api/src/app/github-app/use-cases/get-manifest/get-manifest.controller.tsapps/api/src/app/github-app/use-cases/get-manifest/get-manifest.module.tsapps/api/src/app/github-app/use-cases/get-manifest/get-manifest.response.tsapps/api/src/app/github-app/use-cases/get-manifest/get-manifest.use-case.tsapps/api/src/app/github-app/use-cases/get-manifest/tests/get-manifest.use-case.unit.test.tsapps/api/src/sql/migrations/Migration20260610100415.tsdocs/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
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>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.command.builder.ts (1)
4-19: ⚡ Quick winBuilder returns same object reference on repeated
build()calls.The current implementation creates a single
ConvertManifestCommandinstance (line 5) and returns that same instance frombuild()(line 18). If the builder instance is reused across multiplebuild()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 objectConsider 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
📒 Files selected for processing (10)
apps/api/.claude/CLAUDE.mdapps/api/src/app/github-app/entities/github-app.builder.tsapps/api/src/app/github-app/entities/manifest-state.builder.tsapps/api/src/app/github-app/entities/manifest-state.entity.tsapps/api/src/app/github-app/manifest-state/manifest-state.service.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.command.builder.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.tsapps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.e2e.test.tsapps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.use-case.unit.test.tsdocs/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
…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>
There was a problem hiding this comment.
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 winRegenerate both OpenAPI spec and web client to resolve CI failure.
Both
apps/api/openapi.jsonandapps/web/app/api/index.tsare 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
📒 Files selected for processing (21)
apps/api/openapi.jsonapps/api/src/app/github-app/github-app.config.tsapps/api/src/app/github-app/github-app.types.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.controller.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.response.tsapps/api/src/app/github-app/use-cases/convert-manifest/convert-manifest.use-case.tsapps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.e2e.test.tsapps/api/src/app/github-app/use-cases/convert-manifest/tests/convert-manifest.use-case.unit.test.tsapps/api/src/app/github-app/use-cases/get-manifest/get-manifest.response.tsapps/api/src/app/github-app/use-cases/get-manifest/get-manifest.use-case.tsapps/api/src/app/github-app/utils/app-name.tsapps/api/src/modules/github-client/github-client.types.tsapps/api/src/modules/github-client/github-manifest.client.tsapps/api/src/modules/github-client/tests/github-manifest.client.unit.test.tsapps/api/src/utils/strip-trailing-slash.tsapps/web/app/api/index.tsapps/web/app/api/types.gen.tsapps/web/app/api/zod.gen.tsapps/web/app/composables/__tests__/useGithubProvisioning.nuxt.spec.tsapps/web/app/composables/__tests__/useGithubProvisioning.spec.tsapps/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
- 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
Summary
AgDR-0005-github-app-integration-model). Newgithub-appAPI slice exposesGET /api/v1/github-app/manifest(builds the manifest + an HMAC-signed CSRFstate) andPOST /api/v1/github-app/conversions(verifiesstate, exchanges GitHub's one-timecode, then encrypts + persists the returned credentials).SecretCipher(src/modules/crypto/) encryptsclient_secret,webhook_secret, and the private-key PEM with AES-256-GCM (Node built-in, zero new deps; key fromAPP_SECRETS_ENCRYPTION_KEY). Stored as base64iv‖authTag‖ciphertext. Reused by GitHub Auth #23 to mint installation tokens. Rationale + alternatives inAgDR-0006-github-app-credential-storage.github_apptable (encrypted*_enccolumns), removes MikroORM's empty-schema flag, and ships the generated migration. Schema change tracked on this ticket via themigrationlabel +AgDR-0007-migration-github-app-table(rollback =DROP TABLE, zero blast radius)./setup/githubrenders the auto-submitting manifest form to GitHub and/setup/github/callbackcompletes the conversion and surfaces the install link. Responses are Zod-validated at the boundary per the web↔api contract.@mikro-orm/cli— the repo'smigration:*scripts referenced themikro-ormbin 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 choicesAgDR-0007-migration-github-app-table— the schema migration (rollback, blast radius, observability)Testing
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.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).pnpm format:check && pnpm lint && pnpm build— all green;openapi.jsonregenerated and committed (CI drift-checked); web client regenerated from it.Out of scope (follow-ups)
APP_SECRETS_ENCRYPTION_KEY,MARSA_WEB_URL,MARSA_API_PUBLIC_URL→ marsa-charts follow-up.Refs #58
Glossary
codethat's exchanged for the App's credentials.POST https://api.github.com/app-manifests/{code}/conversions— exchanges the one-timecodefor the Appid,client_id,client_secret,webhook_secret, and private-keypem.statebase64url(payload).hmac, payload = nonce + expiry) that travels to GitHub and back; verified for signature + freshness on the callback.*_enccolumnSummary by CodeRabbit
New Features
Security / Data
Documentation
Tests & Chores