Skip to content

Feat(client&extension): 온보딩<->익스텐션 데이터 통신 작업#99

Merged
jllee000 merged 4 commits intodevelopfrom
feat/#85/data-cross-communication
Sep 13, 2025
Merged

Feat(client&extension): 온보딩<->익스텐션 데이터 통신 작업#99
jllee000 merged 4 commits intodevelopfrom
feat/#85/data-cross-communication

Conversation

@jllee000
Copy link
Collaborator

@jllee000 jllee000 commented Sep 13, 2025

📌 Related Issues

관련된 Issue를 태그해주세요. (e.g. - close #25)

📄 Tasks

[플로우] 는 다음과 같습니당!

  1. 익스텐션 크롬 권한을 통해 유저메일 추출
  2. 익스텐션 설치와 동시에 온보딩 랜딩 (쿼리스트링으로 메일 전달)
  3. 온보딩 : 쿼리스트링 메일로 유저 토큰 요청
  4. 온보딩 : 받은 유저토큰을 익스텐션으로 크롬 메세징으로 전달 (content 스크립트 통해서!)
  5. 익스텐션 : 크롬 메시지로 받은 유저토큰 크롬스토리지에 저장

⭐ PR Point (To Reviewer)

1. 익스텐션의 크롬 권한을 최대한 활용!!

(1) "identity","identity.email" 권한을 추가해서! 이메일을 받아오기
(2) chrome.runtime.onInstalled.addListener를 활용해서, 익스텐션 설치 시점 활용하기!
(3) content.ts 스크립트는, 온보딩이든 대시보드이든 무조건 항상 ui 뒤에서 작동하고 있어서! 이 스크립트로, 메시지를 주고받을 수 있어요!

2. 익스텐션은 로컬 스토리지가 없어요!

  chrome.storage.local.set({ 'userEmail': info.email }, () => {
          console.log(info.email);
        });

이와 동일한 크롬 스토리지 활용하기!

📷 Screenshot

Summary by CodeRabbit

  • 신규 기능

    • 온보딩 URL에서 이메일을 자동 인식해 가입 정보에 반영
    • 확장 프로그램 최초 설치 시 계정 이메일을 받아 저장하고 온보딩 페이지를 자동 오픈
    • 웹 앱 로그인 성공 시 토큰을 확장 프로그램으로 안전 전달 및 저장
    • 확장 프로그램에서 우클릭 시 외부 사이트로 이동
  • 개선

    • 북마크 저장 후 확장 프로그램 창 자동 종료
    • 저장 불가 페이지에서는 알림 표시 후 창 자동 종료
  • 기타(권한/설정)

    • 확장 프로그램 권한 업데이트: identity/identity.email 추가, scripting 제거

@coderabbitai
Copy link

coderabbitai bot commented Sep 13, 2025

Walkthrough

온보딩-익스텐션 연동을 추가했다. 익스텐션 설치 시 크롬 계정 이메일을 저장하고 온보딩 페이지를 이메일 쿼리로 연다. 온보딩 완료 후 발급된 토큰을 window.postMessage로 전달하면 컨텐츠 스크립트가 백그라운드로 포워딩해 크롬 스토리지에 저장한다. 관련 권한과 키 변경을 반영했다.

Changes

Cohort / File(s) Summary
Client 온보딩 이메일 취득/전달
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
URL 쿼리에서 email을 읽어 상태로 저장하고, 회원가입 요청 페이로드의 email에 반영.
Client→Extension 토큰 브릿지 트리거
apps/client/src/shared/apis/queries.ts
회원가입 성공 시 window.postMessage로 { type: 'SET_TOKEN', token } 송신 로직 추가.
Extension 설치/온보딩 진입 및 토큰 수신
apps/extension/src/background.ts, apps/extension/src/content.ts, apps/extension/manifest.json
- onInstalled에서 chrome.identity.getProfileUserInfo로 이메일 저장 후 온보딩 URL 오픈.
- content 스크립트가 SET_TOKEN 윈도우 메시지를 runtime 메시지로 포워딩.
- background가 SET_TOKEN 수신 시 크롬 스토리지에 토큰 저장.
- manifest 권한에서 scripting 제거, identity, identity.email 추가.
Extension 인증 스토리지 키 정합성
apps/extension/src/apis/axiosInstance.ts
요청 인터셉터가 이메일 키를 emailuserEmail로 변경해 토큰 갱신 입력값 정렬.
Extension UI/행동 조정
apps/extension/src/App.tsx, apps/extension/src/hooks/useSaveBookmarks.ts, apps/extension/src/pages/MainPop.tsx
- 우클릭 시 내부 경로 대신 외부 URL로 이동.
- 북마크 생성 트리거 후 window.close() 호출 추가.
- 저장 불가 페이지 감지 시 알럿 표시 후 창 닫기.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as 사용자
  participant Ext as Extension (Background)
  participant Chrome as Chrome Identity
  participant Onb as Onboarding Page

  rect rgba(230,245,255,0.6)
  note over Ext: 설치 이벤트
  User->>Ext: 확장 설치
  Ext->>Chrome: getProfileUserInfo()
  Chrome-->>Ext: { email }
  Ext->>Ext: chrome.storage.local.set({ userEmail: email })
  Ext->>Onb: open http://localhost:5173/onboarding?email=<email>
  end
Loading
sequenceDiagram
  autonumber
  participant Web as Client (Onboarding Web)
  participant Win as window
  participant CS as Content Script
  participant BG as Background
  participant Store as chrome.storage.local

  rect rgba(242,255,237,0.6)
  note over Web: 회원가입 성공
  Web->>Win: postMessage({ type: "SET_TOKEN", token })
  Win-->>CS: window message
  CS->>BG: chrome.runtime.sendMessage({ type: "SET_TOKEN", token })
  BG->>Store: set({ token })
  Store-->>BG: ok
  BG-->>CS: "Token saved!" (log)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

🛠️ Feature, api, frontend

Suggested reviewers

  • constantly-dev
  • jjangminii

Poem

새 탭에 퐁, 메일이 톡—
토큰은 점프해 상자에 쏙.
창은 살짝 닫히고, 길은 곧게 열려요.
나는 토끼, 귀 쫑긋—메시지 따라 뛰어요.
온보딩 별빛 아래, 확장은 집을 찾아요. 🐰✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning 이 PR은 linked issue #85의 핵심 요구사항을 충족합니다: background.ts에서 chrome.identity.getProfileUserInfo로 이메일을 추출해 chrome.storage.local에 'userEmail'로 저장하고 설치 시 온보딩 페이지(쿼리스트링 email)로 이동시키며, 클라이언트 쪽에서 쿼리스트링 이메일로 회원가입 후 localStorage에 토큰을 저장하고 window.postMessage로 토큰을 전송하면 content.ts가 이를 chrome.runtime.sendMessage로 포워딩하고 background.ts가 chrome.storage.local에 token으로 저장하는 흐름이 구현되어 'localStorage와 크롬 스토리지 모두 업데이트' 및 익스텐션 쪽 토큰 관리 요구를 만족합니다. 그러나 linked issue #25(Progress 컴포넌트 구현)는 본 PR의 변경 목록에 포함되어 있지 않습니다. 해결 방안으로는 #25 관련 작업을 본 PR에 포함시켜 컴포넌트·스토리북·테스트를 추가하거나, 아니라면 PR에서 #25 링크를 제거하고 해당 작업을 별도 PR로 분리해 주세요.
Out of Scope Changes Check ⚠️ Warning 본 PR에는 온보딩<->익스텐션 데이터 통신 목적과 직접 관련이 없어 보이는 변경이 포함되어 있습니다: apps/extension/src/App.tsx에서 내부 라우팅을 외부 URL(https://www.pinback.today/)로 변경한 점, apps/extension/src/pages/MainPop.tsx에서 제목이 없을 때 alert와 window.close를 활성화한 점, useSaveBookmarks에서 window.close를 즉시 호출하도록 변경한 점, background.ts에서 기존 OG 메타 fetch/응답 패턴을 제거한 점 등이 그것입니다. manifest의 identity 권한 추가와 onInstalled 기반 이메일 전달은 목적상 타당하지만 UI 동작 변경 및 OG 로직 삭제, 외부 URL 하드코딩(예: localhost → production URL 전환 고려) 등은 범위를 벗어나므로 별도 PR로 분리하거나 변경 이유를 명확히 문서화해야 합니다. 해결을 위해 범위를 벗어난 변경은 롤백하거나 커밋을 분리해 별도 PR로 제출하고 각 변경의 필요성 및 QA 절차(특히 localhost 하드코딩·외부 URL 전환·자동 창 닫기 동작)를 PR 본문에 분명히 기재해 주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "Feat(client&extension): 온보딩<->익스텐션 데이터 통신 작업"은 변경의 핵심인 온보딩과 익스텐션 간 데이터 통신을 명확히 요약하고 있어 간결하며 불필요한 잡음(파일 리스트, 이모지 등)이 없습니다.
Description Check ✅ Passed PR 설명은 레포지토리 템플릿 구조를 따르고 있으며 Related Issues( close #85 ), Tasks(플로우 단계별 설명), PR Point(검토 포인트) 섹션이 충실히 작성되어 있어 구현 목적과 흐름이 명확합니다; 스크린샷은 없지만 템플릿상 선택 항목이므로 전체적으로 양호합니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#85/data-cross-communication

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 and usage tips.

@jllee000 jllee000 self-assigned this Sep 13, 2025
@github-actions github-actions bot added the feat 기능 개발하라 개발 달려라 달려 label Sep 13, 2025
@github-actions
Copy link

✅ Storybook chromatic 배포 확인:
🐿️ storybook

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (4)
apps/extension/manifest.json (1)

15-20: 콘텐츠 스크립트/호스트 권한을 최소 권한으로 축소하세요

현재 <all_urls>는 과도합니다。온보딩/웹앱 도메인으로 한정하면 공격면이 크게 줄어듭니다.

-  "content_scripts": [
-    {
-      "matches": ["<all_urls>"],
-      "js": ["src/content.js"]
-    }
-  ],
-  "host_permissions": ["<all_urls>"]
+  "content_scripts": [
+    {
+      "matches": ["https://www.pinback.today/*", "http://localhost:5173/*"],
+      "js": ["src/content.js"]
+    }
+  ],
+  "host_permissions": ["https://www.pinback.today/*", "http://localhost:5173/*"]
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2)

63-65: Firebase initializeApp 중복 호출 위험

컴포넌트 렌더마다 initializeApp을 호출하면 app/duplicate-app 에러가 납니다. 모듈 스코프로 이동하고 getApps 가드로 보호하세요.

-import { initializeApp } from 'firebase/app';
+import { initializeApp, getApp, getApps } from 'firebase/app';
@@
-const MainCard = () => {
+const firebaseApp = getApps().length ? getApp() : initializeApp(firebaseConfig);
+const MainCard = () => {
@@
-  const app = initializeApp(firebaseConfig);
-  const messaging = getMessaging(app);
+  const messaging = getMessaging(firebaseApp);

137-145: remindTime 상태 갱신 직후 값을 사용해 요청하는 버그

setState는 비동기입니다. normalize 결과를 지역 변수로 만들어 바로 사용하세요.

-      const raw = AlarmsType[alarmSelected - 1].time;
-      setRemindTime(normalizeTime(raw));
-
-      postSignData({
-            "email": userEmail, 
-            "remindDefault": remindTime, 
+      const raw = AlarmsType[alarmSelected - 1].time;
+      const normalized = normalizeTime(raw);
+      setRemindTime(normalized);
+      postSignData({
+            "email": userEmail,
+            "remindDefault": normalized,
             "fcmToken": fcmToken,
         },
apps/extension/src/apis/axiosInstance.ts (1)

64-66: 하드코딩된 이메일로 토큰 재발급 요청

401/403에서 test@gmail.com으로 토큰을 요청하고 있습니다. 저장된 userEmail을 사용해야 합니다.

-      const newToken = await fetchToken('test@gmail.com');
+      const storedEmail = await new Promise<string | undefined>((resolve) => {
+        chrome.storage.local.get('userEmail', (r) => resolve(r.userEmail));
+      });
+      const newToken = await fetchToken(storedEmail);
🧹 Nitpick comments (5)
apps/extension/src/hooks/useSaveBookmarks.ts (1)

32-43: 북마크 생성 성공/오류 확인 전에 창을 닫고 있습니다

chrome.bookmarks.create 콜백에서 lastError 확인 후 닫도록 바꿔야 에러를 놓치지 않습니다. Promise로 감싼 뒤 await 처리 권장.

-      chrome.bookmarks.create(
-        {
-          parentId: '1',
-          title: params.title || params.url,
-          url: params.url,
-        },
-        (newBookmark) => {
-          console.log('크롬 북마크바에 저장 완료: ', newBookmark);
-        }
-      );
-      window.close();
+      await new Promise<void>((resolve, reject) => {
+        chrome.bookmarks.create(
+          {
+            parentId: '1',
+            title: params.title || params.url,
+            url: params.url,
+          },
+          (newBookmark) => {
+            if (chrome.runtime.lastError) {
+              return reject(chrome.runtime.lastError);
+            }
+            console.log('크롬 북마크바에 저장 완료: ', newBookmark);
+            resolve();
+          }
+        );
+      });
+      window.close();
apps/extension/src/pages/MainPop.tsx (1)

41-43: alert 대신 디자인 시스템 컴포넌트 사용 고려

브라우저 alert는 거칠게 느껴집니다. DS의 Modal/Toast로 교체하면 일관된 UX를 유지할 수 있습니다. 창 닫기는 사용자 확인 후 진행하세요.

apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)

52-58: URL 쿼리 파싱은 react-router의 useLocation으로 처리

글로벌 location 의존은 리렌더 트리거가 약합니다. useLocation으로 안전하게 파싱하세요. 이메일 유효성 검증도 추가하세요.

+import { useLocation } from 'react-router-dom';
@@
-  useEffect(() => {
-    const params = new URLSearchParams(location.search);
+  const location = useLocation();
+  useEffect(() => {
+    const params = new URLSearchParams(location.search);
     const emailParam = params.get("email"); 
-    if (emailParam) {
+    if (emailParam && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailParam)) {
       setUserEmail(emailParam);
     }
-  }, [location.search]);
+  }, [location.search]);
apps/extension/src/apis/axiosInstance.ts (1)

30-33: userEmail 미존재 시 대비 로직 보강

userEmail이 없으면 fetchToken이 실패합니다. 초기 온보딩 전 경로에서는 요청을 스킵하거나 명확한 오류를 내세요.

-  const email = await new Promise<string | undefined>((resolve) => {
+  const email = await new Promise<string | undefined>((resolve) => {
     chrome.storage.local.get('userEmail', (result) => resolve(result.userEmail));
   });
+  if (!email) {
+    // 온보딩 전: 인증 필요 없는 요청만 허용
+    if (isNoAuth) return config;
+    throw new Error('No userEmail in chrome.storage.local');
+  }
apps/client/src/shared/apis/queries.ts (1)

64-72: postMessage 채널 하드닝 제안

type만으로는 부족합니다. nonce/txId를 추가해 재사용/오용을 방지하고, 서버 응답에 포함된 임시 nonce와 매칭하는 방식을 고려하세요.

-      const sendTokenToExtension = (token: string) => {
+      const sendTokenToExtension = (token: string) => {
+        const nonce = crypto.getRandomValues(new Uint32Array(1))[0].toString(16);
         window.postMessage(
           {
             type: 'SET_TOKEN',
             token,
+            nonce,
           },
           window.location.origin
         );
       };
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80100db and 84e6148.

📒 Files selected for processing (9)
  • apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2 hunks)
  • apps/client/src/shared/apis/queries.ts (1 hunks)
  • apps/extension/manifest.json (1 hunks)
  • apps/extension/src/App.tsx (1 hunks)
  • apps/extension/src/apis/axiosInstance.ts (1 hunks)
  • apps/extension/src/background.ts (1 hunks)
  • apps/extension/src/content.ts (1 hunks)
  • apps/extension/src/hooks/useSaveBookmarks.ts (1 hunks)
  • apps/extension/src/pages/MainPop.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-08T11:47:10.642Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.

Applied to files:

  • apps/extension/src/App.tsx
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.

Applied to files:

  • apps/extension/src/pages/MainPop.tsx
📚 Learning: 2025-07-15T20:00:13.756Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.756Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.

Applied to files:

  • apps/extension/src/pages/MainPop.tsx
🪛 ast-grep (0.38.6)
apps/client/src/shared/apis/queries.ts

[warning] 73-73: Detected potential storage of sensitive information in browser localStorage. Sensitive data like email addresses, personal information, or authentication tokens should not be stored in localStorage as it's accessible to any script.
Context: localStorage.setItem('token', newToken)
Note: [CWE-312] Cleartext Storage of Sensitive Information [REFERENCES]
- https://owasp.org/www-community/vulnerabilities/HTML5_Security_Cheat_Sheet
- https://cwe.mitre.org/data/definitions/312.html

(browser-storage-sensitive-data)

🪛 GitHub Check: lint
apps/extension/src/background.ts

[warning] 21-21:
Unexpected console statement


[warning] 6-6:
Unexpected console statement

🔇 Additional comments (4)
apps/extension/manifest.json (1)

9-9: 유지: 'identity.email' 권한은 유효하고 필요함
chrome.identity.getProfileUserInfo가 이메일을 반환하려면 manifest에 "identity.email" 권한 선언이 필요합니다(단순 "identity"만으로는 이메일이 빈값일 수 있음). 사용자가 Chrome에 로그인/동기화되어 있어야 하며 accountStatus 옵션(예: ANY)을 활용할 수 있습니다.
파일: apps/extension/manifest.json — permissions 배열(라인 9) 그대로 유지.

apps/extension/src/background.ts (3)

2-3: 설치 이벤트 분기는 적절합니다

초기 설치에만 온보딩을 트리거하는 조건 분기 로직은 의도와 맞습니다.


4-4: 확인: chrome.identity.getProfileUserInfo는 MV3에서 deprecated 아님 — 이메일 반환은 권한/로그인 상태에 의존

요약: deprecated 아님. 이메일이 빈 문자열로 반환되는 경우 — (1) 사용자가 Chrome에 로그인된 프로필(주계정)이 없음, (2) manifest에 "identity.email" 권한이 선언되지 않음, (3) accountStatus 기본값("SYNC") 사용 시 프로필이 동기화되지 않으면 빈 값이 될 수 있음. 조치: manifest에 "identity.email" (및 보통 "identity") 추가하고, 동기화되지 않은 프로필을 포함하려면 getProfileUserInfo 호출 시 {accountStatus: "ANY"} 사용을 검토. 위치: apps/extension/src/background.ts (라인 4) — 해당 호출의 권한/옵션 확인 필요.


6-6: console.log 제거 및 개발 환경 전용 가드 적용

토큰/이메일 로그는 삭제하고, 나머지 console 호출은 개발 환경에서만 허용하세요 (import.meta.env.DEV).

위치: apps/extension/src/background.ts — lines 6, 21

-console.log('백그라운드 기능');
+if (import.meta.env.DEV) console.log('background boot');

검증 불가: lint 실행 실패 (turbo: not found · node_modules 없음 · Corepack 활성화 필요). 로컬에서 lint 재실행 결과 첨부.

Comment on lines 140 to 144
postSignData({
"email": "tesdfdfsst@gmail.com", // TODO : 익스텐션에게서 메일 받기
"email": userEmail,
"remindDefault": remindTime,
"fcmToken": fcmToken,
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이메일 누락 시 가입 요청 차단

userEmail이 비어있으면 요청을 막고 사용자 안내하세요.

-      postSignData({
+      if (!userEmail) {
+        alert('이메일 정보가 없어 회원가입을 진행할 수 없어요.');
+        return;
+      }
+      postSignData({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
postSignData({
"email": "tesdfdfsst@gmail.com", // TODO : 익스텐션에게서 메일 받기
"email": userEmail,
"remindDefault": remindTime,
"fcmToken": fcmToken,
},
if (!userEmail) {
alert('이메일 정보가 없어 회원가입을 진행할 수 없어요.');
return;
}
postSignData({
"email": userEmail,
"remindDefault": remindTime,
"fcmToken": fcmToken,
},
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx around lines
140-144, the signup call is made even when userEmail is empty; add a guard that
validates userEmail (e.g., check trimmed length) before calling postSignData,
and if it's missing return early and show the user an inline error or toast
message indicating the email is required; ensure the function exits without
making the request when validation fails.

Comment on lines 73 to 76
if (newToken) {
localStorage.setItem('token', newToken);
sendTokenToExtension(newToken);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

토큰을 localStorage에 저장하는 것은 XSS에 취약

가능하다면 서버가 HttpOnly Secure 쿠키로 토큰(또는 세션)을 설정하도록 전환하세요. 프론트에서는 localStorage 보관을 제거합니다.

대안:

  • 서버: /auth/signup 응답 시 Set-Cookie(HttpOnly; Secure; SameSite=Strict; Path=/)로 액세스/리프레시 토큰 설정.
  • 클라이언트: Authorization 헤더 제거, 쿠키 기반 인증으로 전환.
  • 불가 시: 토큰을 메모리 저장 + 짧은 만료 + 엄격한 CSP로 리스크 완화.
🧰 Tools
🪛 ast-grep (0.38.6)

[warning] 73-73: Detected potential storage of sensitive information in browser localStorage. Sensitive data like email addresses, personal information, or authentication tokens should not be stored in localStorage as it's accessible to any script.
Context: localStorage.setItem('token', newToken)
Note: [CWE-312] Cleartext Storage of Sensitive Information [REFERENCES]
- https://owasp.org/www-community/vulnerabilities/HTML5_Security_Cheat_Sheet
- https://cwe.mitre.org/data/definitions/312.html

(browser-storage-sensitive-data)


const handleDuplicateRightClick = () => {
window.location.href = "/dashboard";
window.location.href = "https://www.pinback.today/";
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

외부 페이지 이동은 chrome.tabs.create를 사용하세요

popup 내부에서 window.location.href 변경은 UX가 어색합니다. 새 탭을 여는 쪽이 일반적이며, 환경별 베이스 URL로 관리하세요.

-  const handleDuplicateRightClick = () => {
-    window.location.href = "https://www.pinback.today/";
-  };
+  const handleDuplicateRightClick = () => {
+    const url = import.meta.env.VITE_WEB_BASE_URL ?? "https://www.pinback.today/";
+    chrome.tabs.create({ url });
+  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window.location.href = "https://www.pinback.today/";
const handleDuplicateRightClick = () => {
const url = import.meta.env.VITE_WEB_BASE_URL ?? "https://www.pinback.today/";
chrome.tabs.create({ url });
};
🤖 Prompt for AI Agents
In apps/extension/src/App.tsx around line 27, replace the direct
window.location.href assignment with chrome.tabs.create to open
"https://www.pinback.today/" in a new tab using a base URL taken from your
environment/config (do not hardcode); after creating the tab, close the popup
(window.close()) to match extension UX. Also ensure the extension has the "tabs"
permission or use chrome.tab API appropriate to your manifest version and pull
the base URL from a central config/env var so it can vary by environment.

Comment on lines +4 to 7
chrome.identity.getProfileUserInfo(function (info) {
chrome.storage.local.set({ 'userEmail': info.email }, () => {
console.log(info.email);
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

이메일 수집 시 PII 로그 제거 + 빈 이메일/에러 처리 필요

  • 사용자 이메일을 콘솔에 출력하면 PII 노출 위험이 큽니다.
  • 크롬에 로그인되어 있지 않으면 info.email이 빈 문자열일 수 있어 가드가 필요합니다.
  • chrome.storage.local.setruntime.lastError 체크가 없습니다.

아래처럼 수정해 주세요.

-    chrome.identity.getProfileUserInfo(function (info) {
-       chrome.storage.local.set({ 'userEmail': info.email }, () => {
-          console.log(info.email);
-        });
+    chrome.identity.getProfileUserInfo(function (info) {
+      const email = info?.email?.trim();
+      if (email) {
+        chrome.storage.local.set({ userEmail: email }, () => {
+          if (chrome.runtime.lastError) {
+            console.warn('userEmail 저장 실패:', chrome.runtime.lastError.message);
+          }
+        });
+      } else {
+        console.warn('Chrome 프로필 이메일을 사용할 수 없습니다(비로그인/권한 미부여).');
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
chrome.identity.getProfileUserInfo(function (info) {
chrome.storage.local.set({ 'userEmail': info.email }, () => {
console.log(info.email);
});
chrome.identity.getProfileUserInfo(function (info) {
const email = info?.email?.trim();
if (email) {
chrome.storage.local.set({ email }, () => {
if (chrome.runtime.lastError) {
console.warn('email 저장 실패:', chrome.runtime.lastError.message);
}
});
} else {
console.warn('Chrome 프로필 이메일을 사용할 수 없습니다(비로그인/권한 미부여).');
}
});
🧰 Tools
🪛 GitHub Check: lint

[warning] 6-6:
Unexpected console statement

🤖 Prompt for AI Agents
In apps/extension/src/background.ts around lines 4-7, remove the console.log
that prints info.email to avoid PII exposure; add a guard that checks if
info.email is a non-empty string before calling chrome.storage.local.set (skip
storing if empty and handle accordingly); after calling
chrome.storage.local.set, check chrome.runtime.lastError and handle/log it via
chrome.runtime.lastError.message (do not log the email itself); ensure any error
or empty-email paths are handled gracefully (e.g., set a flag like
userEmailAvailable: false or return early).

Comment on lines +8 to +12
setTimeout(() => {
chrome.tabs.create({
url: `http://localhost:5173/onboarding?email=${info.email}`,
});
}, 1000);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

이메일을 URL 쿼리로 전달하지 마세요(PII 유출 위험) + 불필요한 setTimeout 제거

  • 이메일을 쿼리스트링으로 넘기면 서버 로그/리퍼러/분석 도구로 쉽게 유출될 수 있습니다.
  • 1초 대기는 비결정적입니다. 탭 생성 콜백에서 바로 처리하세요.
  • 권장: 탭만 열고, 컨텐츠 스크립트로 이메일을 전달해 페이지로 postMessage 브리징.
-      setTimeout(() => {
-        chrome.tabs.create({
-          url: `http://localhost:5173/onboarding?email=${info.email}`,
-        });
-      }, 1000);
+      chrome.tabs.create({ url: 'http://localhost:5173/onboarding' }, (tab) => {
+        const email = info?.email?.trim();
+        if (!email || !tab?.id) return;
+        // 컨텐츠 스크립트가 페이지로 window.postMessage('SET_EMAIL') 포워딩
+        chrome.tabs.sendMessage(
+          tab.id,
+          { type: 'SET_EMAIL', email },
+          () => void chrome.runtime.lastError // 컨텐츠 스크립트 로드 전 호출시 에러 무시
+        );
+      });

컨텐츠 스크립트 측에서는 SET_EMAIL 수신 → window.postMessage로 페이지에 전달하도록 맞춰 주세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTimeout(() => {
chrome.tabs.create({
url: `http://localhost:5173/onboarding?email=${info.email}`,
});
}, 1000);
chrome.tabs.create({ url: 'http://localhost:5173/onboarding' }, (tab) => {
const email = info?.email?.trim();
if (!email || !tab?.id) return;
// 컨텐츠 스크립트가 페이지로 window.postMessage('SET_EMAIL') 포워딩
chrome.tabs.sendMessage(
tab.id,
{ type: 'SET_EMAIL', email },
() => void chrome.runtime.lastError
);
});

return true; // async 응답
setTimeout(() => {
chrome.tabs.create({
url: `http://localhost:5173/onboarding?email=${info.email}`,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

개발용 localhost URL 하드코딩 제거

배포 시 깨집니다. 환경변수/설정으로 분리하세요.

적용 예(파일 상단 등 외부 추가 코드):

// 외부 추가 코드
const ONBOARDING_ORIGIN = import.meta.env.VITE_CLIENT_ORIGIN ?? 'https://app.pinback.io';
const ONBOARDING_URL = new URL('/onboarding', ONBOARDING_ORIGIN).toString();

해당 라인 교체:

-          url: `http://localhost:5173/onboarding?email=${info.email}`,
+          url: ONBOARDING_URL,
🤖 Prompt for AI Agents
In apps/extension/src/background.ts around line 10, the onboarding URL is
hardcoded to localhost which will break in production; define an
environment-backed origin constant at the top of the file (e.g., read
import.meta.env.VITE_CLIENT_ORIGIN with a production fallback like
https://app.pinback.io), build the onboarding base URL using that origin and the
/onboarding path, then replace the hardcoded string with code that appends the
email query param to that constructed URL so runtime origin is configurable via
env.

Comment on lines +18 to +24
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'SET_TOKEN') {
chrome.storage.local.set({ 'token': message.token }, () => {
console.log('Token saved!', message.token);
});
}
}); No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

토큰 콘솔 출력 제거 + 검증/응답 처리 + 발신자 검증 추가

  • 토큰을 콘솔에 남기면 심각한 보안 리스크입니다.
  • 메시지 스키마 검증과 저장 성공/실패 응답을 추가해 신뢰성을 높이세요.
  • 내부 발신자만 허용하도록 sender.id를 점검하세요.
-chrome.runtime.onMessage.addListener((message) => {
-  if (message.type === 'SET_TOKEN') {
-    chrome.storage.local.set({ 'token': message.token }, () => {
-      console.log('Token saved!', message.token);
-    });
-  }
-});
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  if (message?.type !== 'SET_TOKEN') return;
+  if (sender?.id && sender.id !== chrome.runtime.id) {
+    sendResponse({ ok: false, error: 'unauthorized_sender' });
+    return;
+  }
+  const token = typeof message.token === 'string' ? message.token.trim() : '';
+  if (!token) {
+    sendResponse({ ok: false, error: 'invalid_token' });
+    return;
+  }
+  chrome.storage.local.set({ token }, () => {
+    if (chrome.runtime.lastError) {
+      sendResponse({ ok: false, error: chrome.runtime.lastError.message });
+    } else {
+      sendResponse({ ok: true });
+    }
+  });
+  return true; // 비동기 응답 유지
+});

참고: 이슈 #85 요구사항에 따라 refreshToken도 함께 저장해야 하면 동일 스키마로 확장하세요.

🧰 Tools
🪛 GitHub Check: lint

[warning] 21-21:
Unexpected console statement

🤖 Prompt for AI Agents
In apps/extension/src/background.ts around lines 18-24, remove the console.log
that prints the token, validate the incoming message schema (ensure message.type
=== 'SET_TOKEN' and required fields like token and optional refreshToken are
strings), verify the sender by checking sender?.id === chrome.runtime.id before
proceeding, persist token (and refreshToken if present) via
chrome.storage.local.set and in its callback send a success or failure response
back to the sender (including an error message on failure), and ensure any
exceptions are caught and result in a failure response; expand schema and
storage to include refreshToken if Issue #85 requires it.

Comment on lines +2 to +10
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data.type === 'SET_TOKEN') {
chrome.runtime.sendMessage({
type: 'SET_TOKEN',
token: event.data.token,
});
}
}); No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

postMessage 출처 검증 없음 → 임의 사이트가 토큰 주입 가능

콘텐츠 스크립트가 모든 사이트(<all_urls>)에서 메시지를 수신합니다. origin 화이트리스트 및 페이로드 검증이 필요합니다.

-window.addEventListener('message', (event) => {
+const ALLOWED_ORIGINS = ['https://www.pinback.today', 'http://localhost:5173'];
+window.addEventListener('message', (event) => {
   if (event.source !== window) return;
-  if (event.data.type === 'SET_TOKEN') {
+  if (!ALLOWED_ORIGINS.includes(event.origin)) return;
+  if (event.data?.type === 'SET_TOKEN' && typeof event.data.token === 'string') {
     chrome.runtime.sendMessage({
       type: 'SET_TOKEN',
       token: event.data.token,
     });
   }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data.type === 'SET_TOKEN') {
chrome.runtime.sendMessage({
type: 'SET_TOKEN',
token: event.data.token,
});
}
});
const ALLOWED_ORIGINS = ['https://www.pinback.today', 'http://localhost:5173'];
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (!ALLOWED_ORIGINS.includes(event.origin)) return;
if (event.data?.type === 'SET_TOKEN' && typeof event.data.token === 'string') {
chrome.runtime.sendMessage({
type: 'SET_TOKEN',
token: event.data.token,
});
}
});
🤖 Prompt for AI Agents
In apps/extension/src/content.ts around lines 2 to 10, the window message
handler accepts messages from any origin and blindly forwards tokens to the
extension; add origin and payload validation: define a whitelist array of
allowed origins and immediately return unless event.origin is in that whitelist,
verify event.source === window (keep) and that event.data is an object with type
=== 'SET_TOKEN' and that event.data.token is a non-empty string (optionally
validate format/length), then only forward the sanitized token to
chrome.runtime.sendMessage; also drop or log unexpected messages and avoid
forwarding if validation fails.

Copy link
Member

@constantly-dev constantly-dev left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~~ 👍

chrome.storage.local.get('email', (result) => {
resolve(result.email);
});
chrome.storage.local.get('userEmail', (result) => resolve(result.userEmail));
Copy link
Member

Choose a reason for hiding this comment

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

storage이거 key email로 되어있지 않나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넹네! 이거 익스텐션 크롬스토리지랑 그냥 웹 로컬스토리지 각각의 키 구분할라고 (제가 헷갈려서..ㅎ)
익스텐션 쪽은 userEmail로 했씁니당!

Copy link
Member

Choose a reason for hiding this comment

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

그렇다면 익스텐션 axios interceptor 확인해보셔야 할 것 같아요! 이전에 인터셉터 설정할때 email로 통일한 기억이..!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발하라 개발 달려라 달려

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 온보딩 <-> 익스텐션 데이터 교류 로직

2 participants