eCourt-note/docs/WORDADDIN-DEV.md
Suphonchai Phoonsawat cffc571da5 feat(addin): implement e-Court ticket auth (ticket_data + HmacSha256)
Replace the token/owner pass-through with the ticket flow from
docs/requirement/addin-workflow.md (full plan: docs/PLAN-ADDIN-AUTH.md):

- utils/docver-ticket.ts: shared pure ticket parse + hex digest helpers,
  client/server inject their own crypto (Web Crypto / node:crypto)
- lib/addin-ticket.ts + POST /api/documents/token second entry: body
  {ticket_data, HmacSha256} verified server-side (HMAC timing-safe + exp,
  secret ADDIN_TICKET_SECRET -> SECRET -> "super_secret"); x-api-key path
  unchanged
- wordaddin: ticketService (Web Crypto HMAC, localStorage session
  wordaddin-auth keyed by fid, same-origin token request), authService
  rewrite (7-step flow, silent renew), apiClient 401 renew-retry once,
  metadataService reads ticket_data/HmacSha256, no-access messages per
  cause
- scripts/make-ticket-docx.mjs: dev tool to stamp tickets onto any .docx
  for testing in real Word
- app/wordaddin/error.tsx: error boundary showing the real error + stack
  in the pane (webviews have no devtools)
- tests: docver-ticket + addin-ticket unit suites (21 cases) + temporary
  crash repro suite for the ongoing client-crash debug session
- docs sync: PLAN-DOCVER, WORDADDIN-DEV, addin-ui-flow, openapi (two
  request bodies for /token), CLAUDE.md, .env.example

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 11:21:15 +07:00

16 KiB

Word Add-in (wordaddin) — คู่มือ dev และการทดสอบ

Taskpane ของ Word add-in "e-Court Documents Versioning" พอร์ตมาเป็น route ของ Next.js ที่ /docedit/wordaddin (ต้นฉบับ webpack + Fluent UI: D:\word-addin-poc\addin) — spec พฤติกรรม UI ทั้งหมดอยู่ที่ docs/addin-ui-flow.md

ทางเข้า: เปิด /docedit เปล่า ๆ จะ redirect มาที่ taskpane ทันที (ไม่ผ่าน login — คำขอผู้ใช้ 2026-10-14) ยกเว้นลิงก์ e-Court /docedit/?fileUrl=&fileId=&typeId= ที่ยังส่งต่อให้ /note ตามสัญญา N8 · manifest ชี้ /docedit/wordaddin ตรง ๆ (ไม่ว่าผ่าน root เพื่อลด hop ใน webview)

สถานะช่วงนี้: ไม่มี login แล้ว (ยกเลิก Keycloak 2026-10-27 — auth เข้าทางเดียวคือ ticket ของ e-Court (ticket_data + HmacSha256 ในไฟล์): add-in ตรวจ HMAC เอง → ขอ token ที่ /api/documents/token → เก็บใน localStorage ตาม fid ดูหัว "ทดสอบ e-Court ticket" และแผน docs/PLAN-ADDIN-AUTH.md) ส่วน API ยังเป็น mock (version store เก็บ metadata ค้างข้าม session ใน localStorage ของ taskpane เพื่อทดสอบวงปิดเอกสาร→เปิดใหม่ — สลับกลับของจริงด้วยการลบ wordaddin-mock-versions หรือเคลียร์ storage ของ origin) รอ backend จริง — ดูหัว "การสลับกลับของจริง" ด้านล่าง

ไฟล์หลัก

ที่ คือ
app/wordaddin/page.tsx + components/wordaddin/WordAddinGate.tsx gate โหลด Office.js (self-host) → Office.onReady → mount TaskpaneApp
app/wordaddin/manifest.xml/route.ts serve manifest พร้อม rewrite origin ตามโฮสต์ที่ขอ (อ่าน x-forwarded-proto/x-forwarded-host ก่อนแล้ว fallback host ตรง ผ่าน utils/request-origin.ts ที่แชร์กับ origin ของ deep link N10 — โฮสต์ที่ไม่ใช่ localhost บังคับ https เพราะ Office รับ https เท่านั้น)
components/wordaddin/TaskpaneApp.tsx state machine checking/no-access/untracked/tracking + ทุก handler
wordaddin/services/* ชั้น Office.js พอร์ตดิบจากต้นฉบับ + versioningService เป็น mock (authService + ticketService = ticket auth จริง ไม่มี login — PLAN-ADDIN-AUTH.md)
public/wordaddin/manifest.template.xml template ของ manifest (ค่า default dev https://localhost:3000) — serve ผ่าน route ด้านบนเท่านั้น ไม่ได้ใช้ตรง ๆ
public/wordaddin/office-js/ Office.js runtime — สร้างโดย postinstall scripts/fetch-office-js.mjs ดาวน์โหลดฉบับล่าสุดจาก CDN ของ Microsoft (gitignore — รัน npm install ทุกครั้งหลัง clone เครื่องต้องออกอินเทอร์เน็ตตอนติดตั้ง ส่วนตัวระบบ deploy ใช้ intranet ล้วน)
wordaddin/config.ts env NEXT_PUBLIC_ADDIN_* (ตารางด้านล่าง)

Env

ตัวแปร default ใช้เมื่อไหร่
NEXT_PUBLIC_ADDIN_API_BASE_URL "" (ว่าง) ปลด mock ต่อ backend จริง (ต้องเป็น HTTPS) — ไม่เกี่ยวกับการขอ token (ยิง same-origin /docedit/api/documents/token เสมอ)
NEXT_PUBLIC_ADDIN_MOCK_USERNAME "tester01" ชื่อ fallback ของโหมด ?addindemo (แสดง header + เป็น createdByUserId ของ mock)
NEXT_PUBLIC_ADDIN_TICKET_SECRET "super_secret" secret ตรวจ HMAC ของ ticket ฝั่ง client (UX gate) — ต้องตรงกับตัว stamp ของ e-Court และ ADDIN_TICKET_SECRET/SECRET ฝั่ง server (PLAN-ADDIN-AUTH.md §7) · production ต้องตั้งค่าจริงทั้งสองฝั่ง

รัน dev ด้วย HTTPS (จำเป็นสำหรับ sideload ใน Word)

manifest ต้องชี้ https — 2 ทางเลือก:

  1. self-signed ของ Next (ง่ายสุด): npm run dev:https → https://localhost:3000 — Word อาจเตือน certificate ครั้งแรก (เปิด URL ใน browser กดผ่านก่อน 1 ครั้ง)
  2. ใบรับรอง trusted (แนะนำ ไม่มี warning): รัน npx office-addin-dev-certs install ครั้งเดียว แล้ว copy %USERPROFILE%\.office-addin-dev-certs\localhost.crt + localhost.key มาที่ certs/ (gitignore แล้ว) แล้วใช้ npm run dev:https:cert

Sideload ใน Word Desktop (Windows)

เครื่อง dev ที่มี repo (แนะนำ — ไม่ต้องดาวน์โหลด/คัดลอกอะไรเลย):

  1. Word: File > Options > Trust Center > Trust Center Settings > Trusted Add-in Catalogs เพิ่มโฟลเดอร์ public/wordaddin ของ repo (เช่น D:\dms\judes-note\public\wordaddin) > restart Word
  2. Insert > My Add-ins > SHARED FOLDER > เปิด "e-Court Documents Versioning"
  3. ชื่อไฟล์ไม่สำคัญ — Word สแกน *.xml ทุกไฟล์ในโฟลเดอร์ โดย manifest.template.xml คือ dev manifest อยู่แล้ว (URL localhost:3000) และเนื้อไฟล์อัปเดตตาม branch ที่ checkout อยู่เสมอ — ห้ามวาง xml manifest อื่นเพิ่มในโฟลเดอร์นี้ (GUID เดียวกันสองไฟล์ = Word สับสน)

เครื่องที่ไม่มี repo (production/เครื่องผู้ใช้):

  1. ดาวน์โหลด manifest จาก environment นั้น: https://<โฮสต์>/docedit/wordaddin/manifest.xml
  2. เอาไฟล์ไว้โฟลเดอร์ใดโฟลเดอร์หนึ่ง เช่น %USERPROFILE%\Documents\wordaddin-manifest\
  3. ทำขั้น Trust Center ข้างบนด้วยโฟลเดอร์นั้น > restart Word > Insert > My Add-ins > SHARED FOLDER > เปิด "e-Court Documents Versioning"

Deploy จริง (intranet): ไม่ต้อง rewrite อะไรแล้ว — manifest serve ผ่าน app/wordaddin/manifest.xml/route.ts ซึ่งเขียนทับ origin ให้ตรงโฮสต์ที่ขอไฟล์อัตโนมัติ ดาวน์โหลด https://<โฮสต์>/docedit/wordaddin/manifest.xml แล้วใช้ sideload ได้ทันทีทุก environment (dev/UAT/production) · proxy ฝั่งหน้าควรส่ง X-Forwarded-Host/X-Forwarded-Proto ตามมาตรฐาน — ถ้าไม่ส่ง route ก็ยังใช้ Host ของ request และบังคับ https ให้โฮสต์ที่ไม่ใช่ localhost อยู่ดี

รุ่น Office ที่รองรับ: manifest กำหนด WordApi 1.3 เป็นค่าต่ำสุด (customXmlParts คือ API สูงสุดที่ใช้ประจำ) — Microsoft 365 / Office 2021+ ใช้ได้ครบทุกฟีเจอร์ · Office 2019 โหลดได้ แต่ปุ่ม "…และปิดเอกสาร" ถูกซ่อนอัตโนมัติ เพราะ document.close() ต้องการ WordApi 1.4/1.5 (Microsoft 365 เท่านั้น) — canCloseDocument() เช็ค isSetSupported ตอน runtime แล้วซ่อน พฤติกรรมเดียวกับ Word on the web · Office 2016 (WordApi 1.1) โหลดไม่ขึ้น (เดิมเคยตั้ง 1.5 ทำให้ Office 2019 โหลดไม่ขึ้นด้วย — แก้ 2026-10-16)

ทดสอบใน browser ธรรมดา (ไม่ต้องเปิด Word)

  • /docedit/wordaddin — เส้นทางจริง: Office.js โหลดได้แม้นอก host (host เป็น null) → detect() อ่าน custom properties ไม่ได้ (ไม่มี Word.run) → เห็น no-access — ตรวจ state machine ได้แค่นี้ใน browser ปกติ ส่วน phase untracked/tracking ต้องใช้ demo ด้านล่าง (หรือ sideload ใน Word จริง)
  • /docedit/wordaddin?addindemo=untracked / ?addindemo=tracking — dev-only: ข้าม Office.js + ตั้ง session mock ทันที + ตั้ง phase ตามค่า (tracking มี history ตัวอย่าง 3 รายการ) — ทุกปุ่มกดได้ แต่ flow ที่แตะ Word จริง (Start Versioning / Save & Version / บันทึก) จะจบที่ error ตาม expected
  • หมายเหตุ: ใน browser ปกติ (ไม่ใช่ demo) banner "บันทึกอัตโนมัติไม่สำเร็จ" จะโผล่หลัง ~30 วิ เพราะ auto-save (FR-17) พยายาม save ทุก 30 วิแต่ไม่มี Word — ถือว่าถูกต้อง

ทดสอบ e-Court ticket (auth ทางเข้าเดียว)

ไฟล์ที่ e-Court stamp custom properties ticket_data (JSON exp/fid/tid/uid/uname) + HmacSha256 (HMAC-SHA256 ของ string ดิบ ด้วย secret ปัจจุบัน super_secret) ไว้ จะใช้ได้เลย — ไม่มี login ให้กด: detect() อ่านคู่นี้ตอนเปิด task pane แล้วเรียก authService.signInWithTicket() ตาม flow ของ docs/requirement/addin-workflow.md (ตรวจ HMAC ฝั่ง client → ตรวจ exp → ดู session ใน localStorage wordaddin-auth ตาม uid/fid → ขอ access_token ที่ /docedit/api/documents/token แบบ body ticket → เก็บ token ตาม fid) · ไฟล์ที่ไม่มีคู่นี้ / HMAC ไม่ตรง / ticket หมดอายุ = phase no-access แยกข้อความตามสาเหตุ

ทดสอบเองใน Word โดยไม่ต้องรอ e-Court — สร้างไฟล์ทดสอบด้วยสคริปต์ (คำนวณ HMAC ตรงกับตัว stamp จริงของกลุ่ม template):

# สร้าง <file>.ticket.docx (ไม่แตะไฟล์เดิม) exp +5 นาที เท่า TICKET_SEC ของตัว stamp
node scripts/make-ticket-docx.mjs path/to/file.docx --fid DEMO_FID_001 --uid oom --uname "สรวิชญ์" --tid T12
# ดู custom properties ที่มีในไฟล์
node scripts/make-ticket-docx.mjs path/to/file.ticket.docx --list
# ทดสอบ phase tracking ด้วย (stamp SavedByAddIn + DocumentTrackingId เพิ่ม)
node scripts/make-ticket-docx.mjs path/to/file.docx --tracked --in-place

แล้วเปิดไฟล์ใน Word ที่ sideload แล้ว → ต้องเข้าเฟส untracked (หรือ tracking เมื่อ --tracked) ทันที ชื่อผู้ใช้ = uname · ทดสอบ fail-case: แก้ ticket_data ผ่าน File → Info → Properties → Advanced Properties → Custom สัก 1 ตัวอักษร → no-access "ไฟล์ถูกแก้ไข" · สร้างด้วย --exp +1 รอ 1 นาทีแล้วเปิด → no-access "ticket หมดอายุ" · เปิดเอกสารเดิมซ้ำ (จน ticket หมดอายุแต่ token ยังอยู่ใน localStorage) → ยังใช้ได้ตาม requirement ขั้น 7

หมายเหตุ: การขอ token ยิง same-origin /docedit/api/documents/token จริง (ไม่ผูก NEXT_PUBLIC_ADDIN_API_BASE_URL) — dev server ต้องรันอยู่และ secret ฝั่ง server (ADDIN_TICKET_SECRET → SECRET → default super_secret) ตรงกับ --secret ของสคริปต์

การสลับกลับของจริง (วันหน้า)

  • Auth: ไม่มี login ให้สลับแล้ว — ยกเลิก Keycloak ถาวร 2026-10-27 · auth ของ add-in คือ ticket ของ e-Court ผ่าน signInWithTicket (ticket_data + HmacSha256 ในไฟล์ — แผนเต็ม docs/PLAN-ADDIN-AUTH.md): add-in ตรวจ HMAC ฝั่ง client (UX gate) แล้วขอ access_token ที่ /api/documents/token ซึ่ง server ตรวจ HMAC+exp อีกชั้นด้วย secret ฝั่ง server · โดน 401 กลาง flow = ลอง renew จาก ticket 1 ครั้ง ยังไม่ผ่าน = AuthRequiredError → หน้า no-access (ticket ต่ออายุไม่ได้ — ต้องได้ไฟล์ใหม่จาก e-Court)
  • API: wordaddin/services/versioningService.ts เก็บชื่อ/ลายเซ็นเหมือนต้นฉบับเป๊ะ — ก้อบ body เดิม (multipart POST ผ่าน apiClient ซึ่งพอร์ตไว้แล้ว) กลับมา + ตั้ง NEXT_PUBLIC_ADDIN_API_BASE_URL (contract ปัจจุบันของกลุ่ม docver สรุปไว้ใน docblock ของไฟล์นั้น + docs/PLAN-DOCVER.md — identity/docId แกะจาก Bearer token ทั้งหมด)
  • ชุด unit test ของ services (jest → vitest) ยังไม่พอร์ต — ต้นฉบับอยู่ที่ D:\word-addin-poc\addin\src\services\*.test.ts + mock ที่ src/test/

Docker

ไม่ต้องทำอะไรเพิ่ม — docker/web.Dockerfile รัน scripts/fetch-office-js.mjs ใน build อยู่แล้ว (build stage ต้องออกอินเทอร์เน็ตถึง appsforoffice.microsoft.com ได้) และ runner stage คัดลอก public/ เข้า image เอง (COPY /app/public ./public — Next 14.2 standalone ไม่รวม public/ ให้อัตโนมัติ ต้อง copy เองตามเอกสาร deploy ของ Next; เคยเข้าใจผิดว่า Next รวมให้ เพราะบางไฟล์หลุดเข้า image ผ่าน output file tracing ของ Next ที่ตาม fs.readFile ใน server code — แต่ public/wordaddin/office-js ไม่มีใครอ่านผ่าน fs จึงไม่ถูกดึงมา แล้ว taskpane พังใน production 2026-10-16)

ทำไมต้องเป็นฉบับ CDN ล่าสุด: แพ็กเกจ npm @microsoft/office-js เลิกอัปเดตที่ 1.1.110 (~2018) ก่อน WordApi 1.4/1.5 — runtime ตัวนั้นไม่มี Document.close (ปุ่ม "บันทึกเวอร์ชันและปิดเอกสาร" พัง: context.document.close is not a function) และไม่มี enum Word.CloseBehavior/SaveBehavior ทับ ๆ กับ @types/office-js ฉบับใหม่ที่ใช้ compile