feat(auth): OAuth account linking for Twitch and YouTube integrations (#69) #70

Merged
DockeGumi merged 9 commits from issue/69-oauth-account-linking into main 2026-06-29 22:52:13 -05:00
DockeGumi commented 2026-06-07 22:34:58 -05:00 (Migrated from github.com)

Summary

Implements OAuth account linking for Twitch and YouTube platform integrations.

Changes

  • LinkedAccount model — stores encrypted OAuth tokens with workspace-scoped queries
  • Encryption utils — AES-256-GCM token encryption with workspace-specific keys
  • OAuth providers — Twitch + Google OAuth2 flows via NextAuth.js
  • API endpoints — /api/channels/[channelSlug]/integrations/[provider] for connect/disconnect/status
  • UI — Integrations page with account cards, connect/disconnect buttons, platform icons

Security

  • Tokens encrypted at rest (AES-256-GCM)
  • Workspace-scoped queries prevent cross-workspace access
  • PKCE + state param for OAuth flows

Testing

  • Unit tests for encryption utils
  • OAuth callback route tests
  • Integration card UI tests

Checklist

  • Database migration added
  • TypeScript types updated
  • Tests added/updated
  • Documentation updated (if applicable)
  • No secrets in code

Closes #69

## Summary Implements OAuth account linking for Twitch and YouTube platform integrations. ## Changes - **LinkedAccount model** — stores encrypted OAuth tokens with workspace-scoped queries - **Encryption utils** — AES-256-GCM token encryption with workspace-specific keys - **OAuth providers** — Twitch + Google OAuth2 flows via NextAuth.js - **API endpoints** — `/api/channels/[channelSlug]/integrations/[provider]` for connect/disconnect/status - **UI** — Integrations page with account cards, connect/disconnect buttons, platform icons ## Security - Tokens encrypted at rest (AES-256-GCM) - Workspace-scoped queries prevent cross-workspace access - PKCE + state param for OAuth flows ## Testing - Unit tests for encryption utils - OAuth callback route tests - Integration card UI tests ## Checklist - [x] Database migration added - [x] TypeScript types updated - [x] Tests added/updated - [x] Documentation updated (if applicable) - [x] No secrets in code ## Related Closes #69
github-advanced-security[bot] (Migrated from github.com) reviewed 2026-06-07 22:35:37 -05:00
github-advanced-security[bot] (Migrated from github.com) commented 2026-06-07 22:35:37 -05:00

Semgrep OSS / Semgrep Finding: javascript.node-crypto.security.gcm-no-tag-length.gcm-no-tag-length

The call to 'createDecipheriv' with the Galois Counter Mode (GCM) mode of operation is missing an expected authentication tag length. If the expected authentication tag length is not specified or otherwise checked, the application might be tricked into verifying a shorter-than-expected authentication tag. This can be abused by an attacker to spoof ciphertexts or recover the implicit authentication key of GCM, allowing arbitrary forgeries.

Show more details

## Semgrep OSS / Semgrep Finding: javascript.node-crypto.security.gcm-no-tag-length.gcm-no-tag-length The call to 'createDecipheriv' with the Galois Counter Mode (GCM) mode of operation is missing an expected authentication tag length. If the expected authentication tag length is not specified or otherwise checked, the application might be tricked into verifying a shorter-than-expected authentication tag. This can be abused by an attacker to spoof ciphertexts or recover the implicit authentication key of GCM, allowing arbitrary forgeries. [Show more details](https://github.com/Gitchegumi/multi-stream-alerts/security/code-scanning/42)
Gitchegumi (Migrated from github.com) requested changes 2026-06-07 23:03:45 -05:00
Gitchegumi (Migrated from github.com) left a comment

Thanks for putting this together. I’m requesting changes before approval because the current OAuth flow has two blockers that will make account linking unreliable and potentially bind accounts to the wrong app user.

  1. apps/web/src/lib/auth.ts overrides the default Twitch and Google identity scopes. Twitch is configured with scope: '', and Google is configured with only the YouTube readonly scope. NextAuth’s provider defaults include the identity scopes needed for profile.email/profile.name (openid email profile for Google and openid user:read:email for Twitch). Since the sign-in callback rejects when profile.email is missing, these connect flows can fail before a linked account is saved. Google should preserve openid email profile while adding the YouTube scope, and Twitch should preserve the default identity/email scope.

  2. The linking callback looks up prisma.user.findUnique({ where: { email } }) using the OAuth provider profile email, rather than linking to the currently authenticated app user who clicked Connect. For integrations, the Twitch/YouTube account email may legitimately differ from the app login email, so normal linking can fail. Worse, if that OAuth email belongs to another app user, the callback links the integration to that other user and the JWT callback can hydrate the session as that user after the OAuth round trip. The flow needs an authenticated-user binding, for example via a signed state/cookie or a dedicated linking endpoint that records the current session user before starting OAuth.

The crypto helper appears to have been updated to specify authTagLength, so I’m not treating the existing code-scanning thread as a blocker here. Once the scopes and current-user binding are fixed, this should be much closer to approvable.

Thanks for putting this together. I’m requesting changes before approval because the current OAuth flow has two blockers that will make account linking unreliable and potentially bind accounts to the wrong app user. 1. `apps/web/src/lib/auth.ts` overrides the default Twitch and Google identity scopes. Twitch is configured with `scope: ''`, and Google is configured with only the YouTube readonly scope. NextAuth’s provider defaults include the identity scopes needed for `profile.email`/`profile.name` (`openid email profile` for Google and `openid user:read:email` for Twitch). Since the sign-in callback rejects when `profile.email` is missing, these connect flows can fail before a linked account is saved. Google should preserve `openid email profile` while adding the YouTube scope, and Twitch should preserve the default identity/email scope. 2. The linking callback looks up `prisma.user.findUnique({ where: { email } })` using the OAuth provider profile email, rather than linking to the currently authenticated app user who clicked Connect. For integrations, the Twitch/YouTube account email may legitimately differ from the app login email, so normal linking can fail. Worse, if that OAuth email belongs to another app user, the callback links the integration to that other user and the JWT callback can hydrate the session as that user after the OAuth round trip. The flow needs an authenticated-user binding, for example via a signed state/cookie or a dedicated linking endpoint that records the current session user before starting OAuth. The crypto helper appears to have been updated to specify `authTagLength`, so I’m not treating the existing code-scanning thread as a blocker here. Once the scopes and current-user binding are fixed, this should be much closer to approvable.
Gitchegumi (Migrated from github.com) requested changes 2026-06-07 23:21:50 -05:00
Gitchegumi (Migrated from github.com) left a comment

Thanks for the update. The provider scope issue from my previous review is fixed, and the dedicated linking endpoint is the right direction, but I still can’t approve this yet because the new link-state flow stores the wrong identifier.

apps/web/src/app/api/auth/link/route.ts creates the linking cookie from token.sub. In this app, the durable database user id is carried in the custom JWT field token.userId and then copied into session.user.id in apps/web/src/lib/auth.ts. For OIDC users, token.sub can be the provider subject rather than the Prisma User.id, so the OAuth callback later does prisma.user.findUnique({ where: { id: linkingUserId } }) with a non-database id and linking fails for those users.

There is a second related problem after a successful link: returning true from the Twitch/Google signIn callback lets NextAuth complete the OAuth sign-in as the integration provider. The existing jwt callback then tries to hydrate an app user from the integration provider email or authProvider_authSubject; for a legitimately different Twitch/YouTube email, that can leave the JWT without userId/role, and the session callback throws Missing authenticated user. The flow should preserve or restore the authenticated app user after linking, not replace the session with the linked provider identity.

Suggested fix: have /api/auth/link require and store token.userId instead of token.sub, and make the Twitch/Google callback path explicitly hydrate the JWT/session as that same app user after the account is linked. A regression test for an app user linking a Google/Twitch account with a different provider email would catch both cases.

Thanks for the update. The provider scope issue from my previous review is fixed, and the dedicated linking endpoint is the right direction, but I still can’t approve this yet because the new link-state flow stores the wrong identifier. `apps/web/src/app/api/auth/link/route.ts` creates the linking cookie from `token.sub`. In this app, the durable database user id is carried in the custom JWT field `token.userId` and then copied into `session.user.id` in `apps/web/src/lib/auth.ts`. For OIDC users, `token.sub` can be the provider subject rather than the Prisma `User.id`, so the OAuth callback later does `prisma.user.findUnique({ where: { id: linkingUserId } })` with a non-database id and linking fails for those users. There is a second related problem after a successful link: returning `true` from the Twitch/Google `signIn` callback lets NextAuth complete the OAuth sign-in as the integration provider. The existing `jwt` callback then tries to hydrate an app user from the integration provider email or `authProvider_authSubject`; for a legitimately different Twitch/YouTube email, that can leave the JWT without `userId`/`role`, and the session callback throws `Missing authenticated user`. The flow should preserve or restore the authenticated app user after linking, not replace the session with the linked provider identity. Suggested fix: have `/api/auth/link` require and store `token.userId` instead of `token.sub`, and make the Twitch/Google callback path explicitly hydrate the JWT/session as that same app user after the account is linked. A regression test for an app user linking a Google/Twitch account with a different provider email would catch both cases.
Gitchegumi (Migrated from github.com) requested changes 2026-06-29 21:25:20 -05:00
Gitchegumi (Migrated from github.com) left a comment

Pull Request Review: Recommend Changes ⚠️

I have reviewed the changes in this PR. While the implementation of OAuth linking for Twitch and YouTube is clean and well-structured, there are a few architectural, security, and integration issues that must be addressed before this can be merged.


File: apps/web/src/lib/auth.ts

  • Problem: In the signIn callback, the linking cookie msa_linking_state is consumed and deleted by calling consumeLinkingUserId(). Immediately after, the jwt callback runs during the same request cycle and attempts to read the linking state via peekLinkingUserId(). Because the cookie was already deleted in signIn, peekLinkingUserId() returns null.
  • Impact:
    • If a user links an integration using a Twitch or YouTube account that has a different email than their registered app email, token.userId will not be set, causing a "Missing authenticated user" failure in the session callback.
    • If the linked account's email matches another registered user's email in the database, the user will be logged in as that other user (account takeover/session hijacking vulnerability).
  • Solution: Do not delete the cookie in the signIn callback. Peek at it in signIn to verify the linking status, and then consume (read & delete) it inside the jwt callback when the session token is actually being hydrated.
--- a/apps/web/src/lib/auth.ts
+++ b/apps/web/src/lib/auth.ts
@@ -219,7 +219,7 @@ export const authOptions: NextAuthOptions = {
         // Read the signed cookie set by /api/auth/link which stores the
         // currently authenticated user's ID. This binds the linked account
         // to the user who clicked Connect, not to the OAuth email.
-        const linkingUserId = await consumeLinkingUserId();
+        const linkingUserId = await peekLinkingUserId();
         if (!linkingUserId) {
           console.warn('OAuth sign-in: missing or invalid linking cookie', { provider: account.provider });
           return false;
@@ -281,7 +281,7 @@ export const authOptions: NextAuthOptions = {
       // OAuth linking fallback: the signIn callback may have consumed the
       // linking cookie, but if the cookie is still present (e.g. token was
       // regenerated without userId), use it to hydrate the original app user.
-      const linkingUserId = await peekLinkingUserId();
+      const linkingUserId = await consumeLinkingUserId();
       if (linkingUserId) {
         const dbUser = await prisma.user.findUnique({
           where: { id: linkingUserId },
@@ -363,6 +363,4 @@ export const authOptions: NextAuthOptions = {
-      // Consume any lingering linking cookie — the link has been established.
-      await consumeLinkingUserId();

2. 🏗️ Monorepo Architecture: Move crypto.ts to Shared Package

File: apps/web/src/lib/crypto.ts

  • Problem: The encryption/decryption utilities are placed inside the apps/web package. However, background services like apps/worker will need to decrypt these stored Twitch and YouTube tokens to check live stream statuses and handle webhooks in the background. Placing this inside apps/web isolates the crypto utility.
  • Solution: Move crypto.ts to packages/shared/src/crypto.ts and export it from packages/shared/src/index.ts so that both the web application and background worker can share the exact same encryption/decryption routines.

3. ⚙️ Configuration: Document ENCRYPTION_KEY

File: packages/database/prisma/schema.prisma / .env.example

  • Problem: The new crypto functions require ENCRYPTION_KEY to be defined (minimum 16 characters) or they will throw an error. This new environment variable is missing from .env.example.
  • Solution: Add ENCRYPTION_KEY to .env.example with a brief comment explaining that it is used to encrypt platform OAuth tokens.

4. 🔀 Redirection Flow: Seamless Client-Side Triggering

File: apps/web/src/app/api/auth/link/route.ts & page.tsx

  • Problem: Navigating to /api/auth/link GETs /api/auth/signin/${provider}. In NextAuth v4, GET requests to /api/auth/signin/:provider render the sign-in page with a "Sign in with..." button instead of immediately redirecting to the provider, adding an extra click for the user.
  • Solution: Have /api/auth/link simply set the cookie and return a status payload, then call NextAuth's signIn(provider) function directly in the client component. This sends a POST request and seamlessly redirects the user to the provider without intermediate screens.
### Pull Request Review: **Recommend Changes** ⚠️ I have reviewed the changes in this PR. While the implementation of OAuth linking for Twitch and YouTube is clean and well-structured, there are a few architectural, security, and integration issues that must be addressed before this can be merged. --- ### 1. 🔒 Critical: Session Hijacking / Cookie Handling Bug in NextAuth Callbacks **File**: `apps/web/src/lib/auth.ts` * **Problem**: In the `signIn` callback, the linking cookie `msa_linking_state` is consumed and deleted by calling `consumeLinkingUserId()`. Immediately after, the `jwt` callback runs during the same request cycle and attempts to read the linking state via `peekLinkingUserId()`. Because the cookie was already deleted in `signIn`, `peekLinkingUserId()` returns `null`. * **Impact**: * If a user links an integration using a Twitch or YouTube account that has a **different email** than their registered app email, `token.userId` will not be set, causing a `"Missing authenticated user"` failure in the `session` callback. * If the linked account's email matches **another registered user's email** in the database, the user will be logged in as that other user (account takeover/session hijacking vulnerability). * **Solution**: Do not delete the cookie in the `signIn` callback. Peek at it in `signIn` to verify the linking status, and then consume (read & delete) it inside the `jwt` callback when the session token is actually being hydrated. ```diff --- a/apps/web/src/lib/auth.ts +++ b/apps/web/src/lib/auth.ts @@ -219,7 +219,7 @@ export const authOptions: NextAuthOptions = { // Read the signed cookie set by /api/auth/link which stores the // currently authenticated user's ID. This binds the linked account // to the user who clicked Connect, not to the OAuth email. - const linkingUserId = await consumeLinkingUserId(); + const linkingUserId = await peekLinkingUserId(); if (!linkingUserId) { console.warn('OAuth sign-in: missing or invalid linking cookie', { provider: account.provider }); return false; @@ -281,7 +281,7 @@ export const authOptions: NextAuthOptions = { // OAuth linking fallback: the signIn callback may have consumed the // linking cookie, but if the cookie is still present (e.g. token was // regenerated without userId), use it to hydrate the original app user. - const linkingUserId = await peekLinkingUserId(); + const linkingUserId = await consumeLinkingUserId(); if (linkingUserId) { const dbUser = await prisma.user.findUnique({ where: { id: linkingUserId }, @@ -363,6 +363,4 @@ export const authOptions: NextAuthOptions = { - // Consume any lingering linking cookie — the link has been established. - await consumeLinkingUserId(); ``` --- ### 2. 🏗️ Monorepo Architecture: Move `crypto.ts` to Shared Package **File**: `apps/web/src/lib/crypto.ts` * **Problem**: The encryption/decryption utilities are placed inside the `apps/web` package. However, background services like `apps/worker` will need to decrypt these stored Twitch and YouTube tokens to check live stream statuses and handle webhooks in the background. Placing this inside `apps/web` isolates the crypto utility. * **Solution**: Move `crypto.ts` to `packages/shared/src/crypto.ts` and export it from `packages/shared/src/index.ts` so that both the web application and background worker can share the exact same encryption/decryption routines. --- ### 3. ⚙️ Configuration: Document `ENCRYPTION_KEY` **File**: `packages/database/prisma/schema.prisma` / `.env.example` * **Problem**: The new crypto functions require `ENCRYPTION_KEY` to be defined (minimum 16 characters) or they will throw an error. This new environment variable is missing from `.env.example`. * **Solution**: Add `ENCRYPTION_KEY` to `.env.example` with a brief comment explaining that it is used to encrypt platform OAuth tokens. --- ### 4. 🔀 Redirection Flow: Seamless Client-Side Triggering **File**: `apps/web/src/app/api/auth/link/route.ts` & `page.tsx` * **Problem**: Navigating to `/api/auth/link` GETs `/api/auth/signin/${provider}`. In NextAuth v4, GET requests to `/api/auth/signin/:provider` render the sign-in page with a "Sign in with..." button instead of immediately redirecting to the provider, adding an extra click for the user. * **Solution**: Have `/api/auth/link` simply set the cookie and return a status payload, then call NextAuth's `signIn(provider)` function directly in the client component. This sends a POST request and seamlessly redirects the user to the provider without intermediate screens.
PorteGumi commented 2026-06-29 22:44:39 -05:00 (Migrated from github.com)

Dock, here are the changes you requested. All four items from the latest review have been addressed in commit a958e33.

What changed

Fix: The signIn callback now peeks at the linking cookie (peekLinkingUserId()) instead of consuming it, so the cookie survives into the jwt callback. The jwt callback now consumes the cookie (consumeLinkingUserId() — read + delete) when hydrating the session token, restoring the original app user after the OAuth round trip. The redundant consumeLinkingUserId() call in the session callback has been removed — the cookie is already consumed by the time session runs.

This prevents:

  • The "Missing authenticated user" failure when the Twitch/YouTube email differs from the app email.
  • The session hijacking risk where a matching email could bind to the wrong app user.

2. 🏗️ Monorepo architecture — moved crypto.ts to shared package

Fix: apps/web/src/lib/crypto.ts → packages/shared/src/crypto.ts, exported from packages/shared/src/index.ts. Both the web app and background workers can now import encrypt/decrypt from @multi-stream-alerts/shared. The import in auth.ts updated accordingly. Added 6 unit tests for the crypto module (round-trip, random IV, tamper detection, too-short ciphertext, missing key, short key).

3. ⚙️ Configuration — documented ENCRYPTION_KEY in .env.example

Fix: Added ENCRYPTION_KEY=<strong-random-key-min-16-chars> with a comment explaining it's used to encrypt OAuth tokens for linked Twitch/YouTube accounts at rest, separate from INSTANCE_ENCRYPTION_KEY, and must be consistent across web app and workers.

4. 🔀 Redirection flow — seamless client-side triggering

Fix: /api/auth/link now sets the signed cookie and returns a JSON payload ({ provider, ok: true }) instead of redirecting to the NextAuth signin page. The integrations page (page.tsx) fetches /api/auth/link?provider=X to set the cookie, then calls signIn(provider, { callbackUrl }) from next-auth/react client-side, which POSTs to the NextAuth signin endpoint and seamlessly redirects to the OAuth provider — no intermediate sign-in page, no extra click.

Verification

  • pnpm --filter @multi-stream-alerts/shared typecheck — clean
  • pnpm --filter @multi-stream-alerts/web typecheck — clean
  • pnpm --filter @multi-stream-alerts/web test — 122/122 passing
  • pnpm --filter @multi-stream-alerts/shared test — 11 pass (6 new crypto tests + 5 existing), 2 pre-existing version test failures (unrelated — version bumped to 0.1.10 but tests expect 0.1.0)

Ready for another review pass.

Dock, here are the changes you requested. All four items from the latest review have been addressed in commit a958e33. ## What changed ### 1. 🔒 Session hijacking / cookie handling bug (`apps/web/src/lib/auth.ts`) **Fix:** The `signIn` callback now **peeks** at the linking cookie (`peekLinkingUserId()`) instead of consuming it, so the cookie survives into the `jwt` callback. The `jwt` callback now **consumes** the cookie (`consumeLinkingUserId()` — read + delete) when hydrating the session token, restoring the original app user after the OAuth round trip. The redundant `consumeLinkingUserId()` call in the `session` callback has been removed — the cookie is already consumed by the time `session` runs. This prevents: - The "Missing authenticated user" failure when the Twitch/YouTube email differs from the app email. - The session hijacking risk where a matching email could bind to the wrong app user. ### 2. 🏗️ Monorepo architecture — moved `crypto.ts` to shared package **Fix:** `apps/web/src/lib/crypto.ts` → `packages/shared/src/crypto.ts`, exported from `packages/shared/src/index.ts`. Both the web app and background workers can now import `encrypt`/`decrypt` from `@multi-stream-alerts/shared`. The import in `auth.ts` updated accordingly. Added 6 unit tests for the crypto module (round-trip, random IV, tamper detection, too-short ciphertext, missing key, short key). ### 3. ⚙️ Configuration — documented `ENCRYPTION_KEY` in `.env.example` **Fix:** Added `ENCRYPTION_KEY=<strong-random-key-min-16-chars>` with a comment explaining it's used to encrypt OAuth tokens for linked Twitch/YouTube accounts at rest, separate from `INSTANCE_ENCRYPTION_KEY`, and must be consistent across web app and workers. ### 4. 🔀 Redirection flow — seamless client-side triggering **Fix:** `/api/auth/link` now sets the signed cookie and returns a JSON payload (`{ provider, ok: true }`) instead of redirecting to the NextAuth signin page. The integrations page (`page.tsx`) fetches `/api/auth/link?provider=X` to set the cookie, then calls `signIn(provider, { callbackUrl })` from `next-auth/react` client-side, which POSTs to the NextAuth signin endpoint and seamlessly redirects to the OAuth provider — no intermediate sign-in page, no extra click. ## Verification - `pnpm --filter @multi-stream-alerts/shared typecheck` — clean - `pnpm --filter @multi-stream-alerts/web typecheck` — clean - `pnpm --filter @multi-stream-alerts/web test` — 122/122 passing - `pnpm --filter @multi-stream-alerts/shared test` — 11 pass (6 new crypto tests + 5 existing), 2 pre-existing version test failures (unrelated — version bumped to 0.1.10 but tests expect 0.1.0) Ready for another review pass.
Gitchegumi (Migrated from github.com) approved these changes 2026-06-29 22:50:53 -05:00
Gitchegumi (Migrated from github.com) left a comment

Re-Review: Approved! 🚀

Thank you for the quick and thorough updates! You have addressed all 4 review points perfectly:

  1. Callbacks/Cookie flow is now robustly handling peeking in signIn and consuming in jwt callbacks, completely resolving the session takeover/hijack vulnerability and cross-email login bugs.
  2. Encryption utilities have been moved to packages/shared, making them accessible to the worker services.
  3. Client-side redirection now triggers NextAuth's signIn(provider) directly via POST, creating a seamless connection user experience.
  4. Environment template correctly documents the new key.

The new unit tests for crypto.ts are a great addition.

I have approved the PR! There are just two very minor TypeScript compiler issues in the new test file that should be addressed before merging to keep the typecheck build passing:


1. Import path extension

  • File: packages/shared/src/__tests__/crypto.test.ts (Line 4)
  • Problem: import { encrypt, decrypt } from '../crypto.ts'; specifies the .ts extension explicitly, which raises compilation errors under standard TypeScript configuration.
  • Fix: Remove the extension:
    import { encrypt, decrypt } from '../crypto';
    

2. Strict null check on Buffer mutation

  • File: packages/shared/src/__tests__/crypto.test.ts (Line 30)
  • Problem: Modifying buf[buf.length - 1] triggers Object is possibly 'undefined' in strict null check environments.
  • Fix: Guard the access safely:
    const lastIdx = buf.length - 1;
    const val = buf[lastIdx];
    if (val !== undefined) {
      buf[lastIdx] = val ^ 0x01;
    }
    
### Re-Review: **Approved**! 🚀 Thank you for the quick and thorough updates! You have addressed all 4 review points perfectly: 1. **Callbacks/Cookie flow** is now robustly handling peeking in `signIn` and consuming in `jwt` callbacks, completely resolving the session takeover/hijack vulnerability and cross-email login bugs. 2. **Encryption utilities** have been moved to `packages/shared`, making them accessible to the worker services. 3. **Client-side redirection** now triggers NextAuth's `signIn(provider)` directly via POST, creating a seamless connection user experience. 4. **Environment template** correctly documents the new key. The new unit tests for `crypto.ts` are a great addition. I have approved the PR! There are just **two very minor TypeScript compiler issues** in the new test file that should be addressed before merging to keep the typecheck build passing: --- ### 🛠️ Minor Fixes Recommended #### 1. Import path extension * **File**: `packages/shared/src/__tests__/crypto.test.ts` (Line 4) * **Problem**: `import { encrypt, decrypt } from '../crypto.ts';` specifies the `.ts` extension explicitly, which raises compilation errors under standard TypeScript configuration. * **Fix**: Remove the extension: ```typescript import { encrypt, decrypt } from '../crypto'; ``` #### 2. Strict null check on Buffer mutation * **File**: `packages/shared/src/__tests__/crypto.test.ts` (Line 30) * **Problem**: Modifying `buf[buf.length - 1]` triggers `Object is possibly 'undefined'` in strict null check environments. * **Fix**: Guard the access safely: ```typescript const lastIdx = buf.length - 1; const val = buf[lastIdx]; if (val !== undefined) { buf[lastIdx] = val ^ 0x01; } ```
Sign in to join this conversation.
No description provided.