- Removed client-side HMAC verification; server now handles all HMAC checks. - Updated ticket processing logic to improve UX and security. - Adjusted API request handling to return discriminated results based on HTTP status. - Enhanced error messages for better clarity on authentication failures. - Added unit tests for the new token endpoint to ensure compliance with updated contract. - Cleaned up unused code and comments related to previous HMAC handling.
28 KiB
PLAN-ADDIN-AUTH — Add-in Ticket Authen (ticket_data + HmacSha256)
วันที่ตกลงแผน: 2026-10-27 · สถานะ: แผนรออนุมัติ · requirement ต้นทาง:
docs/requirement/addin-workflow.mdแก้ใหญ่ 2026-11-04 (ตาม code review ของผู้ใช้): ถอด client HMAC +NEXT_PUBLIC_ADDIN_TICKET_SECRETออก (execute D13 ของ PLAN-SSO.md — secret ใน bundle สกัดได้ = ปลอม ticket ผ่าน server ได้จริง ไม่ใช่แค่ UX gate อย่าง D3 เดิม) · ทางเข้า ticket ของ /token แยก status ชัดเจน 200 / 400 ฟอร์แม็ตพัง / 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่หมดอายุ (เลิก hasVersionByUser ของ requirement ขั้น 7 ฉบับ 2026-10-28) · token TTL default 604800 (7 วัน) แทนระบบ refresh · flow ฝั่ง addin เป็น pre-check + ยิง /token ทุกครั้งที่เปิดเอกสาร
0. บริบท/เป้าหมาย
เปลี่ยน auth ของ wordaddin จาก e-Court pass-through แบบเดิม (e-Court ขอ token เองแล้ว stamp
token/owner ลงไฟล์) เป็น ticket ตาม requirement:
- e-Court stamp custom properties 2 ค่าในไฟล์ .docx:
ticket_data(JSON) +HmacSha256 - addin ตรวจ ฟอร์แม็ต + exp เองฝั่ง client เป็น UX gate — การตรวจลายเซ็น (HMAC) เป็นของ server อย่างเดียว ยิง /token ทุกครั้งที่เปิดเอกสาร (แก้ 2026-11-04 — เดิมตรวจ HMAC ฝั่ง client ด้วย secret ใน bundle)
- addin เป็นฝ่ายขอ access_token ที่
POST /api/documents/tokenเก็บใน localStorage ตาม fid (session ต่อ userId) — ไม่ใช่ e-Court อีกต่อไป - ถอดระบบเก่า
token/ownerออกทั้งหมด (implement ไว้ 2026-10-27 ยังไม่มีไฟล์จริงใช้ — ไม่ต้องรองรับสองแบบพร้อมกัน)
1. ข้อตกลงที่ตัดสินใจแล้ว (2026-10-27)
| # | เรื่อง | ข้อตกลง |
|---|---|---|
| D1 | addin ยืนยันกับ /token อย่างไร |
ขยาย POST /api/documents/token รับ ticket เพิ่ม — body {ticket_data, HmacSha256} server ตรวจ HMAC+exp ด้วย secret ใน env ฝั่ง server ก่อนออก token · ทางเข้า x-api-key เดิม (e-Court server-to-server) ใช้ต่อคู่กัน |
| D2 | โค้ดซ้ำสองฝั่ง | แชร์ส่วน pure ผ่าน utils/docver-ticket.ts (โซนกลาง client ใช้ได้ — ไม่แตะกติกา "wordaddin ห้าม import จาก lib/") · ฝั่ง crypto ฉีดเข้าทาง parameter: client = Web Crypto, server = node:crypto |
| D3 | client-side HMAC | NEXT_PUBLIC_ADDIN_TICKET_SECRETNEXT_PUBLIC_ADDIN_TICKET_SECRET ออกจาก bundle ทั้งหมด (secret ใน bundle ต้องตรงกับของ server เสมอจึงผ่าน gate = แกะ bundle ก็ปลอม ticket ผ่าน server ได้จริง) |
| D4 | รูปแบบ HMAC | sign บน string ดิบของ ticket_data ตามที่อ่านได้จาก property (ห้าม trim/normalize) · digest เป็น hex lowercase 64 ตัว — การเทียบ normalize เป็น lowercase ก่อน (ฝั่ง e-Court stamp ตัวพิมพ์ใหญ่ก็ผ่าน) |
| D5 | tid |
= template ของ claims (tid optional → template: null) · map uid→userid, uname→username, fid ตรง ๆ |
| D6 | token หมดอายุ | แก้ 2026-11-04: TTL default 604800 (7 วัน) แทนระบบ refresh/renew — renew จาก ticket (โดน 401 กลาง session → renew 1 ครั้งแล้ว retry) เหลือเป็นเส้นทางรองที่แทบไม่ถูกใช้ · ticket หมดอายุแล้ว server ตอบ 403 → renew ไม่ได้ → ใช้ JWT เดิมใน session ต่อ |
| D7 | localStorage | key เดียว wordaddin-auth เก็บ JSON {v:1, userId, userName, tokens: {[fid]: {token, exp}}} · เจอ user ใหม่ = เคลียร์ทั้งก้อน (รวม token ทุก fid ของ user เก่า) · webview block localStorage = fallback memory (pattern versioningService) |
| D8 | "disable Add-in ทั้งหมด" | = phase no-access ที่มีอยู่ โดยแยกข้อความตามสาเหตุ: ไม่พบ ticket / ไฟล์ถูกแก้ไข (HMAC) / ticket หมดอายุ |
| D9 | ช่วง mock (NEXT_PUBLIC_ADDIN_API_BASE_URL ว่าง) |
addin ยิงขอ token จริงที่ same-origin withBasePath("/api/documents/token") (ไม่ผูกกับ apiBaseUrl ของ versioning) — versioning ยัง mock ต่อ |
| D10 | เครื่องมือทดสอบ | มีสคริปต์ stamp ticket ลงไฟล์ .docx ตัวอย่าง (scripts/make-ticket-docx.mjs) — ไม่งั้นทดสอบ flow นี้ใน Word จริงไม่ได้เลย |
2. Contract ของ ticket (ต้องส่งให้ทีม e-Court ฝั่ง stamp)
Custom properties ของไฟล์ .docx (ชื่อ key ต้องตรงเป๊ะ — case-sensitive):
ticket_data = {"exp":1793067433,"fid":"T12_FD155_3s-long_ZZZ","tid":"T12","uid":"oom","uname":"สรวิชญ์"}
HmacSha256 = <hex 64 ตัว lowercase> = HMAC-SHA256(secret, ticket_data ดิบทั้ง string ไม่ trim)
| field | type | เงื่อนไข |
|---|---|---|
exp |
int (unix วินาที) | บังคับ · exp <= now = หมดอายุ |
fid |
string | บังคับ · trim 1–200 · เป็น docId ของทุก operation ของกลุ่ม docver |
uid |
string | บังคับ · trim 1–100 |
uname |
string | บังคับ · trim 1–200 |
tid |
string | optional · ≤100 · template ต้นทาง (ไม่มี = template: null) |
- field อื่นปนมา = reject (allowlist ตาม convention ของ repo)
- secret ปัจจุบัน =
"super_secret"— e-Court (ตัว stamp) +ADDIN_TICKET_SECRET(server ของเรา ตัว verify) ต้องตรงกันสองฝั่ง (แก้ 2026-11-04 — เดิมต้องตรงสามฝั่งรวมNEXT_PUBLIC_ADDIN_TICKET_SECRETฝั่ง client ซึ่งถอดออกแล้ว) - ⚠️ custom property ของ Word จำกัด value ~255 ตัวอักษร — JSON ของ
ticket_dataต้องออกแบบสั้น (fid/tid/uname ยาวมากจะทะลุ; ฝั่ง addin เจอค่าเกิน = ถือว่าไฟล์เสียหาย → no-access)
3. ฝั่ง server — POST /api/documents/token รับ ticket เพิ่ม
Branch ตาม header (ตัวตั้ง = x-api-key):
| เคส | พฤติกรรม |
|---|---|
มี header x-api-key |
ทางเดิมทุกอย่าง — requireDocverApiKey + parseTokenRequestBody (body allowlist userid/username/fid/template, ส่ง field ticket ปนมา = 400 ตาม allowlist เดิม) |
ไม่มี x-api-key + body {ticket_data: string, HmacSha256: string} ถูกทั้งคู่ |
ทางใหม่ — verifyAddinTicketHmac (HMAC+parse ไม่ตรวจ exp) → isTicketExpired → issueDocverToken → 200 response รูปเดียวกัน {access_token, token_type, expires_in} |
| body ไม่ใช่ JSON / ไม่ใช่ object / มี field อื่นปน / field ไม่ใช่ string / ไม่มี ticket fields เลย (missing) | 400 (ฟอร์แม็ตพัง — แก้ 2026-11-04: missing ย้ายจาก 401 มารวม 400) |
| HMAC ผิด / parse ticket ไม่ผ่าน | 401 "ticket ไม่ถูกต้อง" ข้อความเดียวกันทุกสาเหตุ (กัน oracle) |
| ticket หมดอายุ แต่ HMAC ถูก | **403 "ticket ของ e-Court หมดอายุ"** (แก้ 2026-11-04 — เลิก requirement ขั้น 7 ฉบับ 2026-10-28: ถอด hasVersionByUser` ออก ไม่ตรวจ DB อีกต่อไป เพราะ ticket ฝังในไฟล์ ใครได้ไฟล์ก็ได้ ticket การปล่อยให้ ticket เก่าแลก token ใหม่ได้ตลอดไปเท่ากับปล่อยสิทธิ์ตามการครอบครองไฟล์ · client ที่มี JWT ของ fid นี้ใน session ที่ยังไม่หมดใช้ต่อเอง) |
DOCVER_TOKEN_SECRET ไม่ตั้ง |
500 (เดิม) |
-
token TTL: env
DOCVER_TOKEN_TTL_SECONDSdefault 604800 (7 วัน) (แก้ 2026-11-04 จาก 3600) -
route ไม่ import
@/db/minio เลยหลังถอด DB check — unit test ได้โดยไม่ต้องตั้ง DATABASE_URL (tests/app/api/documents/token.test.ts) -
การเทียบ digest ฝั่ง server: hash ทั้งสองฝั่งด้วย SHA-256 แล้ว
timingSafeEqual— patternverifyDocverTokenของlib/docver-token.tsเป๊ะ -
middleware ยกเว้น
api/documents/*อยู่แล้ว — ไม่ต้องแต้
4. โค้ดแชร์ — utils/docver-ticket.ts (pure, ไม่มี dependency เลย)
export interface DocverTicket { exp: number; fid: string; tid: string | null; uid: string; uname: string }
export function parseDocverTicket(raw: string | unknown): { ok: true; ticket: DocverTicket } | { ok: false; error: string }
export function isTicketExpired(exp: number, nowSec?: number): boolean // exp <= now
export function normalizeHexDigest(value: string): string | null // trim+lowercase+ตรวจ ^[0-9a-f]{64}$ (ใช้ฝั่ง server)
(แก้ 2026-11-04: ถอด timingSafeEqualHex + HmacSha256Hex adapter ออก — client ไม่คำนวณ
HMAC แล้วจึงไม่ต้องฉีดตัวคำนวณ, timing-safe จริงอยู่ที่ node:crypto ฝั่ง server)
lib/(server) กับwordaddin/(client) ใช้parseDocverTicket/isTicketExpiredตัวเดียวกัน — schema drift กันได้จากฝั่งเดียว
5. ฝั่ง addin — flow ตอนเปิดเอกสาร (แก้ 2026-11-04 — pre-check + ยิง /token ทุกครั้ง)
TaskpaneApp.detect() → authService.signInWithTicket({ticketData, ticketHmac}):
1. อ่าน properties ticket_data + HmacSha256 (readTrackedProperties)
2. pre-check ฝั่ง client (ไม่ยิง network):
· ขาดข้อใดข้อหนึ่ง / ค่ายาวเกิน 255 → no-access "ไม่พบ ticket ของ e-Court ในไฟล์"
· parseDocverTicket ไม่ผ่าน → no-access "รูปแบบ ticket ในไฟล์ไม่ถูกต้อง"
· ticket หมดอายุ และ ไม่มี JWT ของ fid นี้ใน session (uid ตรงกัน) → no-access
"ticket หมดอายุ" (มีใน session = มีโอกาสเป็นไฟล์เก่าที่เคยแก้ → ไปให้ server ตัดสินต่อ)
3. ยิง POST /api/documents/token **ทุกครั้งที่เปิดเอกสาร** (ตรวจลายเซ็นที่ server —
client ไม่มี secret) แล้วแยกทางตาม status:
· 200 → ได้ JWT ใหม่: uid ≠ session.userId (user ใหม่) = เคลียร์ session เก่าทั้งก้อน
สร้างใหม่ · คนเดิม = update userName + tokens[fid]
· 403 (ลายเซ็นถูกแต่ ticket หมดอายุ) → มี JWT ของ fid ใน session ที่ยังไม่หมด
(uid ตรง) = refresh userName แล้ว**ใช้ JWT เดิมต่อ** · ไม่มี/หมดแล้ว = no-access
"ticket หมดอายุ"
· 400 → no-access "รูปแบบ ticket ไม่ถูกต้อง" · 401 → no-access "ตรวจสอบลายเซ็น (HMAC)
กับระบบไม่ผ่าน" (ทั้งคู่ = ไฟล์ถูกแก้/เสียหาย ต้องขอไฟล์ใหม่)
· network/5xx/ตอบผิดรูป → no-access "ขอ token จากระบบไม่สำเร็จ" (ลองใหม่ได้เมื่อเน็ตกลับ)
4. ตั้ง state ภายใน (fid/username/ticket ดิบไว้ renew) → ไหลต่อ phase
untracked/tracking เดิม (SavedByAddIn → history) ไม่เปลี่ยน
authServicesurface ใหม่:signInWithTicket/getAccessToken(token ของ fid ปัจจุบัน) /renewTokenFromTicket/getUsername— ถอดsignInFromDocument+decodeJwtFieldสำหรับ uid (ticket มี uid/uname ตรง ๆ แล้ว เหลือ decode เฉพาะexpของ JWT ตอนเก็บ token)apiClientตอนโดน 401:renewTokenFromTicket()→ สำเร็จ = retry request 1 ครั้ง → ยัง 401/ไม่มี ticket =AuthRequiredError(UI ไป no-access — เดิมมีอยู่แล้ว)?addindemoยังใช้ได้ (signInForDemoคงเดิม)
6. ไฟล์ที่สร้าง/แต้/ถอด
สร้าง
utils/docver-ticket.ts— §4lib/addin-ticket.ts— server-only:addinTicketSecret()(envADDIN_TICKET_SECRETtrim หรือ default"super_secret") +verifyAddinTicket(ticketData, hmacHex, secret, nowSec?)(HMACnode:crypto+ timing-safe + parse + exp) +ticketToDocverClaims(ticket)— ห้าม import@/db/minio (unit test รันได้เปล่า ๆ)wordaddin/services/ticketService.ts— client:(ถอด 2026-11-04), session store (verifyTicketHmacWeb(crypto.subtle)loadSession/saveSession/clearSession— try/catch + memory fallback),requestDocverToken(POSTwithBasePath("/api/documents/token")body ticket — แก้ 2026-11-04: คืน discriminated result แยก network/http status/malformed ไม่โยน Error)scripts/make-ticket-docx.mjs— §9tests/utils/docver-ticket.test.ts,tests/lib/addin-ticket.test.ts(vitest — patterntests/lib/docver-token.test.ts)
แต้
app/api/documents/token/route.ts— branch ตาม §3wordaddin/config.ts—(ถอด 2026-11-04 — เหลือticketSecret: process.env.NEXT_PUBLIC_ADDIN_TICKET_SECRET ?? "super_secret"apiBaseUrl+mockUsername)wordaddin/services/metadataService.ts— ถอดECOURT_TOKEN_PROPERTY/ECOURT_OWNER_PROPERTY→TICKET_DATA_PROPERTY = "ticket_data"+TICKET_HMAC_PROPERTY = "HmacSha256"(readTrackedPropertiesอ่านคู่นี้แทนecourtToken/ecourtOwner)wordaddin/services/authService.ts— rewrite ตาม §5 (แก้ 2026-11-04: pre-check + แยก status 200/400/401/403 ผ่าน result ของ requestDocverToken)wordaddin/services/apiClient.ts— renew-retry ตอน 401components/wordaddin/TaskpaneApp.tsx—detect()เรียกsignInWithTicket+ ข้อความ no-access (แก้ 2026-11-04: no-access เคส "อ่านข้อมูลเอกสารไม่สำเร็จ" มีปุ่ม "ลองใหม่" เรียก detect() ซ้ำ)components/wordaddin/WordAddinGate.tsx— แก้ 2026-11-04: fallback onReady เดิม 5 วิแล้ว proceed บอด ๆ ทำให้เครื่องช้าจบ no-access ไม่มีทางออก — ตอนนี้ poll ทุก 5 วิว่า globalOfficeโผล่หรือยัง (โผล่แล้ว = proceed ตามเจตนาเดิม · ยังไม่โผล่ = รอต่อ) จนถึง hard cap 60 วิ ค่อยไปหน้า errorlib/openapi.ts— POST /token มี 2 รูป body (oneOf: identity+x-api-key / ticket) + คำอธิบาย 400/401/403 ตาม §3 (แก้ 2026-11-04)tests/app/api/documents/token.test.ts— ใหม่ 2026-11-04: route unit test ครอบ contract (ทำได้เพราะ route ไม่ import @/db แล้ว).env.example,docs/WORDADDIN-DEV.md,docs/addin-ui-flow.md,docs/PLAN-DOCVER.md(§token ชี้มาที่นี่),CLAUDE.md
ถอด
- ระบบ
token/ownerทั้งเส้น (properties +signInFromDocument+decodeJwtFieldส่วน uid) — แทนด้วย ticket เต็มตัว
7. Env
| env | default | หมายเหตุ |
|---|---|---|
ADDIN_TICKET_SECRET |
SECRET ของกลุ่ม template → "super_secret" |
server ตรวจ HMAC ทาง ticket ของ /token — อ่านตามลำดับ ADDIN_TICKET_SECRET → SECRET (deployment เดียวกับตัว stamp ของกลุ่ม template มักตั้งไว้แล้ว) → "super_secret" · production ต้องตั้งค่าใหม่ (ค่า default รู้กันหมด = ปลอม ticket ได้) และต้องตรงกับฝั่ง e-Court · ห้าม log ค่าดิบ |
NEXT_PUBLIC_ADDIN_TICKET_SECRET |
— | ถอด 2026-11-04 (PLAN-SSO.md D13) — ฝั่ง add-in ไม่มี secret อีกต่อไป |
หมายเหตุจากตอน implement: ตัว stamp ticket จริงคือกลุ่ม template (
app/api/template/render/route.ts→applyTemplate— sort key ตามตัวอักษรหลังเติมexpแล้ว HMAC hex lowercase ผ่านlib/lib-utils.ts+ stamp ลงไฟล์ด้วยsetCustomDocPropsของlib/lib-tmpl.tsอ่าน envSECRETกับTICKET_SECdefault 300 วิ — ไฟล์ยังมี propertyEd25519เพิ่มด้วยแต่ out of scope ของแผนนี้)
(เลือก default แทน fail-closed ตาม requirement ที่ระบุค่าปัจจุบันไว้ — dev ใช้ได้ทันที
แต่ comment เตือน production ชัด ๆ ทั้ง .env.example และแผนนี้)
8. Edge cases
- property ยาวเกิน 255 → ไฟล์เสียหาย → no-access (Word ตัดค่าทิ้งจะ parse ไม่ผ่านอยู่แล้ว — เช็คความยาวเพื่อข้อความที่ชัดกว่า)
- เปิดเอกสาร = ต้อง online ทุกครั้ง (ยิง /token เพื่อตรวจลายเซ็นที่ server — แก้ 2026-11-04) — offline ได้ no-access "ขอ token ไม่สำเร็จ" · ระหว่าง session ใช้ต่อได้เพราะ JWT อายุ 7 วัน (apiClient ไม่ยิง /token จนกว่าจะโดน 401)
- ticket หมดอายุ + มี JWT ของ fid ใน session ที่ยังไม่หมด → ผ่านด้วย JWT เดิม (server ตอบ 403)
· ticket หมดอายุ + ไม่มี session / JWT หมดด้วย / localStorage หาย → ต้องขอไฟล์ใหม่จาก
e-Court (แก้ 2026-11-04 — เดิม server ออก token ให้จากหลักฐาน DB
hasVersionByUserซึ่งถอดแล้ว) - localStorage ถูก block (webview บางตัว/private mode) → session อยู่ใน memory ต่อ task pane instance เดียว: ปิด-เปิดใหม่ + ticket หมดอายุ = no-access (ไม่มี JWT ในเครื่อง) · ticket ยังไม่หมด = ขอใหม่ได้ปกติ
- uname ไทย — เป็น string ตรง ๆ ไม่ผ่าน base64 ไม่มีปัญหา mojibake (ต่างจาก decode JWT payload)
- exp == now → หมดอายุ
- 401 จาก endpoint อื่นระหว่างใช้งาน → renew 1 ครั้ง (server ตอบ 200 = ได้ใหม่ · 403 = ticket หมด → false) → retry → ยังไม่ผ่าน → no-access
- เครื่องโหลดช้า จน office.js ยังไม่ evaluate ตอน taskpane เริ่ม → WordAddinGate รอต่อ (poll
ทุก 5 วิ) จนมี global
Officeถึง mount TaskpaneApp — ไม่ proceed บอด ๆ แล้ว (แก้ 2026-11-04); ถ้า detect อ่านไฟล์ไม่ได้จริง หน้า no-access มีปุ่ม "ลองใหม่" เรียก detect() ซ้ำ - Word เปิดหลายเอกสารพร้อมกัน (user เดียวกัน fid ต่างกัน) — tokens map รองรับ; current fid เป็น state ต่อ taskpane instance (memory) — แยกกันอยู่แล้ว
- ไฟล์ทั่วไปไม่มี ticket → no-access เหมือนเดิม (ข้อความเดิมปรับคำ)
9. สคริปต์ทดสอบ — scripts/make-ticket-docx.mjs
Dev tool รันมือ node scripts/make-ticket-docx.mjs <file.docx> [options]:
- option:
--fid --uid --uname --tid --exp <unix|+seconds> --secret <hmac-secret>และ--tracked(stampSavedByAddIn+DocumentTrackingIdเพื่อทดสอบ phase tracking ด้วย) - ทำงาน: สร้าง JSON ticket → HMAC-SHA256 hex → เขียน custom properties ลง
docProps/custom.xmlผ่าน pizzip (dependency อยู่แล้วจากกลุ่ม template) — สร้าง custom.xml เองถ้าไฟล์ยังไม่มี (พร้อม override ใน[Content_Types].xml) · เขียนทับไฟล์เดิมเฉพาะเมื่อส่ง--in-placeไม่งั้นเขียนไฟล์ใหม่<name>.ticket.docx(กฎ non-destructive ผูกกับpublic/เท่านั้นแต่ถือหลักเดียวกัน) - default secret/exp สอดคล้อง .env ตัวอย่าง
10. Verify criteria
npx vitest runผ่านทั้งชุด (รวมtests/app/api/documents/token.test.ts— route contract 400/401/403/200 ที่เพิ่ม 2026-11-04 และtests/wordaddin/ticket-auth-flow.test.tsฉบับใหม่)npx tsc --noEmitผ่าน (error เดิมของpiscina/docx-templatesไม่เกี่ยวกับแผนนี้)- curl regression ที่ /token: (ก)
x-api-key+ identity body ออก token ได้เหมือนเดิม (ข) ticket body ออก token ได้expires_in= 604800 (ค) HMAC ผิด = 401 ข้อความเดียว (ง) body ปนกัน/ไม่ส่ง ticket = 400 (จ) exp อดีตลายเซ็นถูก = 403 - ทดสอบใน Word จริง (sideload ตาม
docs/WORDADDIN-DEV.md) ด้วยไฟล์จาก make-ticket-docx: ไฟล์ถูกต้อง → ใช้ได้ ชื่อผู้ใช้ = uname · dev log เห็น POST /token ทุกครั้งที่เปิด task pane · แก้ticket_data1 ตัวอักษร → no-access ข้อความลายเซ็น (server 401) · ticket หมดอายุ + มี session (JWT 7 วันยังไม่หมด) → ผ่านด้วย JWT เดิม (Network เห็น 403) · ticket หมดอายุ + เคลียร์ localStoragewordaddin-auth→ no-access "ticket หมดอายุ" (แก้ 2026-11-04: ไม่มีทางกลับจากหลักฐาน DB อีก) · หยุด dev server → no-access "ขอ token ไม่สำเร็จ" · ตั้งDOCVER_TOKEN_TTL_SECONDSสั้น ๆ แล้วใช้จน JWT หมด → apiClient renew อัตโนมัติ (ticket ยังไม่หมด) · เปิดไฟล์ uid คนละคน → session เดิมถูกเคลียร์ · browser ธรรมดา → no-access "อ่านข้อมูลเอกสารไม่สำเร็จ"- กด "ลองใหม่" ได้
- รัน
plan-reviewer+code-reviewerตามกติกา.claude/
11. ลำดับงาน
utils/docver-ticket.ts+tests/utils/docver-ticket.test.tslib/addin-ticket.ts+tests/lib/addin-ticket.test.tsapp/api/documents/token/route.ts+lib/openapi.ts→ verify 10.3 ด้วย curlwordaddin/config.ts+metadataService.ts(properties)ticketService.ts+authService.ts(rewrite)TaskpaneApp.tsx(detect + ข้อความ) +apiClient.ts(renew-retry)scripts/make-ticket-docx.mjs- env/docs/CLAUDE.md sync → verify 10.1–10.5
12. Out of scope
ปลด mock(ทำแล้ว 2026-10-27 —versioningServiceversioningServiceยิงกลุ่ม docver จริงผ่าน Bearer same-origin; mock เหลือเฉพาะ?addindemoดูdocs/WORDADDIN-DEV.md)ความไม่ตรงกันของ(ยุติแล้ว 2026-10-27 — เอกสารระบุด้วย fid ใน token ฝั่งเดียว; พารามิเตอร์ documentId ของ versioningService เหลือตาม signature เดิมไม่ส่งถึง server)documentId(GUID ที่ stamp) กับdocId(= fid)- UI จัดการ session/ออกจากระบบ, refresh token แบบมี revoke, ฝั่ง e-Court stamp (เขาทำ — เราส่ง §2 ให้)