◆ wport | pm_52

企業後台網域切割(wport.me → business.wport.me)PRD

企業後台網域切割(business.wport.me)

來源 doc/feature/pm_52/business-domain-split-prd.md

企業後台網域切割(wport.me → business.wport.me)PRD

產出 persona:CEO with PM Background(+ Research-Driven Technical PM 補技術裁決)。
目標 repo:W101-Webapps/recruitment 為主,apps/main-sitepackages/* 連帶)、W101-TalentSearchHubW101-AMS、邊緣路由(Cloudflare/nginx)。
關聯:pm_51(複製公開網址)、pm_40(company_pay)、pm_37pm_41pm_48(Enterprise API/CLI/MCP)、pm_39(第三方登入)。


1. 文件資訊

  • 文件類型:需求規格書(平台基礎建設;跨 repo;不產 Storybook
  • 適用對象:前端(main-site/recruitment/user)、後端(TSH/AMS)、DevOps、產品、QA、客服營運、行銷
  • 最後更新:2026/08/20
  • 版本:1.2.4
  • PRD 識別碼pm_52
  • 對應 Storybook:N/A(無新畫面;見 ADR-010)
  • Storybook(local-prd):N/A — 以本 PRD 為 SoT
  • Storybook(online):N/A
  • 文件入口doc/feature/pm_52/business-domain-split-prd.md
  • 建立日期:2026/08/18
  • 現況網域:企業後台 https://wport.me/recruitment/*
  • 目標網域:企業後台 https://business.wport.me/*(root path)

1.1 修訂紀錄

版本日期摘要
1.0.02026/08/18初版。依 W101-WebW101-TalentSearchHubW101-AMS 實查撰寫:確立 root path、集中式登入、cookie 為 session SoT、同源反向代理、302→301 兩階段切換;含 12 條 ADR 與跨 repo sync-fix list
1.1.02026/08/18依 Eric 回饋補強:§3.2 新增「切換企業模式常因 session 卡住」為驅動力並附誠實說明(切網域不會自動修好,是本 PRD 的 session 工作在修);§3.5 移除原 S1(與新條件重複)並新增 S6「切換成企業模式不再卡住」,其餘重新編號;§4.3 新增職缺/公司「留 vs 搬」對照表與同路徑不同 host 的注意事項
1.2.02026/08/19兩批修訂。(一)依 PM 決議調整驗收環境:移除 staging 相關規劃(staging-business 暫不做),改為 dev 切出 developers-business.wport.me 作為驗收場;連帶更新 §5.3、§9、§10.4、§11、ADR-008、§13.0、§13.2、§14。(二)依後端 spec 的 codebase 實查修正 13 處事實錯誤與遺漏:§6.2 SEC-DEEPLINK 全段重寫(承接頁行為必須改、去前綴發生在下游 app 而非 main-site、redirect_path 可能為空字串)、§8.1 補聊天室 origin 設定、§8.2 後端設定修正、§8.3 移除「必須同版上線」並改為 R1/R2 形狀、ADR-006 Consequences 補聊天室例外、§13.4 補 #4b/#5b/#5c/#6b/#8b 與漏列檔案。新增 §13.3 問題 #7–#11
1.2.12026/08/19修正 §8.3 與 §13.3 #9 對 target_origin 的說明:其作用是「讓契約顯式」而非「修 bug」(v1.2.0 誤寫)。新增 §13.3 問題 #12——buildRedirectPath() 回空字串導致通知點擊停在空白頁,屬既有缺陷、與本案無關、另案處理
1.2.22026/08/19修正兩處自相矛盾:(一) 檔頭版本/日期停在 1.1.0/08-18,與修訂紀錄脫節(本表也一併依版號排序)。(二) §8.2 與 §13.1 寫「禁止在後端字串保留 /recruitment 前綴」,但 §13.4 #5b 把同一件事標為「選配 cleanup」;兩處措辭改為區分「新寫的網址組裝一律不得帶前綴(硬規則)」與「既有 redirect_path 的去前綴=選配 cleanup(#5b)」。來源:後端 spec v1.2 的 Codex 覆審
1.2.32026/08/20依「修正擴散」原則回掃 v1.2.2 未掃到的同主題現場,修 3 處:(一) §7 ACT-6 仍把「後端 redirect_path 去前綴」寫成必做,與 §8.2/§13.4 #5b 的「選配」矛盾(v1.2.2 只改了 §8.2、§13.1 兩處),並移除已被 §8.3 廢止的「scope 決定 origin」措辭。(二) §13.3 問題清單列序 …9, 12, 10, 11,#12 插入時未重排,改為遞增。(三) §8.3 決議新增的 target_origin 欄位是 BE 工作,卻沒有對應的 §13.4 sync-fix 列 → 補 #5d。來源:本次 PRD 全文一致性回掃
1.2.42026/08/20§13.3 #6 補記 BR-042 與 pm_46 撞號business-rules.md v1.10.0 配到 BR-041,本 PRD 與 pm_46 都提案佔用 BR-042,且 pm_46 的 BR-039/040/041 直接覆蓋 pm_48/pm_41/pm_38 已配發的規則。回寫前須以 scripts/prd-lint.py --all(C10)確認號碼未被占用。來源:跨文件一致性檢查

2. 開發進度與設計來源

開發進度

  • 前端 PR #W101-Web):TBD
  • 後端 PR #W101-TalentSearchHub):TBD
  • 後端 PR #W101-AMS):TBD
  • 邊緣路由 / DNS:TBD(Cloudflare 設定,非 repo 內)

設計來源(本 PRD 的事實基礎=實查程式碼,非推測)

事實來源
企業後台為 Vite SPA,base: '/recruitment'apps/recruitment/vite.config.ts
路由表本身已是 root-relative(/dashboard/jobs…)apps/recruitment/src/constants/navLink.tssrc/router/**
靜態檔由 nginx 以 location /recruitment/ 服務nginx/prod/main-web.conf
API 走相對路徑VITE_API_BASE_URL 未設定 → axios baseURL undefined)apps/recruitment/src/utils/http.tsapps/recruitment/.env.production
同一 host 下的後端分流:/api→legacy、/v2/api→TSH、/ams-api→AMS、/chat/socket.io→WSnginx/proxy.dev.conf
token/x-company-id-enc 採 localStorage + 無 Domain 屬性的 cookie 雙寫packages/auth-session/src/browser-session-storage.ts
main-site(Nuxt)以 useCookie 寫同名 cookie,同樣未設 domainapps/main-site/app/utils/nuxt-session-storage.ts
跨 app 連結以 window.location.origin 拼接packages/utils/src/getUrl.ts
auth 完成後跳轉明確拒絕絕對 URL(open-redirect 防護)apps/main-site/app/utils/auth-redirect.ts
後端郵件/通知硬串 /recruitment/*TSH company-invitation.service.ts:30notification-link-token.service.ts:43;AMS company-event.service.ts:163
TSH CORS 由 CORS_ORIGIN 逗號清單控制、credentials: trueW101-TalentSearchHub/src/main.ts:80-93
WebSocket CORS 由 WS_CORS_ORIGIN 控制W101-TalentSearchHub/src/modules/chat/chat.gateway.ts:27-41
wport.me 走 Cloudflare;adminstaging 走 CloudFront;business.wport.me 尚未存在DNS 實查(2026/08/18)
Cloudflare Origin 憑證 SAN=*.wport.me, wport.me,效期至 2040/12nginx/wport-origin.pem

設計稿

  • Figma:N/A(無新畫面;既有 Header/Footer/登入頁不改版)

3. 摘要

3.1 功能概述

  • 功能名稱:企業後台網域切割(Business Domain Split)
  • 主要使用者角色:企業使用者(Owner/Admin/Manager/Member)=主要受影響者;求職者與訪客不應感受到任何變化
  • 核心目標
    1. 讓「公開內容」與「企業營運後台」在網域層級分家:wport.me = 對外品牌與 SEO 資產,business.wport.me = 企業工作台
    2. 讓兩者可獨立部署、獨立快取、獨立防護規則、獨立回滾,縮小每次上線的爆炸半徑
    3. 讓企業客戶有一個可以口頭傳達、可寫進採購文件、可加進公司防火牆白名單的專屬入口
    4. 為後續 admin.wport.me(內部)/business.wport.me(企業)/wport.me(公開)三層命名體系定調

3.2 為什麼現在做(策略理由)

驅動力說明不做的代價
銷售與品牌企業客戶問「後台網址是什麼」時,wport.me/recruitment 講不清楚也記不住;business.wport.me 可直接印在名片、報價單、教育訓練文件每次導入都要口述路徑;企業 IT 也難以白名單
部署解耦目前 Nuxt SSR 主站與企業 SPA 綁在同一組邊緣規則下;主站的 SEO/快取策略與後台的「絕不快取 index.html」互相牽制主站調快取=有機率打到後台;後台出事=主站一起回滾
防護分層分開後可對 business.* 單獨套 WAF/rate limit/未來的 Cloudflare Access 或 IP 允許清單(大型客戶常見要求)只能對整站套同一組規則,過鬆或過緊二選一
SEO 邊界後台路徑不再存在於公開站的 URL 空間,robots.txtnoindex 的責任邊界一刀切乾淨持續要靠 Disallow: /recruitment 這種「事後補丁」維持
既有痛點:切換成企業模式常卡住使用者從求職者切成人資/企業模式時,經常卡在 403、被迫重登、或切了沒生效。根因在 session 契約分散(localStorage 與 cookie 雙寫但以 localStorage 為準、切公司與導頁之間有 race window、多處直讀 localStorage 繞過抽象層),AUTH-SESSION.md §8 已列為已知清單但目前處置是「容忍小機率+401 直接登出」痛點會一直存在;而且不先把這條線收斂,切網域只會讓它更嚴重(多一個 origin=多一份 localStorage)
時機成本最低企業後台路由表已經是 root-relativebase 由 Vite 注入),現在切換幾乎不動路由;等 pm_45/pm_46 人才 CRM/獵頭模組進來後路徑只會更多每晚一季,要改的連結與已寄出的郵件就更多

關於「切換卡住」的誠實說明:網域切割本身不會修好這個問題 —— 它只是把問題推到必須面對的位置。真正修它的是本 PRD 的 session 工作:cookie 升為單一 SoT(ADR-004)、讀取順序反轉、切公司「先寫入再跳轉」的順序約束(§6.2 SEC-SWITCH)、以及 6 處直讀 localStorage 的修正(§13 sync-fix #2)。

換句話說:這條線遲早要收,切網域只是強迫我們一次做對。反過來說,若跳過這些工作直接切網域,business.wport.me 會有自己獨立的 localStorage,現況「偶爾卡住」會惡化成「幾乎每次切換都要重登」—— 所以 §13.2 才把這幾項全部標為 v1 Blocking。

3.3 SaaS Benchmark(公開可觀察之網域慣例,RD/PM 可自行驗證)

平台公開/求職端企業/雇主端形態
Indeedindeed.comemployers.indeed.com子網域
SEEKseek.com.autalent.seek.com.au子網域
104 人力銀行104.com.twpro.104.com.tw子網域
1111 人力銀行1111.com.twhr.1111.com.tw子網域
Greenhousegreenhouse.ioapp.greenhouse.io子網域
Leverlever.cohire.lever.co子網域
Stripestripe.comdashboard.stripe.com子網域
Shopifyshopify.comadmin.shopify.com子網域
LinkedInlinkedin.comlinkedin.com/talent路徑(少數)

結論:招募/B2B SaaS 的主流是「公開站 apex + 企業端獨立子網域,且子網域走 root path」。wport 選 business.* 而非 employers.*hr.*,理由是 wport 的企業端不只招募(含 CRM、方案購買、API 金鑰、未來獵頭),business 語意涵蓋面最廣且中英文都好念。

3.4 範圍界定

v1(必含)

  1. 新網域上線business.wport.me 提供企業後台 SPA(root path),DNS/TLS/邊緣路由到位
  2. 同源後端business.wport.me 下同時代理 /api/v2/api/ams-api/chat/socket.io(ADR-006)
  3. Session 跨站可用:登入狀態、選定公司(x-company-id-enc)在 wport.mebusiness.wport.me 之間一致(ADR-004)
  4. 舊網址轉址wport.me/recruitment/*business.wport.me/*(先 302、觀察期後轉 301;ADR-005),保留 query
  5. 相容轉址business.wport.me/recruitment/*business.wport.me/*(吃下漏改的舊絕對路徑)
  6. 前端跨站連結修正getUrl() 由 origin 拼接改為 app origin 解析;Header/Footer/UserDropdown/切換身分/登出等跨站入口全數改用解析結果(ADR-007)
  7. 登入回跳:未登入進 business.wport.me → 導 wport.me/auth?redirect=...,登入後跳回原目標(ADR-003)
  8. 通知 deep-linkscope=2(企業)解析到 business origin,維持「query 不放絕對 URL」的防護(ADR-007)
  9. 後端連結改用新網域:TSH 邀請信/通知 redirect_path、AMS 審核通知改讀 BUSINESS_DOMAIN
  10. SEO/爬蟲邊界business.wport.me/robots.txtDisallow: /X-Robots-Tag: noindex
  11. 直接讀 localStorage.getItem('token') 的 6 處改走 getToken()(否則新網域首次進站必定判定未登入;見 §13 sync-fix #2)

Out of scope

  • 求職者後台 /user 不搬(維持 wport.me/user/*;ADR-011)
  • ❌ 公開頁(職缺、公司、活動、內容頁)不搬、URL 不變 —— pm_51 複製出來的網址仍是 wport.me
  • admin.wport.me 不動
  • ❌ 不做企業後台改版/不改任何後台功能行為
  • ❌ 不做 Storybook(ADR-010)
  • ❌ 不改 JWT 結構、不改權限模型、不改 currentUserType 判定機制
  • ❌ 不導入 HttpOnly cookie + refresh token 輪替(列 v2;ADR-012)
  • ❌ 不換 email 寄件網域(SPF/DKIM/DMARC 完全不動)

3.5 成功條件

#條件量測
S1所有舊書籤/舊郵件連結可用轉址規則覆蓋 §5.4 全表;上線後 30 天 /recruitment/* 404 數 = 0
S2企業後台既有功能零行為變更既有 unit/Cypress 通過;聊天室 WebSocket 連線成功率不低於切換前
S3求職者端與公開頁零影響main-site Playwright smoke 全綠;GSC 索引數不下降
S4可秒級回滾邊緣轉址規則關閉即回舊行為(觀察期用 302 才成立;ADR-005)
S5企業後台工作階段數不下滑GA4:切換前後 7 日 business/recruitment 合計工作階段數差異 < 5%
S6切換成企業模式不再卡住 —— 切換後不需重新登入、不出現 403、選定公司立即生效dev 環境以「同時具備兩種身分且隸屬 2 間以上公司」的帳號跑完 §10.4 全部 Auth & Lifecycle 情境;上線後追蹤 auth.session_bridge_failed 與切換相關客訴數

4. 商業規則對齊

4.1 本功能使用到的業務規則

規則 ID規則摘要是否關鍵備註
BR-001完成 email 驗證才能登入❌ 否登入邏輯不變,只是入口網域固定在 wport.me/auth
BR-007 / BR-016 / BR-017成員角色與權限❌ 否權限判定不變;但 x-company-id-enc 必須跨站一致,否則會誤判權限(見 §10.4)
BR-014 / BR-015公司/職缺搜尋顯示條件❌ 否公開搜尋留在 wport.me,完全不受影響
BR-018郵件發送規則✅ 是郵件內連結網域變更(寄件網域不變);模板文案不變,僅 URL 來源改讀新 env

4.2 新規則提案

BR-042(提案):網域職責分界

  • wport.me(含 www):公開內容與求職者端。可被索引,是唯一對外分享的網域。
  • business.wport.me:企業後台。一律 noindexDisallow: /,不得放置任何需要被搜尋引擎收錄的內容。
  • admin.wport.me:內部後台,僅內部人員。
  • 任何對外複製/分享的網址一律指向 wport.me(含 pm_51 複製公開網址、社群分享、QR code)。
  • 新增任何 *.wport.me 子網域前,須評估其對 session cookie 的影響(見 ADR-012)。

建議於本 PRD 通過後回寫 business-rules.md(版本 → 1.11.0)。

4.3 衝突檢查

無硬衝突。

公開頁識別碼不受影響(明確聲明):公司/職缺公開頁的 enc_id 由 DB id 加密而來,與網域無關;公開頁 https://wport.me/companies/{enc_id}https://wport.me/job/{enc_id} 留在 apps/main-sitepages/companies/[id].vuepages/job/[id].vue),URL 一字不改。既有分享連結、SEO 索引、pm_50 的 Admin 開公司頁動線皆不受本次切換影響。

註:同一個 enc_id 也被當作 x-company-id-enc(選定公司)使用 —— 值不變,但載體變了:它必須跟 token 一樣升級為 Domain=.wport.me 的共享 cookie,否則兩站選定公司會不一致 → 403(見 ADR-004、§10.4)。

職缺/公司:哪些留、哪些搬(RD 開工對照表)

切割線沿著系統既有的「公開頁 vs 後台管理頁」分界走,不是沿著資料實體走:

實體留在 wport.me搬到 business.wport.me
職缺/job/{enc_id} 公開頁、/jobs 求職者搜尋列表/jobs 職缺管理、/jobs/create/edit-job//preview-job/view-job/
公司/companies/{enc_id} 公開頁、/companies 求職者搜尋列表/company-info/edit-company-info/preview-company-info/audit-certification
  • 判準(= BR-042):要被索引、要能對外分享的 → wport.me;要登入才看得到的 → business.wport.me
  • 企業端的「預覽頁」屬於後台/preview-job/preview-company-info 視覺上長得像公開頁,但它是需登入的後台路由,跟著搬
  • ⚠️ 同路徑不同 host 的組合:切換後會同時存在 wport.me/jobs(求職者搜尋)與 business.wport.me/jobs(職缺管理)。功能上無衝突,但這正是「後台連結必須明確帶 origin、不得用相對路徑或 location.origin」的直接理由(§13 sync-fix #3/#7)

唯一需要點名的語意衝突pm_51「複製公開網址」若以 window.location.origin 拼接,切換後會複製出 https://business.wport.me/job/{enc_id}(死連結)。pm_51 PRD 已將公開 URL 定義為常數 https://wport.me/job/{enc_id}https://wport.me/companies/{enc_id},實作必須照常數走,不得用 origin 拼接(見 §13 sync-fix #7)。


5. 資料實體與設定契約

Rule 31:本節描述「需要被追蹤/設定的資訊與用途」,不鎖死最終 env 變數名或 JSON 結構;命名供跨 repo 對齊參考。

5.1 實體列表

實體說明使用者
AppOriginMap各前端 app 的對外 origin 對照(main/business/user/admin)三個前端 app、packages/utils
SessionCookieContract跨站共享的 session cookie 契約(名稱、domain、屬性、TTL)packages/auth-session、main-site Nuxt、後端(僅讀)
BackendLinkDomains後端組信件/通知連結時使用的網域TSH、AMS
RedirectRuleSet邊緣轉址規則(舊 → 新)Cloudflare/nginx

5.2 語意定義(參考介面,非最終實作)

/** 各 app 對外 origin。由 build-time env 注入,禁止用 window.location.origin 推導 */
interface AppOriginMap {
  main: string;      // https://wport.me        (公開站/登入入口/求職者端)
  business: string;  // https://business.wport.me(企業後台)
  user: string;      // https://wport.me        (求職者後台,本次仍在 main 之下)
}

/** 跨站 session cookie 契約 */
interface SessionCookieContract {
  name: string;        // 'token' / 'x-company-id-enc'(可加環境前綴,見 ADR-008)
  domain: string;      // '.wport.me'  ← 本次新增,現況為 host-only
  path: '/';
  secure: true;        // 本次新增
  sameSite: 'Lax';     // 本次明確化(wport.me 與 business.wport.me 屬同 site,Lax 即可)
  httpOnly: false;     // v1 維持 false(前端需讀);v2 目標改 true,見 ADR-012
  maxAgeDays: 7;       // 維持現況(亦符合 Safari ITP 對 JS 寫入 cookie 的 7 天上限)
}

5.3 網域對照表(常數)

用途現況目標
公開站/求職者端/登入https://wport.me不變
企業後台https://wport.me/recruitment/*https://business.wport.me/*
內部後台https://admin.wport.me不變
Staging(公開站)https://staging.wport.me不變(本次不動;staging 不切企業網域,見下)
dev(公開站/開發機)https://developers.wport.me不變
dev(企業後台)N/Ahttps://developers-business.wport.me單層子網域*.wport.me 憑證只覆蓋一層,business.developers.wport.me 會憑證失效)。這是本案的驗收場

5.4 轉址對照表(v1 必須覆蓋)

通用規則https://wport.me/recruitment/(.*)https://business.wport.me/$1,保留 query string(fragment 由瀏覽器自動保留)。

舊網址新網址來源
/recruitment//recruitment/(SPA 內部再 redirect 到 /jobsHeader「企業刊登」、store-core defaultUrlWhenCompany
/recruitment/dashboard/dashboardweb-components header
/recruitment/jobs/jobs公司註冊完成頁
/recruitment/messages/messages通知中心、MessageButton
/recruitment/talent-pool/talent-poolHeaderNavLinks
/recruitment/account-center/account-center刪除帳號流程(apps/user)
/recruitment/account-center/api-keys/account-center/api-keysmain-site 企業功能介紹頁(pm_37/pm_41/pm_48 動線)
/recruitment/audit-certification/audit-certificationAMS 審核通知信
/recruitment/purchase-plan/purchase-planweb-components header(pm_40 動線)
/recruitment/invite/{enc_company_id}?token=…/invite/{enc_company_id}?token=…TSH 成員邀請信(pm_25)
/recruitment/deep-link?to=…&scope=2&…/deep-link?to=…&scope=2&…通知 deep-link(to 內含的 /recruitment/messages 亦需一併轉換,見 §7 ACT-6)

5.5 容量/限制

項目限制備註
轉址保留期永久(至少 24 個月不得移除)已寄出的郵件無法回收
Cookie TTL7 天(不變)對齊 Safari ITP 對 JS 寫入 cookie 的上限
觀察期(302 → 301)14 天ADR-005
新增/建立上限N/A本功能無 create 行為

6. 畫面區塊與資料需求

本功能不新增畫面,只改變既有元件產生連結與判斷狀態的方式。

6.1 區塊列表

區塊 ID區塊名稱說明
SEC-NAV共用 Header/Footer 跨站連結packages/common-componentspackages/web-componentspackages/utils/HeaderNavLinks
SEC-SWITCH切換身分(求職者 ⇄ 企業)store-core setSelectedCompany() 的導頁目標
SEC-AUTH登入回跳main-site /authauth-redirect.ts
SEC-DEEPLINK通知 deep-link 承接main-site notification-links → business /deep-link
SEC-SESSIONSession 橋接與跨站一致性auth-session cookie 契約 + 可見性重讀
SEC-EDGE邊緣路由與轉址Cloudflare/nginx

6.2 各區塊需求

SEC-NAV 共用 Header/Footer

  • 現況問題getUrl()window.location.origin 拼接,在 business 站會產生 https://business.wport.me/media-section/(404)
  • 要求:所有跨站連結改用 AppOriginMap 解析。呼叫端需標明目標 app,例如 getUrl('/media-section/', 'main')getUrl('/jobs', 'business')
  • 相容:未標明 app 時維持「同 origin 相對路徑」語意,避免一次改動全站
  • 涵蓋清單packages/utils/src/constants/HeaderNavLinks.tspackages/web-components/src/layouts/header.js(含 /recruitment//recruitment/dashboard/recruitment/purchase-plan)、footer.js${this.hostName}/recruitment)、packages/common-components/src/components/W101Header/**W101Footer/**

SEC-SWITCH 切換身分

  • 現況setSelectedCompany(encId)refreshToken()navigate('/recruitment/')
  • 目標:導向 AppOriginMap.business + '/';因為跨 origin,必須是 hard navigationwindow.location.assign),不可用 router.push
  • UI 要求:跨站跳轉期間顯示既有 loading/遮罩直到頁面卸載,避免使用者看到「按了沒反應」後重複點擊
  • 順序約束先確定新 JWT 與 x-company-id-enc 已寫入共享 cookie,再跳轉。否則新站讀到舊值 → 403(見 §10.4)

SEC-AUTH 登入回跳

  • 未登入直接開 business.wport.me/jobs → 導 https://wport.me/auth?redirect=%2Fjobs&app=business
  • 登入成功後 main-site 依 app 參數解析 origin 後 hard navigate
  • 安全redirect 參數維持只接受 / 開頭的相對路徑(現況防護不放寬);跨站以獨立的 app 參數表達,且 app 只接受列舉值(mainbusinessuser),任何非列舉值一律當 main(ADR-007)
  • 後端 notification-links API 回傳 redirect_pathidentity_scope(已存在)

實際呼叫鏈(v1.2.0 依實查修正)——main-site 消費 redirect_path,它只把值 encodeURIComponent 後包進 to 參數轉手(apps/main-site/app/pages/notification-links/index.vue:68-95),真正解析的是下游 app:

BE resolve → redirect_path
  → main-site 包成 /recruitment/deep-link?to=<redirect_path>&scope=2&…(硬跳)
  → apps/recruitment/src/views/deep-link/index.vue 解析 to
  • 承接頁 /deep-link 的行為必須改(v1.1.0 誤寫「不變」)。該檔兩個分支對前綴的假設不一致:

    分支現行程式給無前綴的 /messages
    A:不用切公司35-36to.replace(/^\/(recruitment|user)/, '')router.push剛好能動
    B:需要切公司20-27finalUrl = to + '?enc_id=',經 packages/common-components/src/stores/modules/global.ts:29location.href未去前綴❌ 跳到網站根目錄 → main-site catch-all pages/[...slug].vue:14-16 執行 router.replace('/')靜默導回首頁
  • 要求:兩個分支統一為「先去 /recruitment 前綴 → 再由 app 自己補上正確的 base/origin」。 apps/user/src/views/deep-link/ 是同款寫法,一併處理。

  • 效果:改完之後同一段程式對「帶前綴的舊值」與「不帶前綴的新值」都正確 → 前後端不需要同版上線(見 §8.3 修正)。

  • ⚠️ redirect_path 可能是空字串buildRedirectPath()PUBLIC_TARGET_TYPES (目前只有 COMPANY=公開公司頁)回傳 '',未知 target type 回 '/'。這兩條與 identity_scope 無關, 目標實際在 main origin。若前端照「scope=2 → business origin」硬對應會導錯。 目前後端只發 CONVERSATION / APPLICATION,此路徑休眠但存在 → 見 §13.3 問題 #9。

  • 既有的 token 水合與 switch-role 語意不變,改的只有路徑與 origin 的組法。

SEC-SESSION Session 橋接

  • Cookie 升為 session 的 SoT;localStorage 降為同源快取(ADR-004)
  • 進站時:先讀 cookie,再回填 localStorage(現有 getToken() 的 fallback 順序需反轉為 cookie 優先,否則兩站不同步時舊值會贏)
  • visibilitychangefocus 時重讀 cookie;與記憶體中的 token 不一致 → 重新水合或登出
  • 401 攔截 → signOut()(維持現況)

SEC-EDGE 邊緣路由

business.wport.me 需具備與現行 wport.me 相同的後端分流

Path目的地備註
/ 及所有前端路由recruitment SPA 靜態檔,try_files … /index.htmlindex.html 不得快取/assets/* 可長快取(檔名帶 hash)
/api/*legacy API 上游wport.me 現況
/v2/api/*TSH
/ams-api/*AMS
/chat/socket.io/WebSocket 上游proxy_http_version 1.1UpgradeConnection header;Cloudflare 需允許 WebSocket
/robots.txtUser-agent: * / Disallow: /

7. 使用者動作與後端需求

動作 ID動作名稱類型需要後端說明
ACT-1wport.me 登入後進企業後台導頁main-site 解析 business origin 後 hard navigate;session 由共享 cookie 帶過去
ACT-2直接開 business.wport.me/*(已登入)讀取讀共享 cookie → 水合 → 正常進站
ACT-3直接開 business.wport.me/*(未登入)導頁wport.me/auth?redirect=…&app=business
ACT-4切換公司/切換身分寫入+導頁switchRole 不變;新 JWT 必須寫入共享 cookie 後才跳轉
ACT-5登出寫入必須以相同 Domain 刪除 cookie,否則另一站殘留殭屍 session(見 ADR-004)
ACT-6點通知信/站內通知進企業訊息導頁三件事要分開看(v1.2.3 釐清):FE 必做(R1)——/deep-link 兩分支統一去 /recruitment 前綴,新舊值皆正確(§6.2、§13.4 #5);BE 已決議加——resolve 回應新增顯式 target_origin,FE 優先讀、讀不到才回退 identity_scope 推導(additive,§8.3、§13.4 #5d);BE 選配——既有 redirect_path 去前綴屬 cleanup,不做也不會壞(§13.4 #5b)
ACT-7點成員邀請信導頁TSH 邀請信 URL 改讀 BUSINESS_DOMAIN
ACT-8點審核/補件通知信導頁AMS 通知 URL 改讀 BUSINESS_DOMAIN
ACT-9開啟舊書籤 wport.me/recruitment/*導頁邊緣 302/301
ACT-10企業後台內複製公開網址(pm_51)UI一律輸出 wport.me 常數,不得location.origin
ACT-11企業後台聊天室WebSocket同源 /chat;若採跨源則需 WS_CORS_ORIGIN 加白

8. API/基礎建設 Hints(非最終設計)

8.1 前端設定(build-time)

設定用途現況目標
business origin產生企業後台連結新增(三個 app 都要,含 main-site 的 runtime config)
main originbusiness 站回主站的連結無(用 location.origin新增
SPA base pathVite base/recruitment/
cookie domainsession 共享範圍未設(host-only).wport.me
cookie 名稱前綴環境隔離由 env 提供(ADR-008)
聊天室連線 originsocket.io 連線目標VITE_CHAT_API_ORIGIN / NUXT_PUBLIC_CHAT_API_ORIGIN,各環境指向自己的網域新網域需要自己一份VITE_CHAT_API_ORIGIN=https://business.wport.me

⚠️ 聊天室不走 axios(v1.1.0 的 ADR-006 未涵蓋)。packages/common-components/src/stores/modules/chat.ts:322-334VITE_CHAT_API_ORIGINpackages/chat-core/src/chat.ts:89io(\${apiOrigin}${namespace}`) 連**完整網址**, 不是相對路徑。現況各環境皆已設(.env.staging.example:11,31https://staging.wport.me`、 .env.production.example:11,31https://wport.me),但 business 網域需要新增一份 build 設定, 漏設會 fallback 到硬編的舊網域。同款硬編 fallback 共三處 (common-components/.../chat.tschat-core 呼叫端、apps/main-site/app/stores/modules/chat.ts:119-120),建議一併清理。

8.2 後端設定

Repo設定用途
TSH企業側網域來源(新)邀請信 URL、公司資料管理頁 URL、通知 redirect_path。建議吃單一完整 URL(如 BUSINESS_BASE_URL)並在缺值時 throw——既有 getBaseUrl() 缺值會產出 https://undefined 並照樣送進信裡
TSHCORS_ORIGIN 追加 business origin(dev 與 prod 各一處)ADR-006 採同源,此項為保險。已實查staging.wport.me 不在 prod 清單內卻正常運作,證明同源請求不經 CORS
TSHWS_CORS_ORIGIN 追加 business origin同上。附註:chat.gateway.ts:39WS_CORS_CREDENTIALS === 'true' || true 恆為 true,該旗標實質失效(既有缺陷,非本案造成)
AMSBUSINESS_DOMAIN(新)審核/補件通知 URL

⚠️ 現行 TSH/AMS 皆以 getBaseUrl()getTalentUrl() 字串相接產生連結。本次不要求重構,但必須新增獨立的企業側網域來源

前綴的兩種情況要分開看(v1.2.2 釐清,原措辭「禁止把 /recruitment 前綴留在後端字串裡」與 §13.4 #5b 的「選配」互相矛盾):

情況規定
新寫的網址組裝(邀請信 acceptUrl、公司資料管理頁 URL、任何走 getBusinessUrl() 的地方)硬規則:不得帶 /recruitment 前綴。 新網域是 root path,帶了就是死連結
既有redirect_path 常數(notification-link-token.service.ts:43選配 cleanup(#5b)。 前端 R1 的去前綴邏輯對新舊兩種值皆正確,且 ADR-002 第二條相容轉址會接住;不做的唯一代價是後端字串殘留前綴

8.3 Read/Write 契約

  • 無新 API endpoint。所有既有 API 的路徑、payload、錯誤碼一律不變。
  • 唯一契約變更:通知 API 回傳的 redirect_path 語意由「main-site 可直接使用的絕對路徑」改為「目標 app 內的相對路徑」。此為語意變更(欄位名與型別皆不變,值的意思變了)。

v1.2.0 修正——不需要同版上線。 v1.1.0 此處寫「前後端必須同版上線」,與 §13.0 P3、§10.4 (皆稱 BE 可晚於 FE、相容轉址會接住)互相矛盾。正確的是後者。落地形狀:

做什麼完成後
R1FE§6.2 SEC-DEEPLINK 的兩分支統一去前綴,新舊兩種值都正確解耦完成
R2BEredirect_path 去掉 /recruitment 前綴後端字串不再殘留前綴
R3不需要(R1 的邏輯對兩種輸入皆正確,可永久保留)

R1 未上線前不得執行 R2(否則 SEC-DEEPLINK 表中的分支 B 會靜默失敗)。R1 上線後 R2 可隨時做, 甚至不做也不會壞(舊值由 ADR-002 的相容轉址接住),故 R2 屬 cleanup 而非阻塞項。

請求欄位與回應欄位的方向相反:請求欄位因 forbidNonWhitelisted: true 必須 BE 先接受; 回應欄位的消費者是 FE,寬容層要放在 FE。

新增回應欄位(v1.2.0,依 §13.3 問題 #9 決議):通知 resolve 回應的 target 物件新增一個 顯式的目標 origin 欄位,讓前端不必從 identity_scope 推導要拼哪個網域。

項目內容
建議欄位名target_origin(實際命名由 BE spec 與 FE 對齊後定案)
值域mainbusinessuser(對齊 §5.2 AppOriginMap 的鍵)
phaseexpand(additive)。舊前端不讀此欄位也不會壞
為什麼需要讓「目標在哪個網域」由後端明說,不靠前端從 identity_scope 推導。 identity_scope 表達的是「收件人以什麼身分行動」,與「目標頁在哪個 app」是兩件事,只是目前實際在用的兩種通知(CONVERSATIONAPPLICATION)剛好一致
不是為了什麼⚠️ 這個欄位不修任何現存 bug。目前的推導都正確;buildRedirectPath()PUBLIC_TARGET_TYPES 回空字串的問題,加了此欄位也不會變好(見 §13.3 問題 #12,屬既有缺陷、另案處理)
取捨屬「現在多做一點讓契約清楚」的等級,不是必要項。加不加都不會壞;已決議加
相依與 §6.2 SEC-DEEPLINK 的 R1 一起上;FE 優先讀此欄位,讀不到才回退原推導

8.4 Auth/合約最小集

項目要求
Auth沿用現行 JWT Bearer;不新增 token 種類
TTLJWT 沿用後端現行效期;cookie 7 天(不變)
Revoke沿用:登出清 cookie+localStorage;401 攔截強制 signOut()
過期行為cookie 過期 → 兩站同時失效(因共享);行為與現況一致
Idempotency轉址為 GET/冪等;switchRole 沿用現況
即時權限重查不變(getMePermissions() 進企業後台時抓)
跨站一致性cookie 為 SoT;x-company-id-enc 亦需同 domain,否則兩站選定公司可能不同 → 403

9. 導航/交付 Map

  • Storybook:N/A
  • dev 驗收https://developers.wport.mehttps://developers-business.wport.me(內網,192.168.50.143
  • Productionhttps://wport.mehttps://business.wport.me
  • 驗收方式:dev 全流程手測(§10.4 全表)+ main-site Playwright smoke + recruitment 既有 unit/Cypress
  • ⚠️ dev 驗不到的一項:邊緣轉址規則。prod 走 Cloudflare、dev 走內網 nginx,是兩套設定。轉址仍為 prod 首次上路,靠 ADR-005 的「先 302、關規則即秒回滾」管理風險(見 §13.3 問題 #7)

10. Flowcharts

10.1 User Flow — 企業使用者主動線

flowchart TD
    A["使用者開啟 wport.me"] --> B{已登入?}
    B -- 否 --> C["wport.me/auth 登入"]
    C --> D["寫入共享 cookie<br/>Domain=.wport.me"]
    B -- 是 --> D
    D --> E{有企業身分?}
    E -- 否 --> F[留在 wport.me 求職者動線]
    E -- 是 --> G[選定公司 setSelectedCompany]
    G --> H[switchRole 取得新 JWT<br/>寫入共享 cookie]
    H --> I["hard navigate 至<br/>business.wport.me/"]
    I --> J[business 讀 cookie 水合 session]
    J --> K{cookie 有效?}
    K -- 是 --> L["進入 /jobs 企業後台"]
    K -- 否 --> M["導回 wport.me/auth<br/>帶 redirect + app=business"]
    M --> C

    N["開啟舊書籤<br/>wport.me/recruitment/jobs"] --> O["邊緣 302/301"]
    O --> P["business.wport.me/jobs"]
    P --> J

10.2 System Flow — Session 跨站橋接

sequenceDiagram
    participant U as 使用者瀏覽器
    participant M as wport.me (Nuxt main-site)
    participant E as 邊緣 (Cloudflare/nginx)
    participant B as business.wport.me (recruitment SPA)
    participant API as TSH / AMS

    U->>M: 登入
    M->>API: POST 登入
    API-->>M: JWT
    M->>U: Set-Cookie token(Domain=.wport.me, Secure, SameSite=Lax)
    Note over M,U: 同時寫 localStorage(僅同源快取)

    U->>M: 選定公司
    M->>API: POST /switchRole (帶 x-company-id-enc)
    API-->>M: 新 JWT (currentUserType=2)
    M->>U: 更新共享 cookie(token + x-company-id-enc)
    M-->>U: location.assign(business origin)

    U->>E: GET business.wport.me/ (自動帶 .wport.me cookie)
    E-->>U: SPA index.html(no-store)
    U->>B: 啟動
    B->>B: getToken(): cookie 優先 → 回填 localStorage
    B->>API: GET /v2/api/... (同源,帶 Bearer)
    API-->>B: 200

    Note over U,B: 登出時:以相同 Domain 刪除 cookie<br/>兩站下次請求皆 401 → signOut()

10.3 Navigation — 網域職責

flowchart LR
    subgraph Public["wport.me(公開・可索引)"]
        Home["首頁/職缺/公司頁"]
        Auth["/auth 登入(唯一入口)"]
        UserApp["/user/* 求職者後台"]
    end
    subgraph Business["business.wport.me(企業・noindex)"]
        Jobs["/jobs 職缺管理"]
        Msg["/messages"]
        Acc["/account-center"]
    end
    subgraph Admin["admin.wport.me(內部)"]
        AdminApp[Site Admin]
    end

    Auth -->|"登入後・hard nav"| Jobs
    Jobs -->|"複製公開網址 pm_51(常數)"| Home
    Jobs -->|"切回求職者模式"| UserApp
    Public -.->|"301 /recruitment/*"| Business
    AdminApp -->|"開公司公開頁 pm_50"| Home

10.4 Edge Case Coverage

類別情境系統行為UI 回饋可重試資料一致性備註
Network & Performance跨站跳轉時網路中斷停在原站既有 loading 需有逾時解除Yes未寫入不可讓遮罩永久卡住
Network & Performance邊緣轉址增加一次 RTT單跳(不得產生鏈式轉址)無感/recruitment/x → business /x 必須一次到位
Network & Performanceindex.html 被 CDN 快取舊版 SPA 載到新版 API白畫面/404 chunkYesindex.html 一律 no-store
Network & PerformanceWebSocket 升級失敗聊天無法連線既有重連提示Yes邊緣需帶 UpgradeConnection
Data & Input舊網址帶 query/中文編碼轉址保留原 query無感fragment 由瀏覽器保留
Data & Inputredirect 參數帶絕對 URL一律視為非法 → 回 /無提示現況 open-redirect 防護不放寬
Data & Inputapp 參數非列舉值視為 main無提示ADR-007
Data & Input漏改的 business.wport.me/recruitment/x相容轉址 → /x無感保底規則
User Interruption跳轉途中關閉分頁無副作用Yescookie 已寫入,下次進站即生效
User Interruption兩站分頁同時開著兩份 localStorage 各自快取cookie 為 SoT見下列 Auth 情境
Auth & LifecycleA 站登出、B 站分頁仍開著共享 cookie 已刪 → B 站下次 API 401 → signOut()visibilitychange 時亦會重讀 cookie 提前發現導回登入Yes不會出現「看起來還登入、實際 401」的長期狀態ADR-009;不追求毫秒級即時廣播
Auth & LifecycleA 站切公司、B 站分頁仍開著B 站重讀 x-company-id-enc → 不一致則 reload頁面重載Yes以 cookie 為準不重載會 403
Auth & Lifecycle升級當下既有使用者持有 host-only 舊 cookie進站時先刪 host-only 同名 cookie,再以 .wport.me 重寫無感避免同名雙 cookie 讀取順序不確定必做,見 §13.0 Migration
Auth & LifecyclelocalStorage 有 token、cookie 已失效以 cookie 為準 → 判定未登入導登入Yes反轉現行讀取順序否則 B 站會用過期快取
Auth & LifecycleSafari ITP 限制 JS 寫入 cookie 為 7 天現行 TTL 即 7 天,無退化不需額外處理
Auth & Lifecycle第三方 cookie 封鎖不受影響wport.mebusiness.wport.me同 site,為 first-party;勿誤設 SameSite=None
Logical Inconsistency直接讀 localStorage.getItem('token') 的程式碼新站首次進站必判未登入誤導回登入6 處必改(§13 sync-fix #2)
Logical InconsistencygetUrl() 用 origin 拼接business 站產生 business.wport.me/media-section/ 404死連結SEC-NAV 必改
Logical Inconsistencypm_51 用 origin 拼公開網址複製出 business.wport.me/job/x 死連結使用者分享後才發現必須用常數
Logical Inconsistency後台內以相對路徑指向公開頁(router.push('/job/{enc_id}')openCompany('/companies/{enc_id}')root path 後解析成 business.wport.me/job/xbusiness.wport.me/companies/x —— 看起來像公開頁,實際是 business 站不存在的路由404/空白切換前有 /recruitment 前綴時不會長得像公開頁,切換後才會混淆;一律改用 main origin 解析
Logical Inconsistencydev/staging 與 prod 共用 .wport.me cookiedev 會收到 prod token(反之亦然)難以察覺的狀態污染ADR-008 cookie 名稱環境前綴
Logical Inconsistency後端仍回舊 redirect_path 而前端已升級deep-link 導到 business.wport.me/recruitment/messages相容轉址接住因此相容轉址是必要不是保險
Auth & Lifecycle企業客戶防火牆未放行新網域後台完全打不開企業端回報「壞掉」上線前 2 週主動通知既有付費客戶(§13 sync-fix #10)

11. Mock Data Schema

  • 無 Storybook:不需要 mock schema。
  • 測試資料要求:dev 需至少一組「同時具備求職者+企業身分、且隸屬 2 間以上公司」的帳號,用於驗證切換公司的跨站一致性。

12. ADR(架構決策紀錄)

ADR-001: 企業後台使用獨立子網域 business.wport.me

  • Status: Accepted
  • Context: 企業後台目前掛在公開站的 /recruitment 路徑下,與 SEO 資產、快取策略、防護規則混在同一個 URL 空間;企業客戶也難以口頭傳達入口。可選:(a) 維持路徑;(b) 獨立子網域;(c) 獨立 apex(如 wport-business.com)。
  • Decision: 採 (b) 獨立子網域 business.wport.me
  • Consequences:
    • ✅ 與 admin.wport.me 形成一致命名體系;Cloudflare Origin 憑證 SAN 已含 *.wport.me不需新憑證
    • ✅ 與 wport.me 屬同 site,cookie 可共享 → SSO 成本最低;SameSite=Lax 即可運作
    • ⚠️ 同 site 也代表沒有真正的信任邊界*.wport.me 任一子網域的 XSS 都可讀到 session cookie(見 ADR-012)
  • Alternative rejected: (c) 獨立 apex —— 會變成跨 site,cookie 無法共享,必須自建 SSO 交接(一次性 ticket)+ SameSite=None,成本高出一個量級,且對 7 人團隊不成比例。(a) 維持路徑 —— 無法達成部署/防護解耦,且不符合 §3.3 業界慣例。
  • Decision maker: Eric | Date: 2026-08-18

ADR-002: 新網域採 root path,/recruitment 前綴只保留為相容轉址

  • Status: Accepted
  • Context: 兩種做法:(a) business.wport.me/recruitment/*(只換 host,改動最小);(b) business.wport.me/*(同時去掉前綴)。實查後發現 recruitment 的路由表本身已是 root-relativeNavLink 全部是 /dashboard/jobs…),/recruitment 只由 Vite base 注入;app 內硬寫 /recruitment 的地方僅 9 處(多為 public 圖檔路徑)。
  • Decision: 採 (b)。同時建立兩條相容轉址:wport.me/recruitment/*business.wport.me/*,以及 business.wport.me/recruitment/*business.wport.me/*
  • Consequences:
    • business.wport.me/jobs 語意乾淨,符合業界慣例
    • ✅ 第二條相容轉址讓「後端還沒改完 env」也不會壞 → 前後端可分批上線,不必同一秒切換
    • ⚠️ 需同步處理 public 靜態資產路徑(/recruitment/icon/...)與 Vite base
  • Alternative rejected: (a) —— business.wport.me/recruitment/jobs 語意重複,且等於把技術債搬家;既然路由表本來就 root-relative,這次不做以後成本只會更高。
  • Decision maker: Eric | Date: 2026-08-18

ADR-003: 登入入口集中於 wport.me/auth,business 站不複製登入頁

  • Status: Accepted
  • Context: 登入/註冊/第三方登入(pm_39,含 Apple Service ID 的 redirect_uri = https://wport.me)全部實作在 main-site(Nuxt)。若 business 站要自帶登入,等於在 Vite SPA 複製一套 auth,並新增 OAuth redirect URI 與 Apple Service ID 設定。
  • Decision: business 站不做登入頁。未登入 → 導 wport.me/auth?redirect=…&app=business,登入後跳回。
  • Consequences:
    • ✅ 第三方登入設定(Google/Apple)完全不需異動
    • ✅ 只有一份 auth 實作要維護;BR-001 等規則不會分岔
    • ⚠️ 企業使用者「輸入 business 網址 → 被彈回 wport.me 登入 → 再跳回」會有一次跨站往返,需靠 loading 與文案讓過程可理解
  • Alternative rejected: business 自建登入頁 —— 為了省一次跳轉,換來兩套 auth、兩組 OAuth 設定與長期分岔風險。
  • Decision maker: Eric | Date: 2026-08-18

ADR-004: Session 以 Domain=.wport.me 共享 cookie;cookie 升為 SoT,localStorage 降為快取

  • Status: Accepted
  • Context: 實查現況:browserSessionStoragenuxtSessionStorage 都同時寫 localStorage 與 cookie,但 cookie 未設 Domain(host-only),且讀取順序是 localStorage 優先、cookie fallback。localStorage 是 per-origin,business.wport.me 會是全新的空間 → 不處理則跨站必定要求重新登入。
  • Decision:
    1. token 與 x-company-id-enc 的 cookie 一律加 Domain=.wport.me; Path=/; Secure; SameSite=Lax
    2. 讀取順序反轉為 cookie 優先,localStorage 僅作同源快取
    3. 刪除 cookie 時必須帶相同的 Domain,否則刪不掉 → 另一站殘留殭屍 session
    4. 升級時一次性清掉舊的 host-only 同名 cookie(見 §13.0)
  • Consequences:
    • ✅ 跨站 SSO 免後端改動、免新 API
    • ⚠️ 同名 cookie 若 host-only 與 domain 版本並存,document.cookie 讀取順序不確定 → 必須一次性清理
    • ⚠️ cookie 會被送往所有 *.wport.me(含 apidev-apistagingdevelopersadmin)→ 見 ADR-008、ADR-012
  • Alternative rejected: 一次性 handoff ticket 交接(各站 host-only cookie)—— 安全性較佳但需後端新增簽發/兌換 API、跨站登出要另做,成本不成比例;列為 v2 與 ADR-012 綁定評估。
  • Decision maker: Eric | Date: 2026-08-18

ADR-005: 舊網址先 302 觀察 14 天,再轉 301

  • Status: Accepted
  • Context: 301 會被瀏覽器永久快取。若上線後發現重大問題想回滾,已經吃過 301 的使用者瀏覽器仍會自行跳到新網域,等同無法回滾。
  • Decision: 切換首 14 天使用 302(暫時轉址);穩定後改 301(永久轉址)並長期保留。
  • Consequences:
    • ✅ 觀察期內關掉邊緣規則即可秒級回滾(達成 S4)
    • ⚠️ 302 期間 SEO 不傳遞權重 —— 本案不影響,因 /recruitment 本就 Disallow+非索引資產
    • ⚠️ 需要一張明確的「轉 301」工單,否則會一直停在 302
  • Decision maker: Eric | Date: 2026-08-18

ADR-006: API 與 WebSocket 走同源反向代理,不採 CORS 跨源

  • Status: Accepted
  • Context: recruitment 的 axios baseURL 實際為 undefined(VITE_API_BASE_URL 未設定),所有請求都是相對路徑,且 withCredentials: true。若讓 business 站跨源打 wport.me API,需要:改所有請求的 base、TSH CORS_ORIGINWS_CORS_ORIGIN 加白、每個非簡單請求多一次 preflight、且 credentials: true 下不得使用萬用字元來源。
  • Decision: 在 business.wport.me 下複製現行的後端分流(/api/v2/api/ams-api/chat/socket.io),維持同源。
  • Consequences:
    • 走 axios 的 API 層零改動;無 preflight 額外延遲;無 CORS 設定漂移風險
    • ⚠️ 例外:聊天室不走 axios(v1.2.0 補)。它讀 VITE_CHAT_API_ORIGIN 連完整網址, 所以「同源」不是自動達成的,而是要靠新網域的 build 把該值設成 https://business.wport.me。 漏設會 fallback 到硬編的舊網域 → 聊天室連不上。詳見 §8.1
    • ✅ 設定正確時 WebSocket 同源,WS_CORS_ORIGIN 追加僅為保險(已實查佐證:staging.wport.me 不在 prod 清單內卻正常運作)
    • ⚠️ 邊緣設定要維護兩份(wport.mebusiness.wport.me)→ 要求以同一份 template 產生,避免漂移
    • 🛟 仍建議把 https://business.wport.me 加進 CORS_ORIGINWS_CORS_ORIGIN 作為保險,成本近乎零
  • Alternative rejected: 跨源 + CORS —— 改動面更大、延遲更差、且把安全設定分散到環境變數裡。
  • Decision maker: Eric | Date: 2026-08-18

ADR-007: 跨站跳轉一律「相對路徑 + app 代號」,query 內禁止絕對 URL

  • Status: Accepted
  • Context: apps/main-site/app/utils/auth-redirect.ts 目前明確拒絕絕對 URL 與 protocol-relative //,這是既有的 open-redirect 防護。跨站之後,redirect 目標確實需要落在另一個 origin,最直覺的做法是「允許絕對 URL 但比對白名單」。
  • Decision: 不放寬 redirect 參數的限制(仍只接受 / 開頭相對路徑)。跨站目標以獨立的 app 列舉參數表達(mainbusinessuser),由前端在最後一刻用 AppOriginMap 解析成 origin;非列舉值一律 fallback 到 main。通知 deep-link 沿用既有的 scope 參數,語意相同。
  • Consequences:
    • ✅ open-redirect 防護維持「預設拒絕」,不需維護 URL 白名單比對邏輯(正規化、大小寫、port、尾綴等常見繞過點全部不存在)
    • ✅ 新增網域時只要擴充 AppOriginMap,不必改防護邏輯
    • ⚠️ 呼叫端需同時傳 path 與 app,既有呼叫點要逐一檢視
  • Alternative rejected: 允許絕對 URL + origin 白名單 —— 白名單比對是 open-redirect 最常見的破口來源,收益只有「少傳一個參數」。
  • Decision maker: Eric | Date: 2026-08-18
  • Status: Accepted
  • Context: cookie 一旦設為 Domain=.wport.me,就會同時送到 staging.wport.medevelopers.wport.medevelopers-business.wport.medev-api.wport.meapi.wport.meadmin.wport.me。實查 DNS:staging.wport.meadmin.wport.me(CloudFront)與 apidev-api.wport.me(Cloudflare)皆公開可達developers.wport.me 為內網。prod token 被送進非 prod 環境(或反之)會造成難以察覺的狀態污染——該站讀到另一個環境的 session,兩邊資料庫與使用者 id 不同,症狀難以追查。 本案讓這件事變得更相關:v1.2.0 新增的 developers-business.wport.me 同樣在 .wport.me 底下,dev 與 prod 會互相污染。
  • Decision: cookie 名稱由環境變數提供前綴(例如 prod token/dev dev_token/staging stg_token),三個前端 app 與 main-site SSR 一致。prod 維持現名以免既有 session 全數失效。
  • Consequences:
    • ✅ 環境間 session 不再互相污染
    • ⚠️ 讀寫 cookie 的地方必須全部走 SessionStorage 介面(現況已有此規範,但有 6 處違規直讀 —— 正好一併修)
    • ⚠️ dev/staging 使用者需重新登入一次(可接受)

v1.2.0 補述(依 §13.3 問題 #11 決議)

  • 殘留風險:prod 維持原名 token,所以忘了替新環境設前綴時,該環境會靜默讀到 prod 的 session—— 失敗方向是最差的那邊(不會報錯,只會某天有人發現「dev 站怎麼讀到正式站資料」)。
  • 對策(已決議):把「新增任何 *.wport.me 環境時必須設定 cookie 名稱前綴」列為部署檢查項, 不靠人記得。未採「每個環境都加前綴(含 prod)」的 fail-closed 版本,因為代價是正式站使用者全部登出一次。
  • 這是治標,不是治本。根本原因是「所有環境共用 wport.me 這個註冊網域」,治本有二: ①非 prod 改用另一個註冊網域;②不共用 cookie,改一次性交接票(ADR-004 已評估為「成本高一個量級」)。 兩者皆列 v2,與 ADR-012 一起評估。
  • ⚠️ 釐清一個常見誤解:ADR-012 的 v2 目標(HttpOnly + refresh token)解不了本問題。 HttpOnly 擋的是「XSS 讀得到 token」,不是「cookie 送到別的環境」。加了 HttpOnly, Domain=.wport.me 的 cookie 照樣送到所有 *.wport.me。兩件事要分開處理。
  • Decision maker: Eric | Date: 2026-08-18

ADR-009: 跨站狀態同步採「cookie 為準 + 可見性重讀 + 401 攔截」,不做即時廣播

  • Status: Accepted
  • Context: 現行跨分頁同步靠 storage event(AUTH-SESSION.md §7.7),但 storage event 與 BroadcastChannel 都是 same-origin,切站後 wport.mebusiness.wport.me 之間完全收不到彼此的廣播。
  • Decision: 不追求即時。採三層:(1) cookie 為 SoT;(2) visibilitychangefocus 時重讀 cookie,與記憶體狀態不一致則重新水合或登出/reload;(3) 既有 401 攔截 → signOut()。同 origin 內的 storage event 行為維持不變。
  • Consequences:
    • ✅ 零後端成本,覆蓋「另一站登出/切公司」的實際使用情境(使用者切回分頁時必然觸發 visibilitychange
    • ⚠️ 使用者若把兩站分頁並排放著不切換,最長會延遲到下一次 API 呼叫才發現失效 —— 明確接受
  • Alternative rejected: 後端 session 版本號輪詢 —— 為低頻情境增加持續性請求量,不划算。
  • Decision maker: Eric | Date: 2026-08-18

ADR-010: 不做 Storybook

  • Status: Accepted
  • Context: 本次無新畫面,改動集中在路由、設定、連結產生方式。
  • Decision: 不產 Storybook;以本 PRD + dev 驗收為準。
  • Consequences: §1 Storybook 欄位標 N/A;驗收改以 §10.4 edge case 清單逐條手測。
  • Decision maker: Eric | Date: 2026-08-18

ADR-011: 求職者後台 /user 本次不搬

  • Status: Accepted
  • Context: apps/user 同樣是掛在 wport.me/user/* 的 SPA,理論上可一併切到 my.wport.me 之類。但求職者端與公開站的動線高度交織(看職缺 → 投遞 → 我的應徵),拆開會增加跨站跳轉次數,反而傷 C 端轉換率。
  • Decision: 本次只切企業後台。/user 維持現狀。
  • Consequences:
    • ✅ 範圍可控,C 端零風險
    • ✅ 本次建立的 AppOriginMap/cookie 契約已預留 user 欄位,未來要切成本很低
    • ⚠️ 短期內網域體系不對稱(企業有專屬網域、求職者沒有)—— 可接受,因為兩者的商業角色本就不同
  • Decision maker: Eric | Date: 2026-08-18
  • Status: Accepted(附條件)
  • Context: session cookie 目前不是 HttpOnly(前端需讀取),改為 Domain=.wport.me 後,*.wport.me 上任一應用的 XSS 都能讀到 token —— 這是真實的安全退化,不能因為「同 site 所以沒差」而略過。實查 DNS 現況:n8n.wport.medevelopers.wport.mestorybook.wport.medev-admin.wport.me 解析到內網 IP(192.168.50.143,非公開可達);公開可達的是 wwwmediaapidev-apiadminstaging
  • Decision:
    1. v1 接受共享 cookie,因為目前公開可達的子網域全部是自家可控資產
    2. 立守則(併入 BR-042):任何第三方或低信任應用(含 n8n、外掛、實驗性服務)不得掛在 *.wport.me 的公開可達位置;若必須,須先移到獨立 apex 或加 Cloudflare Access
    3. devstaging 以 cookie 名稱前綴隔離(ADR-008)
    4. v2 目標:token 改 HttpOnly + 短效 access token + refresh 輪替,屆時 ADR-004 的共享 cookie 可退場
  • Consequences:
    • ⚠️ 明確記錄「這是用安全換交付速度」,不假裝沒有代價
    • ✅ 守則讓風險可管理而非放任
    • 📌 v2 若採一次性 handoff ticket,可同時解掉 ADR-004 的共享 cookie 與本 ADR 的爆炸半徑
  • Decision maker: Eric | Date: 2026-08-18

13. 實作備註

13.0 Migration / 相容 / Rollback(Rule 23 必填)

既有使用者的 session 遷移

步驟動作為什麼
M1進站時若偵測到 host-only 的同名 cookie,先以無 Domain 的方式刪除,再以 Domain=.wport.me 重寫同名雙 cookie 時 document.cookie 讀取順序不確定,會造成「時好時壞」的登入狀態
M2讀取順序改為 cookie → localStorage讓兩站以同一份事實為準
M3不強制登出既有使用者prod cookie 名稱不變(ADR-008 只對非 prod 生效),M1 完成即可無縫接續
M4dev/staging 使用者需重新登入一次cookie 名稱加前綴

M1 為冪等操作,重複執行無副作用;三個 app 都要做(誰先被開啟誰先遷移)。

分階段上線

Phase內容對外可見?可獨立回滾?
P0cookie 契約升級(M1–M2)、getUrl() 改 AppOriginMap(預設值=現行同源,行為不變)、6 處直讀 localStorage 改走 getToken()app 參數解析邏輯❌ 完全無感✅ 純前端,可單獨回滾
P1business.wport.me DNS + 邊緣路由上線,與 wport.me/recruitment 雙活⚠️ 僅知道網址的人✅ 撤 DNS 即可
P2前端切換所有企業後台入口至 business origin;wport.me/recruitment/* 302✅ 正式切換✅ 關掉轉址規則
P3TSH/AMS 郵件與 redirect_path 改讀 BUSINESS_DOMAIN✅ 新寄出的信✅ 改回 env(舊值仍被相容轉址接住)
P4觀察 14 天後 302 → 301⚠️ 301 後回滾困難(故需 P2–P3 全綠才執行)

關鍵:P3 之所以可以晚於 P2,是因為 ADR-002 的第二條相容轉址(business.wport.me/recruitment/*/*)會接住後端還沒改的舊路徑。這是刻意設計的安全網,不是冗餘。

Rollback 決策點

  • P2 出問題 → 關閉邊緣轉址規則 + 前端 origin 設定改回 main(一次部署)
  • P3 出問題 → 後端 env 改回(郵件連結退回舊網址,仍可用)
  • P4 之後 → 已吃 301 的瀏覽器無法回滾,故 P4 需獨立核准

13.1 禁止事項

  • ❌ 用 window.location.origin 產生任何跨 app 的連結
  • ❌ 在 redirect query 放絕對 URL(ADR-007)
  • ❌ 為了跨站把 cookie 設成 SameSite=None —— 兩者屬同 site,不需要,且是安全退化
  • ❌ 直讀 localStorage.getItem('token')(一律走 SessionStorage 介面)
  • ❌ 讓 business.wport.meindex.html 進 CDN 快取
  • ❌ 在新寫的後端網址組裝裡帶 /recruitment 前綴(新網域是 root path,帶了即死連結)
    • ⚠️ 這條不涵蓋既有的 redirect_path 常數——那一處是選配 cleanup(§13.4 #5b),不是禁止事項。兩者的差別見 §8.2 的對照表
  • ❌ 在 business.wport.me 放任何需要被索引的內容
  • ❌ 直接上 301(必須先 302 觀察)

13.2 一致性表(Stage 5/6)

Field mapping

欄位/概念PRDW101-WebTSH/AMSSoTVersion
business origin待新增(build env)待新增 BUSINESS_DOMAINPRD §5.3v1
main origin待新增(取代 location.origin既有 FRONTEND_DOMAINPRD §5.3v1
cookie token✅ 加 Domain/Secure/SameSiteauth-sessionnuxt-session-storage僅讀 Bearer,不受影響PRD §5.2v1
cookie x-company-id-enc✅ 同上同上header 語意不變PRD §5.2v1
SPA base path/vite.config.ts basePRD §8.1v1
redirect_path(通知)目標 app 內相對路徑main-site 解析TSH 產生PRD §8.3v1
scope / app 參數1=求職者/2=企業;app 為列舉auth-redirect.tsTSH identity_scopePRD §6.2v1

Constants

常數SoTVersion
企業後台 origin(prod)https://business.wport.mePRDv1
企業後台 origin(dev)https://developers-business.wport.mePRDv1
公開站 originhttps://wport.mePRDv1
cookie domain.wport.mePRDv1
cookie TTL7 天既有v1
轉址碼(觀察期)302PRD/ADR-005v1
轉址碼(穩定後)301PRD/ADR-005v1
觀察期14 天PRDv1
轉址保留期≥ 24 個月PRDv1

Event dictionary

事件觸發Payload 重點SoT
auth.cross_site_navigate由 wport.me 跳往 business(或反向)from_app, to_app, has_tokenPRD v1(建議實作,用於量測 S6)
auth.session_bridge_failedbusiness 站讀不到有效 session 而被導回登入reason(no_cookie/expired/mismatch)PRD v1(建議必做,是 S6 的唯一可觀測訊號)
legacy_url.redirected邊緣轉址命中path(可由邊緣 log 取得,不必前端埋)邊緣 log v1

GA4 說明:wport.mebusiness.wport.me 同屬一個註冊網域,GA4 預設 cookie 網域為最高層級註冊網域,工作階段不會斷,不需要設定跨網域評估;但仍建議把 business.wport.me 加入「不需要的參照連結網址」清單以防自我參照。此段請行銷/RD 於 GA4 後台實查確認後再定案。

Terminology

中文英文定義
企業後台Business consoleapps/recruitment,切換後位於 business.wport.me
求職者後台User consoleapps/user,維持 wport.me/user/*
公開站Main siteapps/main-sitewport.me
內部後台Site adminW101-Admin-Webadmin.wport.me
Session 橋接Session bridge以共享 cookie 讓兩個 origin 使用同一份登入狀態
相容轉址Compat redirectbusiness.wport.me/recruitment/*/* 的保底規則
同 site / 同 originsame-site / same-origin同 site 依註冊網域判定(cookie 用);同 origin 需 scheme+host+port 全同(CORS 用)。本案是同 site、跨 origin

Version freeze

項目v1/v2Blocking?理由Decision makerDate
business.wport.me 上線(root path)v1核心Eric2026-08-18
同源反向代理(API/WS)v1不做則前端 API 層要大改Eric2026-08-18
共享 cookie + cookie 為 SoTv1不做則跨站要重新登入Eric2026-08-18
6 處直讀 localStorage 修正v1不做則新站首次進站必判未登入Eric2026-08-18
getUrl() → AppOriginMapv1不做則 business 站導覽全死連結Eric2026-08-18
舊網址轉址(302)v1書籤與已寄出郵件Eric2026-08-18
後端 BUSINESS_DOMAINv1新寄出的信要對Eric2026-08-18
cookie 環境前綴(dev/staging)v1防 prod/非 prod 狀態污染Eric2026-08-18
302 → 301v1(P4)⚠️ 條件需 P2/P3 全綠並獨立核准Eric2026-08-18
auth.session_bridge_failed 事件v1強烈建議,非硬性Eric2026-08-18
Cloudflare Access/IP 允許清單v2有客戶要求再做Eric2026-08-18
HttpOnly + refresh token 輪替v2解 ADR-012 的根因Eric2026-08-18
/user 搬遷v2+ADR-011Eric2026-08-18
StorybookADR-010 明確不做Eric2026-08-18

13.3 PRD 問題清單

#段落問題建議類別影響SoT
1§6.2 SEC-EDGEprod 邊緣設定不在 repo 內(repo 內只有 dev/staging vhost 與容器內 nginx);wport.me 走 Cloudflare、adminstaging 走 CloudFront,實際 /api/v2/api 分流落在哪一層無法從 repo 確認RD/DevOps 先產出一份現行 prod 邊緣路由圖,再據此複製 business vhost;建議把設定納入版控技術前提高——決定 P1 能否如期RD
2§5.3dev 企業後台命名必須是單層子網域(developers-business.wport.me);business.developers.wport.me 會超出 *.wport.me 憑證覆蓋範圍命名中——錯了會憑證失效PRD
3§13.2 EventGA4 是否需要設定「不需要的參照連結網址」與是否沿用同一容器(recruitment 目前為 GTM-MGLNLCG5行銷/RD 於 GA4 後台實查後回填量測中——影響 S5 判讀行銷
4§5.4pm_40 company_pay 若上線,金流的 return/callback URL 需直接註冊新網域目前後端查無金流 return URL 實作,屬未來相依;請在 pm_40 開工時對照本表相依RD
5§3.4企業客戶內網/防火牆白名單上線前 2 週由業務/客服主動通知既有付費企業客戶新增網域營運高——大型客戶可能整間打不開業務
6§4.2BR-042 尚未寫入 business-rules.md。⚠️ 且與 pm_46(獵頭模組)撞號——該 PRD §8.2 也把 BR-042 提案為「夥伴綁定」(另外它的 BR-039/040/041 更是直接覆蓋 pm_48/pm_41/pm_38 已配發的規則)。business-rules.md v1.10.0 目前配到 BR-041,兩份 PRD 都寫「通過後回寫」,誰先寫誰占用本 PRD 通過後回寫,版本 → 1.11.0。回寫前必跑 python3 scripts/prd-lint.py --all 確認 C10 無搶號告警;回寫當下要通知 pm_46 改號(或依 pm_46 §8.2 的替代方案由本 PRD 讓號)文件中(撞號會靜默覆蓋)PM
7§9、ADR-005邊緣轉址規則在 dev 驗不到:prod 走 Cloudflare、dev 走內網 nginx,是兩套設定。wport.me/recruitment/*business.wport.me/* 仍為 prod 首次上路已決議(2026-08-19):接受。靠 ADR-005 的「先 302、關規則即秒回滾」管理;上線後盯邊緣 log 的 /recruitment/* 命中數與 404技術限制DevOps
8§5.3、§9staging 本次不切企業網域(PM 決議,v1.2.0)。staging(CloudFront)與 prod(Cloudflare)本就是不同邊緣堆疊,建 staging-business 也無法忠實演練 prod 的 Cloudflare 設定 → 不建的損失有限已確認(2026-08-19):理由存證無誤,決議不做。若日後 staging 要切,須先確認它與 prod 用同一套邊緣設定模板範圍已結案
9§6.2 SEC-DEEPLINK、§8.3前端從 identity_scope 推導「目標在哪個網域」。此推導在目前兩種實際使用的通知類型上都正確,但兩者語意本就不同(一個是「收件人身分」、一個是「目標頁位置」),新增通知類型時容易踩到已決議(2026-08-19):新增顯式的目標 origin 欄位(additive,舊前端不讀也不會壞)。契約見 §8.3。⚠️ 理由是「讓契約顯式」,不是修 bug —— 此欄位不修任何現存缺陷契約低(預防性)RD
10§13.4 #4auth-initializer.client.ts 的企業身分強制導向曾造成三次同款故障(/company-register/notification-links、公開職缺頁)已結案(2026-08-19):前端已完成,止血與根治都做了。判定邏輯抽成 apps/main-site/app/utils/companyRedirect.tsCOMPANY_REDIRECT_SKIP_PATHS 新增 /job//companies//job/ 刻意帶尾斜線以排除 /jobs 求職者列表),求職者專屬動作由頁面各自擋(components/job/JobDetailView.vue:119isCompany 分支)。見 origin/develop範圍已完成
11ADR-008前綴只加在非 prod、prod 維持 token忘了替新環境設前綴時,該環境會靜默讀到 prod session(失敗方向最差)已決議(2026-08-19):採①——維持「前綴只給非 prod」,並把「新增任何 *.wport.me 環境時必須設定 cookie 名稱前綴」寫成部署檢查項(見 ADR-008 Consequences 補述),不靠人記得。未採②(每個環境都加前綴)是因為代價是正式站使用者全部登出一次。根治列 v2,見 ADR-008 補述安全/維運已結案
12notification-link-token.service.ts:377-384既有缺陷(非本案造成)buildRedirectPath()PUBLIC_TARGET_TYPES(目前只有 COMPANY=公開公司頁)回空字串,而前端全鏈路無任何空值檢查——空字串照樣被 encodeURIComponent 塞進 to=、不觸發「連結無效」頁、下游 deep-link 直接 route.query.to as string 使用。實際結果:使用者點通知信後停在空白的承接頁。目前後端未發此類 target 故未曝光另案處理,不納入 pm_52(與網域切割無關,切不切都存在)。新增 target_origin 欄位不會修好它——空字串依然是空字串既有缺陷中(休眠)RD

13.4 Sync-fix list

#動作Repo/位置負責人Blocking?
1cookie 契約升級(Domain/Secure/SameSite/刪除時帶 Domain/讀取順序反轉/M1 一次性清理)packages/auth-session/src/browser-session-storage.tsapps/main-site/app/utils/nuxt-session-storage.tsFE
26 處直讀 localStorage.getItem('token') 改走 getToken()apps/recruitment/src/router/guard/loginGuard.tsapps/user/src/router/guards/loginGuard.tspackages/web-components/src/util/axiosHelper.js.../component/jobList.js(2 處)、.../layouts/header.jsFE
3getUrl() 改 AppOriginMap + 呼叫端標明目標 apppackages/utils/src/getUrl.tscommon-componentsweb-components 全部呼叫點FE
4企業後台入口全面改用 business originpackages/utils/src/constants/HeaderNavLinks.tspackages/store-core/src/global/index.tsdefaultUrlWhenCompany)、packages/web-components/src/layouts/{header,footer}.js、main-site auth-initializer.client.tsauth-redirect.tsMessageButton.vueStep5CompanyDone.vueEnterpriseShowcaseSection.vuecompany-register/index.vueapps/user AccountDeletionDialog.vuepermissionGuard.tsFE
4b白名單補 /job//companies/已完成:邏輯抽成 apps/main-site/app/utils/companyRedirect.ts,並在頁面層擋住求職者專屬動作apps/main-site/app/utils/companyRedirect.tscomponents/job/JobDetailView.vue:119FEDoneorigin/develop
5通知 deep-link:兩分支統一去前綴 + 由 app 自補 base(真正要改的是下游 app,不是 main-site)apps/recruitment/src/views/deep-link/index.vue:20-27,35-36apps/user/src/views/deep-link/、main-site pages/notification-links/index.vue(僅需處理 redirect_path === ''FE先做(R1)
5b通知 redirect_path 去掉 /recruitment 前綴TSH notification-link-token.service.ts:43BE選配 cleanup,前置為 #5;不做也不會壞
5cdeep-link 回歸測試to 不帶前綴仍導到正確位置。三個 app 現有 fixture 全部寫死帶前綴的值,無任何無前綴案例main-site/.../notification-links.spec.ts:37,45recruitment/.../deepLink.spec.ts:25deepLinkRealRouter.spec.ts:19apps/user/.../deep-link/__tests__/FE
5d通知 resolve 回應新增顯式 target_origin(值域 mainbusinessuser,對齊 §5.2 AppOriginMap)。v1.2.3 補列——§8.3 已決議加,但先前未進本表。additive/expand,舊前端不讀也不會壞;與 #5 的 R1 一起上,FE 優先讀此欄位、讀不到才回退 identity_scope 推導TSH notification-link-token.service.ts + 三個 app 的 deep-link 消費端BE + FE非阻塞(預防性契約改善,見 §13.3 #9;不修任何現存 bug)
6邀請信/審核信 URL 改讀企業側網域來源TSH company-invitation.service.ts:30TSH companies/utils/company-page.util.ts:23getCompanyManagePageUrl,v1.2.0 補漏)、AMS company-event.service.ts:163BE
6bcompany-page.util.ts 的呼叫端是排程company-profile-grace.service.ts:214,每日 cron,寄給真實企業)。改動需同步 company-profile-grace.service.spec.ts:6,114 的 mock 字串;⚠️ 驗證不可真跑該 cron(無公司過濾、全庫掃描、會寫下架期限),改以對 util 本身的單元測試驗TSHBE
7所有指向公開頁的連結一律以 main origin 解析:pm_51 複製網址用 wport.me 常數;JobManager/ViewJob/index.vue:17router.push('/job/…'))、JobManager/ViewJob/JobDetailView.vue:296,859openJob('/job/…'))、common/Company/CompanyDetailView.vue:163,327openCompany('/companies/…'),目前為空函式 no-op,實作時務必用 main origin)、views/company-info/preview-company-info/index.vue:16apps/recruitmentFE
8Vite base/、public 靜態資產路徑(/recruitment/icon/...)修正apps/recruitmentFE
8b新增一份 business 環境的 build 設定,至少含 VITE_CHAT_API_ORIGIN=https://business.wport.me 與 §8.1 的 main/business origin(現有只有 .env.staging.example.env.production.example 兩套)W101-Web 根目錄FE
9邊緣:DNS、TLS、SPA fallback、後端分流、WebSocket、robots、index.html no-store、轉址規則Cloudflare/nginxDevOps
10既有付費企業客戶通知(防火牆白名單/教育訓練文件更新)業務/客服
11CORS_ORIGINWS_CORS_ORIGIN 追加 business origin(保險)TSHBE
12business-rules.md 新增 BR-042prd repoPM
13更新 doc/feature/README.md 索引prd repoPM
14Coda backlog 新增列 PRD=pm_52prd repoPM
15302 → 301 轉換工單(P4)CloudflareDevOps❌(但不可遺忘)

14. 下一步

  1. RD/DevOps 先回答 §13.3 問題 #1(prod 邊緣路由現況)—— 這是唯一會擋住排程的技術前提
  2. FE 開 P0(cookie 契約 + AppOriginMap + 6 處直讀修正),可立即開工、與 P1 平行
  3. DevOps 建 developers-business.wport.me(內網 DNS + nginx vhost)走完 P1,用 §10.4 全表在 dev 驗收
  4. 業務/客服準備企業客戶通知稿與教育訓練文件更新(新網址)
  5. P2 切換 → 觀察 14 天(盯 auth.session_bridge_failed 與邊緣 404)→ P3 → P4 轉 301
  6. 上線後回寫 business-rules.md BR-042

文件結束