Skip to content

feat(calendar): add encrypted self-hosted credential store - #203

Merged
seonghobae merged 5 commits into
mainfrom
feat/calendar-encrypted-secret-store-v1
Aug 12, 2026
Merged

feat(calendar): add encrypted self-hosted credential store#203
seonghobae merged 5 commits into
mainfrom
feat/calendar-encrypted-secret-store-v1

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Scope

Advance #129 with one bounded calendar-owned credential-storage slice. This draft starts with a test-first contract for a concrete self-hostable encrypted file secret store that implements the existing calendar create/materialization credential ports without persisting plaintext tokens in PostgreSQL.

Required properties

  • AES-256-GCM encrypted-at-rest payloads with a canonical 256-bit bootstrap key
  • opaque UUIDv4-backed lifeos-calendar-secret:// handles
  • handle-bound authenticated encryption so ciphertext cannot be swapped between records
  • bounded, fail-closed reads and fixed credential-free errors
  • owner-only secret-file permissions
  • idempotent deletion and path-traversal-resistant handle validation
  • no runtime fallback to plaintext storage

This intentionally does not yet claim end-to-end OAuth/KMS/runtime composition or retire the deployment-wide Google token. Those remain in #129.

Test-first state

Head 7d1974184df9cdedebe88b0f898acc2e7a0ce382 is the intentional RED contract: it imports the not-yet-implemented production adapter and specifies encryption, tamper/wrong-key failure, deletion, path validation, and environment-key behavior. Production implementation follows only after the RED boundary is observed.

Refs #129, #21.

Summary by CodeRabbit

  • 새 기능
    • 캘린더 자격 증명을 AES-256-GCM으로 암호화해 파일에 안전하게 저장하고 조회할 수 있습니다.
    • 보안 핸들을 사용한 자격 증명 저장, 조회 및 삭제를 지원합니다.
    • 잘못된 핸들, 변조된 데이터, 잘못된 암호화 키를 검증하고 일관된 오류로 처리합니다.
    • 환경 설정을 통해 암호화 저장소를 간편하게 생성할 수 있습니다.
    • 파일 경로 탐색을 방지하고, 이미 삭제된 자격 증명도 안전하게 처리합니다.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@seonghobae, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e61c8fe5-2788-411a-b412-baafc2495118

📥 Commits

Reviewing files that changed from the base of the PR and between 63256ff and d84cd3c.

📒 Files selected for processing (1)
  • apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts
📝 Walkthrough

Walkthrough

AES-256-GCM 기반 파일 시크릿 저장소를 추가했습니다. 자격 증명은 opaque handle에 바인딩된 암호문으로 저장됩니다. 조회·삭제·입력 검증과 환경 변수 기반 생성 기능을 구현했습니다. 관련 통합 테스트도 추가했습니다.

Changes

암호화 파일 시크릿 저장소

Layer / File(s) Summary
저장소 계약 및 입력 검증
apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts
암호화 payload와 envelope를 정의했습니다. 입력, 키, handle, 파일 경로를 검증했습니다. 공통 오류 변환과 0700 디렉터리 초기화를 추가했습니다.
암호화 시크릿 수명 주기
apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts
writeSecret은 AES-256-GCM으로 자격 증명을 암호화하고 0600 파일로 저장합니다. readSecret은 파일과 payload를 검증한 뒤 복호화합니다. deleteSecret은 멱등적으로 파일을 삭제합니다.
환경 팩토리 및 통합 검증
apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts, apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts
환경 변수에서 저장소를 생성하는 팩토리를 추가했습니다. 평문 저장 방지, handle 바인딩, 변조·오류 처리, 경로 탐색 방지, 삭제, 키 검증을 테스트했습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CalendarEncryptedFileSecretStore
  participant FileSystem
  participant AES256GCM
  Caller->>CalendarEncryptedFileSecretStore: writeSecret(credential)
  CalendarEncryptedFileSecretStore->>AES256GCM: encrypt(payload, handle)
  AES256GCM-->>CalendarEncryptedFileSecretStore: ciphertext
  CalendarEncryptedFileSecretStore->>FileSystem: create encrypted file
  Caller->>CalendarEncryptedFileSecretStore: readSecret(handle)
  CalendarEncryptedFileSecretStore->>FileSystem: read and validate file
  CalendarEncryptedFileSecretStore->>AES256GCM: decrypt(ciphertext, handle)
  AES256GCM-->>CalendarEncryptedFileSecretStore: payload
  CalendarEncryptedFileSecretStore-->>Caller: secret
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 캘린더용 암호화 자체 호스팅 자격 증명 저장소 추가라는 주요 변경 사항을 정확하고 간결하게 설명합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/calendar-encrypted-secret-store-v1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@seonghobae
seonghobae marked this pull request as ready for review August 12, 2026 13:57

@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 (5)
apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts (4)

16-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

UUID 패턴의 i 플래그는 실제로 동작하지 않습니다.

parseHandlerequireUuidV4가 소문자로 변환한 결과와 원본 문자열을 엄격히 비교합니다. 따라서 대문자 UUID 핸들은 항상 거부됩니다. i 플래그는 핸들 경로에서 의미가 없습니다.

핸들을 정규 소문자 형식만 허용하는 의도라면, 패턴에서 i를 제거하여 계약을 명시하십시오. requireUuidV4가 다른 곳에서 대소문자 무관 입력을 허용해야 한다면, 현재 코드를 유지하고 주석으로 이유를 기록하십시오.

♻️ 제안 변경
-const UUID_V4_PATTERN =
-  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
+/** Canonical lowercase UUIDv4 form used for opaque handle identifiers. */
+const UUID_V4_PATTERN =
+  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;

Also applies to: 146-148

🤖 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/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts`
around lines 16 - 17, Remove the unused case-insensitive `i` flag from
`UUID_V4_PATTERN` so UUID handles explicitly require lowercase input, matching
the strict comparison performed by `parseHandle` after `requireUuidV4`
normalization. Keep the existing UUID validation behavior otherwise unchanged.

311-323: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

isSymbolicLink() 검사는 중복입니다.

lstat의 결과에서 isFile()이 참이면 isSymbolicLink()는 항상 거짓입니다. 두 검사는 상호 배타적입니다. 심볼릭 링크는 isFile() 검사만으로 이미 차단됩니다.

의도를 문서화하려면 조건을 제거하고 주석으로 남기십시오. dev/ino 재검증 로직은 그대로 유지하십시오.

♻️ 제안 변경
       const before = await lstat(path);
+      // lstat + isFile() rejects symlinks; open() below is re-verified by dev/ino.
       if (
         !before.isFile() ||
-        before.isSymbolicLink() ||
         before.size <= 0 ||
         before.size > MAXIMUM_ENVELOPE_BYTES
       ) {
🤖 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/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts`
around lines 311 - 323, Remove the redundant before.isSymbolicLink() check from
readSecret’s file validation condition, since lstat’s before.isFile() check
already rejects symbolic links. Preserve the existing size checks and dev/ino
revalidation logic; add a comment only if needed to document that symbolic links
are intentionally rejected.

400-413: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

팩토리에서 requireDirectory 호출이 중복됩니다.

생성자(Line 234)가 이미 requireDirectory를 실행합니다. 팩토리의 추가 호출은 동일한 검증을 반복합니다. 동작은 같으므로 결함은 아닙니다.

키 인자와 대칭을 맞추려면 두 값을 모두 원시 환경 값으로 전달하고 검증은 생성자에 위임하십시오. unavailable()never를 반환하므로 현재 삼항 표현식은 유효합니다.

♻️ 제안 변경
   try {
     return new CalendarEncryptedFileSecretStore(
-      requireDirectory(environment.CALENDAR_SECRET_STORE_DIRECTORY),
+      environment.CALENDAR_SECRET_STORE_DIRECTORY as string,
       typeof environment.CALENDAR_SECRET_STORE_KEY === 'string'
         ? environment.CALENDAR_SECRET_STORE_KEY
         : unavailable(),
     );
🤖 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/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts`
around lines 400 - 413, Remove the redundant requireDirectory call from
createCalendarEncryptedFileSecretStoreFromEnvironment and pass
environment.CALENDAR_SECRET_STORE_DIRECTORY directly to
CalendarEncryptedFileSecretStore, keeping the existing key expression and catch
behavior unchanged so validation remains delegated to the constructor.

238-245: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

비공개 헬퍼에 짧은 docstring을 추가하십시오.

ensureDirectorypathForId는 저장소의 파일 배치와 권한 계약을 결정합니다. 새 기여자가 구현을 읽지 않고 계약을 이해하도록 설명을 추가하십시오. 특히 mkdir 뒤의 명시적 chmod가 기존 디렉터리 권한을 강제하는 이유를 기록하십시오.

♻️ 제안 변경
+  /**
+   * Creates the store root if absent and enforces owner-only permissions on
+   * pre-existing directories, because `mkdir` mode applies to creation only.
+   */
   private async ensureDirectory(): Promise<void> {
     await mkdir(this.directory, { recursive: true, mode: 0o700 });
     await chmod(this.directory, 0o700);
   }
 
+  /** Maps one validated UUIDv4 identifier to its envelope file path. */
   private pathForId(id: string): string {

코딩 가이드라인에 따라: "Production declarations must include explanatory docstrings sufficient for a new contributor to understand the contract without reconstructing the implementation."

🤖 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/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts`
around lines 238 - 245, 파일 배치와 권한 계약을 설명하는 짧은 docstring을 private 헬퍼
ensureDirectory와 pathForId에 추가하십시오. ensureDirectory 문서에는 디렉터리를 재귀적으로 생성하고 0o700
권한을 보장하며, 기존 디렉터리에도 명시적으로 chmod를 적용하는 이유를 기록하십시오. pathForId 문서에는 ID가 디렉터리 아래의
JSON 파일 경로로 변환된다는 계약을 설명하십시오.

Source: Coding guidelines

apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts (1)

152-174: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

커버리지 게이트를 충족하려면 분기 테스트를 추가하십시오.

현재 테스트는 정상 경로와 일부 실패 경로만 다룹니다. 다음 분기는 검증되지 않습니다.

  • writeSecretEEXIST 재시도 경로와 MAXIMUM_WRITE_ATTEMPTS 소진 경로.
  • readSecretMAXIMUM_ENVELOPE_BYTES 초과 파일 거부.
  • readSecretdev/ino 불일치 거부.
  • requireEnvelopeschemaVersion/algorithm 불일치 거부와 requirePayload의 필드 거부.
  • 팩토리에서 CALENDAR_SECRET_STORE_DIRECTORY 누락 및 CALENDAR_SECRET_STORE_KEY 누락.

패키지가 커버리지 게이트를 적용하면 위 분기는 통과에 필요합니다. 필요하면 해당 테스트를 생성해 드릴 수 있습니다.

코딩 가이드라인에 따라: "Packages that enforce coverage gates must retain 100% statement, branch, function, and line coverage."

🤖 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/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts`
around lines 152 - 174, Expand the tests around
createCalendarEncryptedFileSecretStoreFromEnvironment and the store’s
writeSecret/readSecret flows to cover all uncovered branches: EEXIST retries
through MAXIMUM_WRITE_ATTEMPTS exhaustion, oversized envelopes, dev/ino
mismatches, invalid schemaVersion or algorithm in requireEnvelope, rejected
payload fields in requirePayload, and missing CALENDAR_SECRET_STORE_DIRECTORY or
CALENDAR_SECRET_STORE_KEY. Assert each branch’s expected error or rejection
while preserving the existing success-path coverage.

Source: Coding guidelines

🤖 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/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts`:
- Around line 131-150: Update the test “deletes idempotently and rejects
malformed handles without path traversal” to also call deleteSecret with the
malformed traversal handle and assert it rejects with
CalendarEncryptedFileSecretStoreError. Keep the existing readSecret assertion
and idempotent deletion checks unchanged.
- Around line 99-108: Update the tampering setup in the encrypted payload test
around store.readSecret to decode payload.ciphertext, flip a bit in the decoded
bytes, re-encode the modified bytes as canonical base64, and assign that value
back before writing the file. Preserve the existing rejection assertions while
ensuring the mutation always changes the ciphertext and reaches GCM
authentication failure rather than canonical-base64 validation.

---

Nitpick comments:
In
`@apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts`:
- Around line 152-174: Expand the tests around
createCalendarEncryptedFileSecretStoreFromEnvironment and the store’s
writeSecret/readSecret flows to cover all uncovered branches: EEXIST retries
through MAXIMUM_WRITE_ATTEMPTS exhaustion, oversized envelopes, dev/ino
mismatches, invalid schemaVersion or algorithm in requireEnvelope, rejected
payload fields in requirePayload, and missing CALENDAR_SECRET_STORE_DIRECTORY or
CALENDAR_SECRET_STORE_KEY. Assert each branch’s expected error or rejection
while preserving the existing success-path coverage.

In
`@apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts`:
- Around line 16-17: Remove the unused case-insensitive `i` flag from
`UUID_V4_PATTERN` so UUID handles explicitly require lowercase input, matching
the strict comparison performed by `parseHandle` after `requireUuidV4`
normalization. Keep the existing UUID validation behavior otherwise unchanged.
- Around line 311-323: Remove the redundant before.isSymbolicLink() check from
readSecret’s file validation condition, since lstat’s before.isFile() check
already rejects symbolic links. Preserve the existing size checks and dev/ino
revalidation logic; add a comment only if needed to document that symbolic links
are intentionally rejected.
- Around line 400-413: Remove the redundant requireDirectory call from
createCalendarEncryptedFileSecretStoreFromEnvironment and pass
environment.CALENDAR_SECRET_STORE_DIRECTORY directly to
CalendarEncryptedFileSecretStore, keeping the existing key expression and catch
behavior unchanged so validation remains delegated to the constructor.
- Around line 238-245: 파일 배치와 권한 계약을 설명하는 짧은 docstring을 private 헬퍼
ensureDirectory와 pathForId에 추가하십시오. ensureDirectory 문서에는 디렉터리를 재귀적으로 생성하고 0o700
권한을 보장하며, 기존 디렉터리에도 명시적으로 chmod를 적용하는 이유를 기록하십시오. pathForId 문서에는 ID가 디렉터리 아래의
JSON 파일 경로로 변환된다는 계약을 설명하십시오.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d815ef4e-c5ee-482d-9377-6ba5eb8c76d6

📥 Commits

Reviewing files that changed from the base of the PR and between f1b2f3c and 63256ff.

📒 Files selected for processing (2)
  • apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts
  • apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts

@seonghobae
seonghobae merged commit c3f95d0 into main Aug 12, 2026
38 checks passed
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