diff --git a/.env.example b/.env.example index f8a0914..a03ea79 100644 --- a/.env.example +++ b/.env.example @@ -33,3 +33,30 @@ MINIO_SECRET_KEY=minioadmin # MINIO_BUCKET=docver # DOCVER_LINK_TTL_SECONDS=900 # DOCVER_TOKEN_TTL_SECONDS=3600 + +# (ถอดแล้ว 2026-10-30) NOTE_SESSION_SECRET ของรอบ cookie oacCaseSig — แอป note +# เปลี่ยน session เป็น token แบบ add-in แล้ว (access_token purpose=login จาก +# POST /api/note/auth/session เก็บใน localStorage "note-auth") จึงใช้ +# DOCVER_TOKEN_SECRET ตัวเดียวทั้งระบบ: เซ็น login-token ของลิงก์ + เซ็น/ตรวจ +# session token ฝั่ง Server Actions และ API ของแอป (lib/note-token.ts) + +# dev-only (ทางเลือก): เปิดฟอร์ม "สร้างลิงก์ทดสอบ" บนหน้า /note + route +# POST /api/note/auth/dev-link — mint ลิงก์ SSO จากหน้าเว็บเพื่อทดสอบ flow จริง +# ⚠️ ตัวช่วยทดสอบเท่านั้น ไม่ใช่ทางเข้าจริง: ปิด = route ตอบ 404 + ฟอร์มหายจากหน้า +# และไม่ทำงานใน production build เด็ดขาด (NODE_ENV=production) — dev server ที่แชร์ +# บน LAN เปิดไว้ = คนในวงแชร์เครือข่าย mint ลิงก์เข้าระบบได้ จึงควรเปิดเฉพาะตอนทดสอบ +# NOTE_SSO_DEV_MINT=1 + +# add-in ticket auth (2026-10-27, docs/PLAN-ADDIN-AUTH.md): add-in ใน Word ขอ token +# ที่ POST /api/documents/token ด้วย body {ticket_data, HmacSha256} ที่ e-Court stamp +# มากับไฟล์ — secret ต้องตรงกันสองฝั่ง: e-Court (ตัว stamp) กับ server นี้ (ตัว verify) +# ฝั่ง server ตรวจ HMAC ของ ticket: ADDIN_TICKET_SECRET (ไม่ตั้ง = ใช้ env SECRET +# ของกลุ่ม template → ยังไม่ตั้งอีก = "super_secret" ตาม requirement) +# ⚠️ production ต้องตั้งค่าจริง — ค่า default รู้กันหมด = ใครก็ปลอม ticket ได้ +# (ใช้ร่วมกับทาง ticket ของ SSO POST /api/note/auth/session ด้วย — PLAN-SSO.md D12) +# 2026-10-30: ทาง ticket ของ SSO (POST /api/note/auth/session) ปฏิเสธค่า default +# นี้เฉพาะใน production — dev ใช้ default ทดสอบโหมด ticket ได้เลย (เครื่องมือ dev +# อย่าง make-login-link/make-ticket-docx ก็ default ค่าเดียวกัน) +# 2026-11-04: ฝั่ง add-in ไม่มี secret อีกต่อไป (ถอด NEXT_PUBLIC_ADDIN_TICKET_SECRET +# ตาม PLAN-SSO.md D13 — การตรวจ HMAC เป็นของ server อย่างเดียว) +# ADDIN_TICKET_SECRET=super_secret diff --git a/AGENTS.md b/AGENTS.md index ba98285..a3f9174 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,12 +54,12 @@ lib/external-api.ts ← N10 ส่วนกลางของ route — parseQ lib/openapi.ts ← OpenAPI 3.0 spec ของ API ทดสอบ (กลุ่ม template + N10 + docver — 2026-10-06) — แหล่งเดียวของ Swagger UI: GET /api/template inline ลง HTML ตรง ๆ ไม่มี endpoint spec แยก; URL ใน paths เป็น path เต็ม /api/… (servers เติมแค่ basePath ตาม invariant #12) — แก้ contract ของ API ต้องมาแก้ไฟล์นี้ให้ตรงด้วย lib/document-versioning.ts ← docver pure validation (ห้าม import อะไรที่ลาก @/db หรือ minio มาด้วย — unit test tests/lib/document-versioning.test.ts ต้องรันได้เหมือน lib/external-api-auth.ts): parseVersionUploadForm (allowlist field — identity ไม่รับจาก form แล้ว แกะจาก token ผ่าน lib/docver-token.ts · **field `fields` ก็ถอดออก 2026-10-30** server แกะจากไฟล์เองที่ route ผ่าน lib/lib-tmpl.ts + message ≤2048), isDocxBytes/isPdfBytes (magic bytes), parseDownloadFormat/parseVersionIdParam, contentDispositionValue (ASCII fallback + filename*=UTF-8'') + docverError สร้าง Response JSON {"error": ไทย} ใช้ร่วมทุก route ของกลุ่ม (parseDocId ถอดแล้ว 2026-10-26 — docId ไม่มาจาก path param) lib/docver-token.ts ← docver token + auth ของกลุ่ม /api/documents/* (2026-10-22 — pure module ห้าม import อะไรที่ลาก @/db หรือ minio มาด้วย เหมือน lib/document-versioning.ts; unit test tests/lib/docver-token.test.ts ต้องรันได้โดยไม่ต้องตั้ง env) -lib/addin-ticket.ts ← add-in ticket auth ฝั่ง server ของทางเข้าที่สองของ POST /api/documents/token (2026-10-27, docs/PLAN-ADDIN-AUTH.md — pure เหมือน lib/docver-token.ts; unit test tests/lib/addin-ticket.test.ts): addinTicketSecret (env ADDIN_TICKET_SECRET → SECRET → "super_secret") + verifyAddinTicketHmac (HMAC timing-safe บน ticket_data ดิบ + parse **ไม่ตรวจ exp** — route ทางเข้า ticket ใช้ตัวนี้แล้วตัดสิน exp เอง: ticket หมดอายุยังออก token ต่อเมื่อ fid นั้นเคยมี version โดย uid เดียวกัน ผ่าน `hasVersionByUser` ของ lib/docver-store.ts — requirement ขั้น 7 แก้บั๊ก 2026-10-28, DB พัง = fail closed 401) + verifyAddinTicket (HMAC+exp เต็มชุด — ผู้เรียกเดิม) + ticketToDocverClaims (uid→userid, uname→username, tid→template) + parseTicketTokenBody (allowlist ticket_data/HmacSha256 — missing → 401 / invalid → 400): issueDocverToken/verifyDocverToken (JWT HS256 มือเขียน node:crypto ล้วน — บังคับ alg=HS256 กัน alg-confusion, timingSafeEqual หลัง hash ทั้งสองฝั่ง, exp บังคับ) + requireDocverApiKey (header x-api-key เทียบ env DOCVER_API_KEY — fail closed 401 ไม่ตั้ง/ว่าง, 401 เดียวกันทั้งไม่ใส่/ผิด กัน enumerate, ห้าม log ค่า key/header ดิบ) + requireDocverBearer (Authorization: Bearer → claims {userid, username, fid, template} — secret จาก env DOCVER_TOKEN_SECRET ไม่ตั้ง = 401, ทุกกรณี 401 ข้อความเดียวกัน) + parseTokenRequestBody (allowlist userid/username/fid/template — **fid = fileId ของ e-Court (string, บังคับ 2026-10-26)** เป็น docId ของทุก operation ตรง ๆ · verify ไม่รับ token ที่ไม่มี fid) +lib/addin-ticket.ts ← add-in ticket auth ฝั่ง server ของทางเข้าที่สองของ POST /api/documents/token (2026-10-27, docs/PLAN-ADDIN-AUTH.md — pure เหมือน lib/docver-token.ts; unit test tests/lib/addin-ticket.test.ts): addinTicketSecret (env ADDIN_TICKET_SECRET → SECRET → "super_secret") + verifyAddinTicketHmac (HMAC timing-safe บน ticket_data ดิบ + parse **ไม่ตรวจ exp** — route ทางเข้า ticket ใช้ตัวนี้แล้วตัดสิน exp เอง: ลายเซ็นถูกแต่หมดอายุ = **403** — contract 2026-11-04 เลิก `hasVersionByUser`/requirement ขั้น 7 ของ 2026-10-28) + verifyAddinTicket (HMAC+exp เต็มชุด — ผู้เรียกเดิม) + ticketToDocverClaims (uid→userid, uname→username, tid→template) + parseTicketTokenBody (allowlist ticket_data/HmacSha256 — missing กับ invalid → **400 ทั้งคู่** contract 2026-11-04): issueDocverToken/verifyDocverToken (JWT HS256 มือเขียน node:crypto ล้วน — บังคับ alg=HS256 กัน alg-confusion, timingSafeEqual หลัง hash ทั้งสองฝั่ง, exp บังคับ) + requireDocverApiKey (header x-api-key เทียบ env DOCVER_API_KEY — fail closed 401 ไม่ตั้ง/ว่าง, 401 เดียวกันทั้งไม่ใส่/ผิด กัน enumerate, ห้าม log ค่า key/header ดิบ) + requireDocverBearer (Authorization: Bearer → claims {userid, username, fid, template} — secret จาก env DOCVER_TOKEN_SECRET ไม่ตั้ง = 401, ทุกกรณี 401 ข้อความเดียวกัน) + parseTokenRequestBody (allowlist userid/username/fid/template — **fid = fileId ของ e-Court (string, บังคับ 2026-10-26)** เป็น docId ของทุก operation ตรง ๆ · verify ไม่รับ token ที่ไม่มี fid) lib/docver-store.ts ← docver server-side MinIO + DB (env MINIO_* อ่านตอนเรียกไม่ใช่ module load; client + cached ensureBucket เก็บบน globalThis แบบ db/index.ts): object key `docver//document.docx|pdf` (uuid ล้วน ไม่มี input ผู้ใช้), putVersionObjects (docx ก่อน — pdf ล้ม = ลบ docx คืนแล้วโยนต่อ), removeVersionObjects (best-effort), presignVersionUrl (TTL env DOCVER_LINK_TTL_SECONDS), insertVersionRecord (**tx เดียว**: upsert documents ON CONFLICT DO NOTHING → SELECT id FOR UPDATE → max(versionNumber)+1 → insert — จ่ายเลข version ปลอดภัยเรียกคู่ขนาน, UNIQUE(document_id, version_number) เป็น safety net) wordaddin/ ← **client-only zone ของ Word add-in** (ห้าม import จาก lib/ หรือถูก import โดย server component — พอร์ตดิบจาก D:\word-addin-poc\addin\src, spec: docs/addin-ui-flow.md, คู่มือ dev: docs/WORDADDIN-DEV.md) wordaddin/config.ts ← env NEXT_PUBLIC_ADDIN_API_BASE_URL ("" = ยังไม่ต่อ backend จริง) + NEXT_PUBLIC_ADDIN_MOCK_USERNAME (ชื่อ fallback ของ ?addindemo/ไฟล์ที่มี token ไม่มี owner) wordaddin/models/versionRecord.ts ← types VersionRecord/FieldsSnapshot + readSnapshotField (พอร์ตดิบ) -wordaddin/services/ ← documentService/fileService/metadataService/autoSaveService/versionFlow/apiClient/errors พอร์ตดิบทั้งไฟล์ + **templateService** (แท็บที่ 2 "เครื่องมือ Template" 2026-10-19 — list/select/applyTestValues + แทรกย่อหน้า-ตัวแบ่งบรรทัด-ตัวแบ่งหน้า ที่ cursor (page break มี fallback OOXML กัน E_FAIL), Office.js ล้วน — ⚠️ `contentControls` ธรรมดาเห็นเฉพาะ rich text CC: feature-detect WordApi 1.5 ใช้ `getContentControls()` เห็นทุกชนิด — helper `supportedContentControls`/`supportsAllContentControlTypes` อยู่ที่ **metadataService** แชร์กับ `readContentControlFields` (fields ที่แนบตอนสร้าง version เก็บ CC ทุกตัวแก้ 2026-10-28 — เดิมเหลือแค่ rich ตัวเดียว), ห้าม top-level Office access) · **authService + ticketService = ticket auth ของ e-Court ทางเดียว ไม่มี login แล้ว** (ยกเลิก Keycloak → pass-through token/owner → ticket 2026-10-27, `docs/PLAN-ADDIN-AUTH.md` ตาม `docs/requirement/addin-workflow.md` — surface เหลือ signInForDemo (dev-only) / signInWithTicket / getAccessToken / renewTokenFromTicket / getUsername; ไฟล์ที่ stamp custom properties `ticket_data`+`HmacSha256` มา = `detect()` ของ TaskpaneApp เรียก signInWithTicket: ตรวจ HMAC ฝั่ง client (Web Crypto + NEXT_PUBLIC_ADDIN_TICKET_SECRET — UX gate) → session localStorage `wordaddin-auth` ตาม uid (user ใหม่ = เคลียร์ทั้งก้อน) → **ต้องการ token เมื่อไหร่ยิงขอ `/api/documents/token` เสมอ แล้วให้ server ตัดสิน exp เอง (แก้ 2026-10-28 ตาม requirement ขั้น 7: ticket หมดอายุแต่ fid เคยมี version โดย uid เดียวกัน = ออกให้ — กลับมาทำเอกสารเดิมใช้ต่อได้แม้ token เก่าหมด/ localStorage หาย)** ผูก fid; ไม่มี ticket/HMAC ไม่ตรง/หมดอายุ+ไม่เคยทำงาน = phase `no-access` แยกข้อความตามสาเหตุ; apiClient โดน 401 = renew จาก ticket 1 ครั้ง (ไม่ตัด exp ฝั่ง client) ยังไม่ผ่าน = AuthRequiredError → no-access) · **versioningService = ของจริง** (ปลด mock 2026-10-27 — uploadVersion/getVersions/getDownloadLink ยิงกลุ่ม `/api/documents/*` ผ่าน apiClient (Bearer same-origin ผ่าน withBasePath — `NEXT_PUBLIC_ADDIN_API_BASE_URL` ตั้งเฉพาะวัน deploy คนละ origin ต้องรวม basePath); เอกสารระบุด้วย fid ใน token — พารามิเตอร์ documentId/templateId เหลือตาม signature เดิมไม่ส่งถึง server และ **ห้ามส่ง userId/userName/templateId ใน form (contract 2026-10-22 ส่งมา = 400) เช่นกันกับ `fields` (contract 2026-10-30 — server แกะจากไฟล์เองเก็บทั้งก้อนเหมือน return ของ `/api/template/extract` `{cc, bookmark, properties, verify}`, versionFlow จึงไม่มีขั้นอ่าน CC ฝั่ง client แล้ว)**; mock เหลือเฉพาะโหมด `?addindemo` ผ่าน `authService.isDemoMode()` — memory + metadata persist ลง localStorage ต่อ documentId, download-link คืน about:blank; unit test tests/wordaddin/versioningService.test.ts คุมทั้งสองโหมด) +wordaddin/services/ ← documentService/fileService/metadataService/autoSaveService/versionFlow/apiClient/errors พอร์ตดิบทั้งไฟล์ + **templateService** (แท็บที่ 2 "เครื่องมือ Template" 2026-10-19 — list/select/applyTestValues + แทรกย่อหน้า-ตัวแบ่งบรรทัด-ตัวแบ่งหน้า ที่ cursor (page break มี fallback OOXML กัน E_FAIL), Office.js ล้วน — ⚠️ `contentControls` ธรรมดาเห็นเฉพาะ rich text CC: feature-detect WordApi 1.5 ใช้ `getContentControls()` เห็นทุกชนิด — helper `supportedContentControls`/`supportsAllContentControlTypes` อยู่ที่ **metadataService** แชร์กับ `readContentControlFields` (fields ที่แนบตอนสร้าง version เก็บ CC ทุกตัวแก้ 2026-10-28 — เดิมเหลือแค่ rich ตัวเดียว), ห้าม top-level Office access) · **authService + ticketService = ticket auth ของ e-Court ทางเดียว ไม่มี login แล้ว** (ยกเลิก Keycloak → pass-through token/owner → ticket 2026-10-27, `docs/PLAN-ADDIN-AUTH.md` ตาม `docs/requirement/addin-workflow.md` — surface เหลือ signInForDemo (dev-only) / signInWithTicket / getAccessToken / renewTokenFromTicket / getUsername; ไฟล์ที่ stamp custom properties `ticket_data`+`HmacSha256` มา = `detect()` ของ TaskpaneApp เรียก signInWithTicket (**flow 2026-11-04 ตาม PLAN-SSO.md D13 — client ไม่ตรวจ HMAC เอง ไม่มี secret ใน bundle ถอด NEXT_PUBLIC_ADDIN_TICKET_SECRET**): pre-check ฝั่ง client (missing/ฟอร์แม็ตพัง · หมดอายุ+ไม่มี JWT ของ fid ใน session = เตะโดยไม่ยิง API) → **ยิง `/api/documents/token` ทุกครั้งที่เปิดเอกสาร** (ต้อง online) แล้วแยกตาม contract: 200 = JWT ใหม่ (user ใหม่ = เคลียร์ session `wordaddin-auth` ทั้งก้อน/คนเดิม = update userName+tokens[fid]) · 400/401 = ฟอร์แม็ต/ลายเซ็นพัง no-access · 403 = ลายเซ็นถูกแต่ ticket หมดอายุ มี JWT ของ fid ที่ยังไม่หมดใน session ก็ใช้ต่อ (เลิก hasVersionByUser — ไม่มี JWT ต้องขอไฟล์ใหม่จาก e-Court); ticketService.requestDocverToken คืน discriminated result {ok:true entry}/{kind: network|http status|malformed} ไม่โยน Error; apiClient โดน 401 = renewTokenFromTicket 1 ครั้ง (server ตอบ 403 = false) ยังไม่ผ่าน = AuthRequiredError → no-access; token TTL 7 วัน) · **versioningService = ของจริง** (ปลด mock 2026-10-27 — uploadVersion/getVersions/getDownloadLink ยิงกลุ่ม `/api/documents/*` ผ่าน apiClient (Bearer same-origin ผ่าน withBasePath — `NEXT_PUBLIC_ADDIN_API_BASE_URL` ตั้งเฉพาะวัน deploy คนละ origin ต้องรวม basePath); เอกสารระบุด้วย fid ใน token — พารามิเตอร์ documentId/templateId เหลือตาม signature เดิมไม่ส่งถึง server และ **ห้ามส่ง userId/userName/templateId ใน form (contract 2026-10-22 ส่งมา = 400) เช่นกันกับ `fields` (contract 2026-10-30 — server แกะจากไฟล์เองเก็บทั้งก้อนเหมือน return ของ `/api/template/extract` `{cc, bookmark, properties, verify}`, versionFlow จึงไม่มีขั้นอ่าน CC ฝั่ง client แล้ว)**; mock เหลือเฉพาะโหมด `?addindemo` ผ่าน `authService.isDemoMode()` — memory + metadata persist ลง localStorage ต่อ documentId, download-link คืน about:blank; unit test tests/wordaddin/versioningService.test.ts คุมทั้งสองโหมด) utils/coordinates.ts ← pixel⇄% + getPointerPercent + clamp + mergeRectsByLine utils/geometry.ts ← bbox/translate/scale/hitTestPath ของ annotation (pure, หน่วย %) utils/annotation-display.ts ← label/ข้อความย่อต่อชนิด + TYPE_SHORT/TYPE_ORDER ชื่อย่อ/ลำดับชนิด (บรรทัดสรุปหน้าของหน้าสรุป + แผงตัวกรอง explorer — ย้ายจาก 2 ที่ซ้ำกันมารวมที่นี่ 2026-09-24) + ค่า default TEXTBOX/STAMP + describeAnnotationChange สรุปสิ่งที่เปลี่ยนของ version history F19 (sidebar/สรุป/groups/CSV ใช้ร่วม) @@ -68,7 +68,7 @@ utils/pdf-export.ts ← แปลงพิกัดจอ⇄PDF (/Rotate) + wr utils/user.ts ← parse/build ค่า cookie oacCase → OacUser {userId: u_userid, userName: u_userlogin} (pure module — middleware ใช้ร่วมได้) utils/external-file.ts ← N8 validation ลิงก์ไฟล์รีโมต (allowlist "admincourt.go.th" + env EXTERNAL_FILE_HOSTS, suffix match, https เท่านั้น ห้าม user:pass@) + titleFromExternalUrl/externalIdFor/isHttpUrl + normDocumentGroup (N9 normalize รหัสกลุ่ม — ใช้ร่วม N8/N4, pure) utils/base-path.ts ← withBasePath() เติม basePath /docedit + app path /note ให้ URL ฝั่ง browser ที่ Next ไม่เติมให้เอง (ค่าจาก next.config.mjs ผ่าน env — invariant #12) -utils/docver-ticket.ts ← ticket ของ add-in ส่วน pure แชร์ client/server (2026-10-27, PLAN-ADDIN-AUTH.md §4 — ไม่ import อะไรเลย ให้สองฝั่งฉีด crypto ของตัวเอง: lib/addin-ticket.ts = node:crypto / wordaddin/services/ticketService.ts = Web Crypto): parseDocverTicket (allowlist exp/fid/tid/uid/uname + cap ยาวตาม endpoint) + isTicketExpired + normalizeHexDigest + timingSafeEqualHex + type HmacSha256Hex — unit test tests/utils/docver-ticket.test.ts +utils/docver-ticket.ts ← ticket ของ add-in ส่วน pure แชร์ client/server (2026-10-27, PLAN-ADDIN-AUTH.md §4 — ไม่ import อะไรเลย · client ไม่คำนวณ HMAC อีกต่อไปหลังถอด client HMAC 2026-11-04 ตาม PLAN-SSO.md D13 — ถอด timingSafeEqualHex/HmacSha256Hex ออก): parseDocverTicket (allowlist exp/fid/tid/uid/uname + cap ยาวตาม endpoint) + isTicketExpired + normalizeHexDigest — unit test tests/utils/docver-ticket.test.ts utils/file-kind.ts ← derive ชนิดไฟล์ pdf/image จาก extension ของ path (N3 — ใช้ร่วม viewer/export/หน้ารายการ/seed) utils/upload-name.ts ← N4 ถอดรหัสชื่อไฟล์จาก FormData ของ Server Action กลับเป็น UTF-8 ก่อนใช้ (undici ถอด header Content-Disposition เป็น latin1 ทำชื่อไทยเพี้ยน "คำ…" — encode กลับ byte แล้วถอด UTF-8; guard: ไม่มีตัว U+0080–U+00FF = ไม่แตะ, ถอดแล้วได้ U+FFFD = เก็บของเดิม, 2026-10-05) utils/tiff-client.ts ← decode TIFF ทุกเฟรม → PNG blob URL ฝั่ง client (N3) @@ -83,7 +83,7 @@ app/api/note/annotations/export/route.ts ← export PDF ฝัง annotation app/api/note/external/documents/route.ts ← N10 API ภายนอกเส้นเดียว (POST + JSON body {fileId และ/หรือ typeId, filter:{type, userId, annotationTypes}} — auth ผ่าน requireExternalApiKey ก่อนแตะ DB, ถาม fileId = ค้น files.external_id = "ecourt:" ไม่เจอ 404 แม้ส่ง typeId มาด้วย / ถาม typeId = ทุกไฟล์ในกลุ่ม กลุ่มว่าง = 200 documents:[] / ส่งทั้งคู่ = AND กรอง externalId+typeId ใน query เดียว เหลือเอกสารเดียว ไม่ตรงคู่ = 404 แยกสาเหตุในข้อความ (2026-09-25 เดิมเคยเป็น OR แล้วเป็น 400); response รูปเดียว {documents:[bundle…]} ผ่าน loadDocumentBundles ทั้งสอง mode; ครอบ try/catch ทุก DB call — ล้ม = log ฝั่ง server + 500 JSON ตาม contract ไม่ปล่อยเป็น 500 ของ framework; read-only ไม่ publish SSE ไม่ revalidatePath — snapshot ณ ตอนเรียก) app/api/swagger/route.ts ← **GET /api/swagger = หน้า Swagger UI** (2026-10-06 — document ของ API ทุกกลุ่ม: spec inline จาก lib/openapi.ts + asset จาก public/note/swagger-ui/ ของ postinstall scripts/copy-swagger-ui.mjs, persistAuthorization เก็บ x-api-key ไว้ใช้ข้าม reload — เปิดกว้างไม่ต้อง login) app/api/template/{test,extract,render}/route.ts ← กลุ่ม API docx template ของ wordaddin (WIP — GET test ตรวจ service / POST extract ดึงค่า Content Control / POST render = template(.docx)+data(JSON ไฟล์หรือ field dataJson) คืน .docx ใหม่; logic รวมอยู่ app/api/template/lib-tmpl.ts เป็นแชร์ไลบรารี pizzip+xpath; ไฟล์ทดสอบ/โน้ตอยู่ app/api/template/test/ — ไม่แตะ DB, URL /docedit/api/template/…) -app/api/documents/token/route.ts + versions/route.ts + versions/[versionId]/download-link/route.ts ← **กลุ่ม API docver จัด version เอกสาร** (docs/PLAN-DOCVER.md — URL /docedit/api/documents/…, auth ของกลุ่มเอง 2026-10-22: POST token ขอ access_token ด้วย header `x-api-key` + env `DOCVER_API_KEY` (lib/docver-token.ts — fail closed 401 ไม่ตั้ง/ว่าง เหมือน N10) แล้ว **endpoint ที่เหลือทุกตัวบังคับ `Authorization: Bearer`** JWT HS256 เซ็นด้วย env `DOCVER_TOKEN_SECRET` (TTL `DOCVER_TOKEN_TTL_SECONDS` default 3600) — identity userid/username + templateId แกะจาก claims ไม่รับจาก request อีกต่อไป; ส่ง userId/userName/templateId ใน form = 400 ไม่รู้จัก field · **2026-10-27 POST token มีทางเข้าที่สองสำหรับ add-in ใน Word** (PLAN-ADDIN-AUTH.md): ไม่มี x-api-key + body `{ticket_data, HmacSha256}` — lib/addin-ticket.ts ตรวจ HMAC+exp (secret env ADDIN_TICKET_SECRET → SECRET → default "super_secret") แล้วออก token จาก claims ของ ticket เอง · **2026-10-26 ถอด segment `[docId]` ออกจาก URL ทุก endpoint — docId = claim `fid` (string) จาก token ตรง ๆ (บังคับตั้งแต่ขอ token)**): POST versions = multipart `file`(.docx บังคับ ≤100MB เช็ค size ก่อน buffer → 413)+`pdf`(optional ไฟล์เปล่า = ไม่มี)+message/clientVersionId/fileName (**form field `fields` ยกเลิก 2026-10-30 ส่งมา = 400 — fields ของ version แกะจากไฟล์ .docx ฝั่ง server เก็บทั้งก้อนเหมือน return ของ `/api/template/extract` เลย** (`{cc, bookmark, properties, verify}` ผ่าน `extractDocumentSnapshot` ของ lib/lib-tmpl.ts ตัวเดียวกับ route นั้น · verify.HmacSha256 ตรวจด้วย secret ของ add-in ticket ฝั่ง server ผ่าน `addinTicketSecret()` · เห็น CC ทุกชนิดเสมอไม่ขึ้นกับ WordApi ของ host แบบตอน add-in อ่านเอง; แกะไม่ได้ = 400 · row เก่าก่อน 2026-10-30 เก็บ map ดิบ — `FieldsSnapshot`/`readSnapshotField` ของ add-in แยกรูปเอง) → สร้างเอกสารอัตโนมัติถ้า docId ใหม่, MinIO ก่อน DB (DB ล้มหลัง put = เก็บกวาด object แบบ N4) → `{hasPdf, versionId, versionNumber}` (เลขจาก server เป็นค่าจริง) / GET versions = DocumentVersionDTO[] ใหม่→เก่า (ไม่เคยมี docId = 404) / GET download-link?format=docx|pdf = presigned URL อายุสั้น (เช็ค docId+versionId คู่กันกันโยน id ข้ามเอกสาร; ขอ pdf ที่ version ไม่มี = 404) — carve-out invariant #4: ไม่ publish SSE ไม่ revalidatePath +app/api/documents/token/route.ts + versions/route.ts + versions/[versionId]/download-link/route.ts ← **กลุ่ม API docver จัด version เอกสาร** (docs/PLAN-DOCVER.md — URL /docedit/api/documents/…, auth ของกลุ่มเอง 2026-10-22: POST token ขอ access_token ด้วย header `x-api-key` + env `DOCVER_API_KEY` (lib/docver-token.ts — fail closed 401 ไม่ตั้ง/ว่าง เหมือน N10) แล้ว **endpoint ที่เหลือทุกตัวบังคับ `Authorization: Bearer`** JWT HS256 เซ็นด้วย env `DOCVER_TOKEN_SECRET` (TTL `DOCVER_TOKEN_TTL_SECONDS` **default 604800 = 7 วัน** — แก้ 2026-11-04 จาก 3600 ให้ token อายุยาวแทนระบบ refresh ของ add-in) — identity userid/username + templateId แกะจาก claims ไม่รับจาก request อีกต่อไป; ส่ง userId/userName/templateId ใน form = 400 ไม่รู้จัก field · **2026-10-27 POST token มีทางเข้าที่สองสำหรับ add-in ใน Word** (PLAN-ADDIN-AUTH.md — contract 2026-11-04): ไม่มี x-api-key + body `{ticket_data, HmacSha256}` — lib/addin-ticket.ts ตรวจ HMAC (secret env ADDIN_TICKET_SECRET → SECRET → default "super_secret") แล้วออก token จาก claims ของ ticket เอง โดยแยก status: **400 ฟอร์แม็ตพัง (รวม missing) / 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่ ticket หมดอายุ (client มี JWT เดิมของ fid ใน session ใช้ต่อ)** — route ไม่ import `@/db`/minio เลย (unit test tests/app/api/documents/token.test.ts) · **2026-10-26 ถอด segment `[docId]` ออกจาก URL ทุก endpoint — docId = claim `fid` (string) จาก token ตรง ๆ (บังคับตั้งแต่ขอ token)**): POST versions = multipart `file`(.docx บังคับ ≤100MB เช็ค size ก่อน buffer → 413)+`pdf`(optional ไฟล์เปล่า = ไม่มี)+message/clientVersionId/fileName (**form field `fields` ยกเลิก 2026-10-30 ส่งมา = 400 — fields ของ version แกะจากไฟล์ .docx ฝั่ง server เก็บทั้งก้อนเหมือน return ของ `/api/template/extract` เลย** (`{cc, bookmark, properties, verify}` ผ่าน `extractDocumentSnapshot` ของ lib/lib-tmpl.ts ตัวเดียวกับ route นั้น · verify.HmacSha256 ตรวจด้วย secret ของ add-in ticket ฝั่ง server ผ่าน `addinTicketSecret()` · เห็น CC ทุกชนิดเสมอไม่ขึ้นกับ WordApi ของ host แบบตอน add-in อ่านเอง; แกะไม่ได้ = 400 · row เก่าก่อน 2026-10-30 เก็บ map ดิบ — `FieldsSnapshot`/`readSnapshotField` ของ add-in แยกรูปเอง) → สร้างเอกสารอัตโนมัติถ้า docId ใหม่, MinIO ก่อน DB (DB ล้มหลัง put = เก็บกวาด object แบบ N4) → `{hasPdf, versionId, versionNumber}` (เลขจาก server เป็นค่าจริง) / GET versions = DocumentVersionDTO[] ใหม่→เก่า (ไม่เคยมี docId = 404) / GET download-link?format=docx|pdf = presigned URL อายุสั้น (เช็ค docId+versionId คู่กันกันโยน id ข้ามเอกสาร; ขอ pdf ที่ version ไม่มี = 404) — carve-out invariant #4: ไม่ publish SSE ไม่ revalidatePath app/note/uploads/[name]/route.ts ← เสิร์ฟไฟล์อัปโหลดจาก public/note/uploads/ (N4 — Next production เห็นไฟล์ static เฉพาะที่มีตอน startup ไฟล์ที่เขียน runtime ต้องเสิร์ฟผ่าน route) app/note/page.tsx ← รายการไฟล์จัด section ตามกลุ่ม type_id + "ไม่มีกลุ่ม" ท้ายสุด (N9 — server component) app/note/groups/[typeId]/page.tsx ← N9 หน้าเอกสารในกลุ่ม (server component force-dynamic — files where type_id orderBy createdAt, ไม่มี = 404; ~~หมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้"~~ ถอดออกตามคำขอผู้ใช้ 2026-09-24; **redesign 2026-09-24 ตามคำขอผู้ใช้: ไม่มี navbar** — explorer 2 คอลัมน์ ตัวกรองซ้าย + list เดียวรวม bookmark+annotation ทั้งกลุ่มไม่แยกไฟล์: query ทั้งหมดฝั่ง server แล้วส่ง `GroupFileSummary[]` + `GroupListItem[]` (เรียง ไฟล์ตาม createdAt → หน้า → bookmark ก่อน annotation → createdAt) ให้ `GroupExplorer` กรองใน memory, แถวรายการ deep link `?ann=`/`?page=` เหมือนหน้าสรุป F12 + ไฟล์ที่มาต้น meta line) diff --git a/CLAUDE.md b/CLAUDE.md index 5f39139..b902645 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,12 +58,12 @@ lib/external-api.ts ← N10 ส่วนกลางของ route — parseQ lib/openapi.ts ← OpenAPI 3.0 spec ของ API ทดสอบ (กลุ่ม template + N10 + docver — 2026-10-06) — แหล่งเดียวของ Swagger UI: GET /api/template inline ลง HTML ตรง ๆ ไม่มี endpoint spec แยก; URL ใน paths เป็น path เต็ม /api/… (servers เติมแค่ basePath ตาม invariant #12) — แก้ contract ของ API ต้องมาแก้ไฟล์นี้ให้ตรงด้วย lib/document-versioning.ts ← docver pure validation (ห้าม import อะไรที่ลาก @/db หรือ minio มาด้วย — unit test tests/lib/document-versioning.test.ts ต้องรันได้เหมือน lib/external-api-auth.ts): parseVersionUploadForm (allowlist field — identity ไม่รับจาก form แล้ว แกะจาก token ผ่าน lib/docver-token.ts · **field `fields` ก็ถอดออก 2026-10-30** server แกะจากไฟล์เองที่ route ผ่าน lib/lib-tmpl.ts + message ≤2048), isDocxBytes/isPdfBytes (magic bytes), parseDownloadFormat/parseVersionIdParam, contentDispositionValue (ASCII fallback + filename*=UTF-8'') + docverError สร้าง Response JSON {"error": ไทย} ใช้ร่วมทุก route ของกลุ่ม (parseDocId ถอดแล้ว 2026-10-26 — docId ไม่มาจาก path param) lib/docver-token.ts ← docver token + auth ของกลุ่ม /api/documents/* (2026-10-22 — pure module ห้าม import อะไรที่ลาก @/db หรือ minio มาด้วย เหมือน lib/document-versioning.ts; unit test tests/lib/docver-token.test.ts ต้องรันได้โดยไม่ต้องตั้ง env) — 2026-10-29 (PLAN-SSO.md D4): claims มี `purpose?: "docver"|"login"` (undefined = docver — token เก่าตอน roll ไม่ตาย), `issueDocverToken` รับ purpose ทาง param (default "docver"), `requireDocverBearer` ปฏิเสธ login-token (กัน scope escalation), **`verifyDocverLoginToken`** แยก reason expired/invalid + บังคับ purpose=login สำหรับ SSO route -lib/addin-ticket.ts ← add-in ticket auth ฝั่ง server ของทางเข้าที่สองของ POST /api/documents/token (2026-10-27, docs/PLAN-ADDIN-AUTH.md — pure เหมือน lib/docver-token.ts; unit test tests/lib/addin-ticket.test.ts): addinTicketSecret (env ADDIN_TICKET_SECRET → SECRET → "super_secret") + verifyAddinTicketHmac (HMAC timing-safe บน ticket_data ดิบ + parse **ไม่ตรวจ exp** — route ทางเข้า ticket ใช้ตัวนี้แล้วตัดสิน exp เอง: ticket หมดอายุยังออก token ต่อเมื่อ fid นั้นเคยมี version โดย uid เดียวกัน ผ่าน `hasVersionByUser` ของ lib/docver-store.ts — requirement ขั้น 7 แก้บั๊ก 2026-10-28, DB พัง = fail closed 401) + verifyAddinTicket (HMAC+exp เต็มชุด — ผู้เรียกเดิม) + ticketToDocverClaims (uid→userid, uname→username, tid→template) + parseTicketTokenBody (allowlist ticket_data/HmacSha256 — missing → 401 / invalid → 400): issueDocverToken/verifyDocverToken (JWT HS256 มือเขียน node:crypto ล้วน — บังคับ alg=HS256 กัน alg-confusion, timingSafeEqual หลัง hash ทั้งสองฝั่ง, exp บังคับ) + requireDocverApiKey (header x-api-key เทียบ env DOCVER_API_KEY — fail closed 401 ไม่ตั้ง/ว่าง, 401 เดียวกันทั้งไม่ใส่/ผิด กัน enumerate, ห้าม log ค่า key/header ดิบ) + requireDocverBearer (Authorization: Bearer → claims {userid, username, fid, template} — secret จาก env DOCVER_TOKEN_SECRET ไม่ตั้ง = 401, ทุกกรณี 401 ข้อความเดียวกัน) + parseTokenRequestBody (allowlist userid/username/fid/template — **fid = fileId ของ e-Court (string, บังคับ 2026-10-26)** เป็น docId ของทุก operation ตรง ๆ · verify ไม่รับ token ที่ไม่มี fid) +lib/addin-ticket.ts ← add-in ticket auth ฝั่ง server ของทางเข้าที่สองของ POST /api/documents/token (2026-10-27, docs/PLAN-ADDIN-AUTH.md — pure เหมือน lib/docver-token.ts; unit test tests/lib/addin-ticket.test.ts): addinTicketSecret (env ADDIN_TICKET_SECRET → SECRET → "super_secret") + verifyAddinTicketHmac (HMAC timing-safe บน ticket_data ดิบ + parse **ไม่ตรวจ exp** — route ทางเข้า ticket ใช้ตัวนี้แล้วตัดสิน exp เอง: ลายเซ็นถูกแต่หมดอายุ = **403** — contract 2026-11-04 เลิก `hasVersionByUser`/requirement ขั้น 7 ของ 2026-10-28) + verifyAddinTicket (HMAC+exp เต็มชุด — ผู้เรียกเดิม) + ticketToDocverClaims (uid→userid, uname→username, tid→template) + parseTicketTokenBody (allowlist ticket_data/HmacSha256 — missing กับ invalid → **400 ทั้งคู่** contract 2026-11-04): issueDocverToken/verifyDocverToken (JWT HS256 มือเขียน node:crypto ล้วน — บังคับ alg=HS256 กัน alg-confusion, timingSafeEqual หลัง hash ทั้งสองฝั่ง, exp บังคับ) + requireDocverApiKey (header x-api-key เทียบ env DOCVER_API_KEY — fail closed 401 ไม่ตั้ง/ว่าง, 401 เดียวกันทั้งไม่ใส่/ผิด กัน enumerate, ห้าม log ค่า key/header ดิบ) + requireDocverBearer (Authorization: Bearer → claims {userid, username, fid, template} — secret จาก env DOCVER_TOKEN_SECRET ไม่ตั้ง = 401, ทุกกรณี 401 ข้อความเดียวกัน) + parseTokenRequestBody (allowlist userid/username/fid/template — **fid = fileId ของ e-Court (string, บังคับ 2026-10-26)** เป็น docId ของทุก operation ตรง ๆ · verify ไม่รับ token ที่ไม่มี fid) lib/docver-store.ts ← docver server-side MinIO + DB (env MINIO_* อ่านตอนเรียกไม่ใช่ module load; client + cached ensureBucket เก็บบน globalThis แบบ db/index.ts): object key `docver//document.docx|pdf` (uuid ล้วน ไม่มี input ผู้ใช้), putVersionObjects (docx ก่อน — pdf ล้ม = ลบ docx คืนแล้วโยนต่อ), removeVersionObjects (best-effort), presignVersionUrl (TTL env DOCVER_LINK_TTL_SECONDS), insertVersionRecord (**tx เดียว**: upsert documents ON CONFLICT DO NOTHING → SELECT id FOR UPDATE → max(versionNumber)+1 → insert — จ่ายเลข version ปลอดภัยเรียกคู่ขนาน, UNIQUE(document_id, version_number) เป็น safety net) wordaddin/ ← **client-only zone ของ Word add-in** (ห้าม import จาก lib/ หรือถูก import โดย server component — พอร์ตดิบจาก D:\word-addin-poc\addin\src, spec: docs/addin-ui-flow.md, คู่มือ dev: docs/WORDADDIN-DEV.md) wordaddin/config.ts ← env NEXT_PUBLIC_ADDIN_API_BASE_URL ("" = ยังไม่ต่อ backend จริง) + NEXT_PUBLIC_ADDIN_MOCK_USERNAME (ชื่อ fallback ของ ?addindemo/ไฟล์ที่มี token ไม่มี owner) wordaddin/models/versionRecord.ts ← types VersionRecord/FieldsSnapshot + readSnapshotField (พอร์ตดิบ) -wordaddin/services/ ← documentService/fileService/metadataService/autoSaveService/versionFlow/apiClient/errors พอร์ตดิบทั้งไฟล์ + **templateService** (แท็บที่ 2 "เครื่องมือ Template" 2026-10-19 — list/select/applyTestValues + แทรกย่อหน้า-ตัวแบ่งบรรทัด-ตัวแบ่งหน้า ที่ cursor (page break มี fallback OOXML กัน E_FAIL), Office.js ล้วน — ⚠️ `contentControls` ธรรมดาเห็นเฉพาะ rich text CC: feature-detect WordApi 1.5 ใช้ `getContentControls()` เห็นทุกชนิด — helper `supportedContentControls`/`supportsAllContentControlTypes` อยู่ที่ **metadataService** แชร์กับ `readContentControlFields` (fields ที่แนบตอนสร้าง version เก็บ CC ทุกตัวแก้ 2026-10-28 — เดิมเหลือแค่ rich ตัวเดียว), ห้าม top-level Office access) · **authService + ticketService = ticket auth ของ e-Court ทางเดียว ไม่มี login แล้ว** (ยกเลิก Keycloak → pass-through token/owner → ticket 2026-10-27, `docs/PLAN-ADDIN-AUTH.md` ตาม `docs/requirement/addin-workflow.md` — surface เหลือ signInForDemo (dev-only) / signInWithTicket / getAccessToken / renewTokenFromTicket / getUsername; ไฟล์ที่ stamp custom properties `ticket_data`+`HmacSha256` มา = `detect()` ของ TaskpaneApp เรียก signInWithTicket: ตรวจ HMAC ฝั่ง client (Web Crypto + NEXT_PUBLIC_ADDIN_TICKET_SECRET — UX gate) → session localStorage `wordaddin-auth` ตาม uid (user ใหม่ = เคลียร์ทั้งก้อน) → **ต้องการ token เมื่อไหร่ยิงขอ `/api/documents/token` เสมอ แล้วให้ server ตัดสิน exp เอง (แก้ 2026-10-28 ตาม requirement ขั้น 7: ticket หมดอายุแต่ fid เคยมี version โดย uid เดียวกัน = ออกให้ — กลับมาทำเอกสารเดิมใช้ต่อได้แม้ token เก่าหมด/ localStorage หาย)** ผูก fid; ไม่มี ticket/HMAC ไม่ตรง/หมดอายุ+ไม่เคยทำงาน = phase `no-access` แยกข้อความตามสาเหตุ; apiClient โดน 401 = renew จาก ticket 1 ครั้ง (ไม่ตัด exp ฝั่ง client) ยังไม่ผ่าน = AuthRequiredError → no-access) · **versioningService = ของจริง** (ปลด mock 2026-10-27 — uploadVersion/getVersions/getDownloadLink ยิงกลุ่ม `/api/documents/*` ผ่าน apiClient (Bearer same-origin ผ่าน withBasePath — `NEXT_PUBLIC_ADDIN_API_BASE_URL` ตั้งเฉพาะวัน deploy คนละ origin ต้องรวม basePath); เอกสารระบุด้วย fid ใน token — พารามิเตอร์ documentId/templateId เหลือตาม signature เดิมไม่ส่งถึง server และ **ห้ามส่ง userId/userName/templateId ใน form (contract 2026-10-22 ส่งมา = 400) เช่นกันกับ `fields` (contract 2026-10-30 — server แกะจากไฟล์เองเก็บทั้งก้อนเหมือน return ของ `/api/template/extract` `{cc, bookmark, properties, verify}`, versionFlow จึงไม่มีขั้นอ่าน CC ฝั่ง client แล้ว)**; mock เหลือเฉพาะโหมด `?addindemo` ผ่าน `authService.isDemoMode()` — memory + metadata persist ลง localStorage ต่อ documentId, download-link คืน about:blank; unit test tests/wordaddin/versioningService.test.ts คุมทั้งสองโหมด) +wordaddin/services/ ← documentService/fileService/metadataService/autoSaveService/versionFlow/apiClient/errors พอร์ตดิบทั้งไฟล์ + **templateService** (แท็บที่ 2 "เครื่องมือ Template" 2026-10-19 — list/select/applyTestValues + แทรกย่อหน้า-ตัวแบ่งบรรทัด-ตัวแบ่งหน้า ที่ cursor (page break มี fallback OOXML กัน E_FAIL), Office.js ล้วน — ⚠️ `contentControls` ธรรมดาเห็นเฉพาะ rich text CC: feature-detect WordApi 1.5 ใช้ `getContentControls()` เห็นทุกชนิด — helper `supportedContentControls`/`supportsAllContentControlTypes` อยู่ที่ **metadataService** แชร์กับ `readContentControlFields` (fields ที่แนบตอนสร้าง version เก็บ CC ทุกตัวแก้ 2026-10-28 — เดิมเหลือแค่ rich ตัวเดียว), ห้าม top-level Office access) · **authService + ticketService = ticket auth ของ e-Court ทางเดียว ไม่มี login แล้ว** (ยกเลิก Keycloak → pass-through token/owner → ticket 2026-10-27, `docs/PLAN-ADDIN-AUTH.md` ตาม `docs/requirement/addin-workflow.md` — surface เหลือ signInForDemo (dev-only) / signInWithTicket / getAccessToken / renewTokenFromTicket / getUsername; ไฟล์ที่ stamp custom properties `ticket_data`+`HmacSha256` มา = `detect()` ของ TaskpaneApp เรียก signInWithTicket (**flow 2026-11-04 ตาม PLAN-SSO.md D13 — client ไม่ตรวจ HMAC เอง ไม่มี secret ใน bundle ถอด NEXT_PUBLIC_ADDIN_TICKET_SECRET**): pre-check ฝั่ง client (missing/ฟอร์แม็ตพัง · หมดอายุ+ไม่มี JWT ของ fid ใน session = เตะโดยไม่ยิง API) → **ยิง `/api/documents/token` ทุกครั้งที่เปิดเอกสาร** (ต้อง online) แล้วแยกตาม contract: 200 = JWT ใหม่ (user ใหม่ = เคลียร์ session `wordaddin-auth` ทั้งก้อน/คนเดิม = update userName+tokens[fid]) · 400/401 = ฟอร์แม็ต/ลายเซ็นพัง no-access · 403 = ลายเซ็นถูกแต่ ticket หมดอายุ มี JWT ของ fid ที่ยังไม่หมดใน session ก็ใช้ต่อ (เลิก hasVersionByUser — ไม่มี JWT ต้องขอไฟล์ใหม่จาก e-Court); ticketService.requestDocverToken คืน discriminated result {ok:true entry}/{kind: network|http status|malformed} ไม่โยน Error; apiClient โดน 401 = renewTokenFromTicket 1 ครั้ง (server ตอบ 403 = false) ยังไม่ผ่าน = AuthRequiredError → no-access; token TTL 7 วัน) · **versioningService = ของจริง** (ปลด mock 2026-10-27 — uploadVersion/getVersions/getDownloadLink ยิงกลุ่ม `/api/documents/*` ผ่าน apiClient (Bearer same-origin ผ่าน withBasePath — `NEXT_PUBLIC_ADDIN_API_BASE_URL` ตั้งเฉพาะวัน deploy คนละ origin ต้องรวม basePath); เอกสารระบุด้วย fid ใน token — พารามิเตอร์ documentId/templateId เหลือตาม signature เดิมไม่ส่งถึง server และ **ห้ามส่ง userId/userName/templateId ใน form (contract 2026-10-22 ส่งมา = 400) เช่นกันกับ `fields` (contract 2026-10-30 — server แกะจากไฟล์เองเก็บทั้งก้อนเหมือน return ของ `/api/template/extract` `{cc, bookmark, properties, verify}`, versionFlow จึงไม่มีขั้นอ่าน CC ฝั่ง client แล้ว)**; mock เหลือเฉพาะโหมด `?addindemo` ผ่าน `authService.isDemoMode()` — memory + metadata persist ลง localStorage ต่อ documentId, download-link คืน about:blank; unit test tests/wordaddin/versioningService.test.ts คุมทั้งสองโหมด) utils/coordinates.ts ← pixel⇄% + getPointerPercent + clamp + mergeRectsByLine utils/geometry.ts ← bbox/translate/scale/hitTestPath ของ annotation (pure, หน่วย %) utils/annotation-display.ts ← label/ข้อความย่อต่อชนิด + TYPE_SHORT/TYPE_ORDER ชื่อย่อ/ลำดับชนิด (บรรทัดสรุปหน้าของหน้าสรุป + แผงตัวกรอง explorer — ย้ายจาก 2 ที่ซ้ำกันมารวมที่นี่ 2026-09-24) + ค่า default TEXTBOX/STAMP + describeAnnotationChange สรุปสิ่งที่เปลี่ยนของ version history F19 (sidebar/สรุป/groups/CSV ใช้ร่วม) @@ -72,7 +72,7 @@ utils/pdf-export.ts ← แปลงพิกัดจอ⇄PDF (/Rotate) + wr utils/user.ts ← **ถอดทั้งไฟล์ 2026-10-30 (ฉบับ token)** — เดิม parse/build cookie oacCase + OAC_SIG_COOKIE/SESSION_MAX_AGE ของรอบ cookie utils/external-file.ts ← N8 validation ลิงก์ไฟล์รีโมต (allowlist "admincourt.go.th" + env EXTERNAL_FILE_HOSTS, suffix match, https เท่านั้น ห้าม user:pass@) + titleFromExternalUrl/externalIdFor/isHttpUrl + normDocumentGroup (N9 normalize รหัสกลุ่ม — ใช้ที่เดียวคือ N8 `registerExternalFile`, pure; ~~sanitizeNextPath~~ ถอดแล้วพร้อมหน้า login 2026-10-29) utils/base-path.ts ← withBasePath() เติม basePath /docedit + app path /note ให้ URL ฝั่ง browser ที่ Next ไม่เติมให้เอง (ค่าจาก next.config.mjs ผ่าน env — invariant #12) -utils/docver-ticket.ts ← ticket ของ add-in ส่วน pure แชร์ client/server (2026-10-27, PLAN-ADDIN-AUTH.md §4 — ไม่ import อะไรเลย ให้สองฝั่งฉีด crypto ของตัวเอง: lib/addin-ticket.ts = node:crypto / wordaddin/services/ticketService.ts = Web Crypto): parseDocverTicket (allowlist exp/fid/tid/uid/uname + cap ยาวตาม endpoint + ห้ามอักขระ & = + % ใน uid/uname — เกณฑ์เดียวกับ parseLoginTokenBody: อักขระที่ตีความต่างกันตาม context ง่าย, 2026-10-30) + isTicketExpired + normalizeHexDigest + timingSafeEqualHex + type HmacSha256Hex — unit test tests/utils/docver-ticket.test.ts +utils/docver-ticket.ts ← ticket ของ add-in ส่วน pure แชร์ client/server (2026-10-27, PLAN-ADDIN-AUTH.md §4 — ไม่ import อะไรเลย · client ไม่คำนวณ HMAC อีกต่อไปหลังถอด client HMAC 2026-11-04 ตาม PLAN-SSO.md D13 — ถอด timingSafeEqualHex/HmacSha256Hex ออก): parseDocverTicket (allowlist exp/fid/tid/uid/uname + cap ยาวตาม endpoint + ห้ามอักขระ & = + % ใน uid/uname — เกณฑ์เดียวกับ parseLoginTokenBody: อักขระที่ตีความต่างกันตาม context ง่าย, 2026-10-30) + isTicketExpired + normalizeHexDigest — unit test tests/utils/docver-ticket.test.ts utils/file-kind.ts ← derive ชนิดไฟล์ pdf/image จาก extension ของ path (N3 — ใช้ร่วม viewer/export/หน้ารายการ/seed) utils/upload-name.ts ← N4 ถอดรหัสชื่อไฟล์จาก FormData ของ Server Action กลับเป็น UTF-8 ก่อนใช้ (undici ถอด header Content-Disposition เป็น latin1 ทำชื่อไทยเพี้ยน "คำ…" — encode กลับ byte แล้วถอด UTF-8; guard: ไม่มีตัว U+0080–U+00FF = ไม่แตะ, ถอดแล้วได้ U+FFFD = เก็บของเดิม, 2026-10-05) utils/tiff-client.ts ← decode TIFF ทุกเฟรม → PNG blob URL ฝั่ง client (N3) @@ -91,7 +91,7 @@ app/api/note/annotations/export/route.ts ← export PDF ฝัง annotation app/api/note/external/documents/route.ts ← N10 API ภายนอกเส้นเดียว (POST + JSON body {fileId และ/หรือ typeId, filter:{type, userId, annotationTypes}} — auth ผ่าน requireExternalApiKey ก่อนแตะ DB, ถาม fileId = ค้น files.external_id = "ecourt:" ไม่เจอ 404 แม้ส่ง typeId มาด้วย / ถาม typeId = ทุกไฟล์ในกลุ่ม กลุ่มว่าง = 200 documents:[] / ส่งทั้งคู่ = AND กรอง externalId+typeId ใน query เดียว เหลือเอกสารเดียว ไม่ตรงคู่ = 404 แยกสาเหตุในข้อความ (2026-09-25 เดิมเคยเป็น OR แล้วเป็น 400); response รูปเดียว {documents:[bundle…]} ผ่าน loadDocumentBundles ทั้งสอง mode; ครอบ try/catch ทุก DB call — ล้ม = log ฝั่ง server + 500 JSON ตาม contract ไม่ปล่อยเป็น 500 ของ framework; read-only ไม่ publish SSE ไม่ revalidatePath — snapshot ณ ตอนเรียก) app/api/swagger/route.ts ← **GET /api/swagger = หน้า Swagger UI** (2026-10-06 — document ของ API ทุกกลุ่ม: spec inline จาก lib/openapi.ts + asset จาก public/note/swagger-ui/ ของ postinstall scripts/copy-swagger-ui.mjs, persistAuthorization เก็บ x-api-key ไว้ใช้ข้าม reload — เปิดกว้างไม่ต้อง login) app/api/template/{test,extract,render}/route.ts ← กลุ่ม API docx template ของ wordaddin (WIP — GET test ตรวจ service / POST extract ดึงค่า Content Control / POST render = template(.docx)+data(JSON ไฟล์หรือ field dataJson) คืน .docx ใหม่; logic รวมอยู่ app/api/template/lib-tmpl.ts เป็นแชร์ไลบรารี pizzip+xpath; ไฟล์ทดสอบ/โน้ตอยู่ app/api/template/test/ — ไม่แตะ DB, URL /docedit/api/template/…) -app/api/documents/token/route.ts + versions/route.ts + versions/[versionId]/download-link/route.ts ← **กลุ่ม API docver จัด version เอกสาร** (docs/PLAN-DOCVER.md — URL /docedit/api/documents/…, auth ของกลุ่มเอง 2026-10-22: POST token ขอ access_token ด้วย header `x-api-key` + env `DOCVER_API_KEY` (lib/docver-token.ts — fail closed 401 ไม่ตั้ง/ว่าง เหมือน N10) แล้ว **endpoint ที่เหลือทุกตัวบังคับ `Authorization: Bearer`** JWT HS256 เซ็นด้วย env `DOCVER_TOKEN_SECRET` (TTL `DOCVER_TOKEN_TTL_SECONDS` default 3600) — identity userid/username + templateId แกะจาก claims ไม่รับจาก request อีกต่อไป; ส่ง userId/userName/templateId ใน form = 400 ไม่รู้จัก field · **2026-10-27 POST token มีทางเข้าที่สองสำหรับ add-in ใน Word** (PLAN-ADDIN-AUTH.md): ไม่มี x-api-key + body `{ticket_data, HmacSha256}` — lib/addin-ticket.ts ตรวจ HMAC+exp (secret env ADDIN_TICKET_SECRET → SECRET → default "super_secret") แล้วออก token จาก claims ของ ticket เอง · **2026-10-26 ถอด segment `[docId]` ออกจาก URL ทุก endpoint — docId = claim `fid` (string) จาก token ตรง ๆ (บังคับตั้งแต่ขอ token)**): POST versions = multipart `file`(.docx บังคับ ≤100MB เช็ค size ก่อน buffer → 413)+`pdf`(optional ไฟล์เปล่า = ไม่มี)+message/clientVersionId/fileName (**form field `fields` ยกเลิก 2026-10-30 ส่งมา = 400 — fields ของ version แกะจากไฟล์ .docx ฝั่ง server เก็บทั้งก้อนเหมือน return ของ `/api/template/extract` เลย** (`{cc, bookmark, properties, verify}` ผ่าน `extractDocumentSnapshot` ของ lib/lib-tmpl.ts ตัวเดียวกับ route นั้น · verify.HmacSha256 ตรวจด้วย secret ของ add-in ticket ฝั่ง server ผ่าน `addinTicketSecret()` · เห็น CC ทุกชนิดเสมอไม่ขึ้นกับ WordApi ของ host แบบตอน add-in อ่านเอง; แกะไม่ได้ = 400 · row เก่าก่อน 2026-10-30 เก็บ map ดิบ — `FieldsSnapshot`/`readSnapshotField` ของ add-in แยกรูปเอง) → สร้างเอกสารอัตโนมัติถ้า docId ใหม่, MinIO ก่อน DB (DB ล้มหลัง put = เก็บกวาด object แบบ N4) → `{hasPdf, versionId, versionNumber}` (เลขจาก server เป็นค่าจริง) / GET versions = DocumentVersionDTO[] ใหม่→เก่า (ไม่เคยมี docId = 404) / GET download-link?format=docx|pdf = presigned URL อายุสั้น (เช็ค docId+versionId คู่กันกันโยน id ข้ามเอกสาร; ขอ pdf ที่ version ไม่มี = 404) — carve-out invariant #4: ไม่ publish SSE ไม่ revalidatePath +app/api/documents/token/route.ts + versions/route.ts + versions/[versionId]/download-link/route.ts ← **กลุ่ม API docver จัด version เอกสาร** (docs/PLAN-DOCVER.md — URL /docedit/api/documents/…, auth ของกลุ่มเอง 2026-10-22: POST token ขอ access_token ด้วย header `x-api-key` + env `DOCVER_API_KEY` (lib/docver-token.ts — fail closed 401 ไม่ตั้ง/ว่าง เหมือน N10) แล้ว **endpoint ที่เหลือทุกตัวบังคับ `Authorization: Bearer`** JWT HS256 เซ็นด้วย env `DOCVER_TOKEN_SECRET` (TTL `DOCVER_TOKEN_TTL_SECONDS` **default 604800 = 7 วัน** — แก้ 2026-11-04 จาก 3600 ให้ token อายุยาวแทนระบบ refresh ของ add-in) — identity userid/username + templateId แกะจาก claims ไม่รับจาก request อีกต่อไป; ส่ง userId/userName/templateId ใน form = 400 ไม่รู้จัก field · **2026-10-27 POST token มีทางเข้าที่สองสำหรับ add-in ใน Word** (PLAN-ADDIN-AUTH.md — contract 2026-11-04): ไม่มี x-api-key + body `{ticket_data, HmacSha256}` — lib/addin-ticket.ts ตรวจ HMAC (secret env ADDIN_TICKET_SECRET → SECRET → default "super_secret") แล้วออก token จาก claims ของ ticket เอง โดยแยก status: **400 ฟอร์แม็ตพัง (รวม missing) / 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่ ticket หมดอายุ (client มี JWT เดิมของ fid ใน session ใช้ต่อ)** — route ไม่ import `@/db`/minio เลย (unit test tests/app/api/documents/token.test.ts) · **2026-10-26 ถอด segment `[docId]` ออกจาก URL ทุก endpoint — docId = claim `fid` (string) จาก token ตรง ๆ (บังคับตั้งแต่ขอ token)**): POST versions = multipart `file`(.docx บังคับ ≤100MB เช็ค size ก่อน buffer → 413)+`pdf`(optional ไฟล์เปล่า = ไม่มี)+message/clientVersionId/fileName (**form field `fields` ยกเลิก 2026-10-30 ส่งมา = 400 — fields ของ version แกะจากไฟล์ .docx ฝั่ง server เก็บทั้งก้อนเหมือน return ของ `/api/template/extract` เลย** (`{cc, bookmark, properties, verify}` ผ่าน `extractDocumentSnapshot` ของ lib/lib-tmpl.ts ตัวเดียวกับ route นั้น · verify.HmacSha256 ตรวจด้วย secret ของ add-in ticket ฝั่ง server ผ่าน `addinTicketSecret()` · เห็น CC ทุกชนิดเสมอไม่ขึ้นกับ WordApi ของ host แบบตอน add-in อ่านเอง; แกะไม่ได้ = 400 · row เก่าก่อน 2026-10-30 เก็บ map ดิบ — `FieldsSnapshot`/`readSnapshotField` ของ add-in แยกรูปเอง) → สร้างเอกสารอัตโนมัติถ้า docId ใหม่, MinIO ก่อน DB (DB ล้มหลัง put = เก็บกวาด object แบบ N4) → `{hasPdf, versionId, versionNumber}` (เลขจาก server เป็นค่าจริง) / GET versions = DocumentVersionDTO[] ใหม่→เก่า (ไม่เคยมี docId = 404) / GET download-link?format=docx|pdf = presigned URL อายุสั้น (เช็ค docId+versionId คู่กันกันโยน id ข้ามเอกสาร; ขอ pdf ที่ version ไม่มี = 404) — carve-out invariant #4: ไม่ publish SSE ไม่ revalidatePath app/note/uploads/[name]/route.ts ← เสิร์ฟไฟล์อัปโหลดจาก public/note/uploads/ (N4 — Next production เห็นไฟล์ static เฉพาะที่มีตอน startup ไฟล์ที่เขียน runtime ต้องเสิร์ฟผ่าน route) app/note/page.tsx ← **หน้าแรก = ข้อมูลเกี่ยวกับระบบ + client bootstrap ของลิงก์ e-Court** (2026-10-30 รอบสอง ตามคำขอผู้ใช้ — server shell บาง ๆ เนื้อหาทั้งหมดอยู่ใน `components/NoteHome.tsx`): "ระบบนี้ทำอะไรได้" (การ์ด 4 ใบ: มาร์ก/วาด, ความเห็น/ทำงานร่วม, คั่นหน้า/สรุป, เอกสารจาก e-Court) + วิธีเข้าใช้ (ลิงก์ e-Court, session 8 ชม.) + ลิงก์ /note/dev · มี credential ใน URL (t/ticket) = แลก token ที่ POST /api/note/auth/session → เก็บ localStorage → ตัด credential ทิ้ง → มี fileId พาเข้า viewer · แบนเนอร์ sso/fileError · public app/note/dev/page.tsx ← **เครื่องมือทดสอบ** (ย้ายมาจากหน้าแรก 2026-10-30): เปิดเอกสารตัวอย่าง (PLAN-SSO.md D14 2026-10-29 — dropdown 2 ระดับกลุ่ม→ไฟล์ รายการ fixed จาก public/note/samples/ ไม่ query DB + รายการลิงก์ GET แบบ /tmpl "A Link Get(new)" ชี้ `/api/note/files/open?fileUrl=/samples/…` เมื่อ mint ปิด / dev-link เมื่อ mint เปิด + section Flow ของแอป — 2026-10-30 รอบสอง) + **ฟอร์ม `MockEcourtForm` "จำลอง e-Court — ขอ token ด้วย API key" (2026-11-02, แสดงทุก environment)** — ใส่ `DOCVER_API_KEY` + uid/uname + เลือกไฟล์ → ยิง `POST /api/note/auth/token` (ทางเข้าจริงเหมือน e-Court) → redirect เข้า `/note?fileUrl=…&t=…` ให้ bootstrap แลก session ต่อ · **คู่มือทางเข้า SSO 2 แบบ + flow แต่ละแบบ** (การ์ดทาง A JWT/ทาง B ticket + กล่อง dev `make-login-link` — ตัวอย่างเป็นรูปแบบ+ขั้นตอนเท่านั้น **ห้าม mint จริงจากหน้า public**) + ฟอร์ม mint ลิงก์ `DevLoginLinkForm` · **ทางเข้าหน้าคุมด้วย env `NOTE_SSO_DEV_MINT=1` อย่างเดียว (2026-11-02 — ใช้ได้ทุก environment รวม production ที่ตั้ง env ไว้) · 0/ไม่ตั้ง = `notFound()` 404 ทั้งหน้า ไม่ใช่หน้า public อีกต่อไป** · แบนเนอร์ `?sso=`/`?fileUrl=` ไม่อยู่ที่หน้านี้ (คงอยู่ที่ `/note` ตาม contract) diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts new file mode 100644 index 0000000..a41b378 --- /dev/null +++ b/app/api/auth/session/route.ts @@ -0,0 +1,106 @@ +// POST /api/auth/session (path สั้นของ POST /api/note/auth/session — โค้ดก้อนเดียวกัน +// copy มา 2026-11-05) — แลก credential ของลิงก์ e-Court เป็น access_token +// (ฉบับ token 2026-10-30 ตามคำขอผู้ใช้ "note ใช้แบบ add-in" — แทน GET e-court ที่ +// set cookie คู่ oacCase ซึ่งถอดแล้ว): body {t | ticket_data+HmacSha256, fileUrl?, +// typeId?} → verify → ลงทะเบียนเอกสาร N8 แบบ trusted ฝั่ง server เลย (fileId จาก +// fid ที่เซ็นมา — ไม่ต้องมี link proof อีก) → ออก login-token อายุ NOTE_SESSION_TTL +// (8 ชม.) ให้ client เก็บ localStorage "note-auth" +// · 400 body ผิด · 401 {error, reason: expired|invalid} · 500 secret ไม่ตั้ง +// · audit log เฉพาะ userid (strip อักขระควบคุม) — ห้าม log token/query ดิบ +import { NextResponse } from "next/server"; + +import { addinTicketSecret, verifyAddinTicket } from "@/lib/addin-ticket"; +import { issueDocverToken, verifyDocverLoginToken } from "@/lib/docver-token"; +import { registerExternalFile } from "@/lib/external-import"; +import { parseSessionExchangeBody } from "@/lib/note-auth"; +import { NOTE_SESSION_TTL } from "@/lib/note-token"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +function json(body: unknown, status = 200): NextResponse { + return NextResponse.json(body, { status, headers: { "cache-control": "no-store" } }); +} + +export async function POST(request: Request): Promise { + let raw: unknown; + try { + raw = await request.json(); + } catch { + raw = null; + } + const parsed = parseSessionExchangeBody(raw); + if (!parsed.ok) return json({ error: parsed.error }, 400); + + const secret = process.env.DOCVER_TOKEN_SECRET?.trim(); + if (secret === undefined || secret === "") { + console.error("[sso] DOCVER_TOKEN_SECRET ยังไม่ถูกตั้งค่า"); + return json({ error: "ระบบยังไม่พร้อมให้เข้าสู่ระบบ" }, 500); + } + + let identity: { userid: string; username: string; fid: string }; + if (parsed.credential.kind === "jwt") { + const verified = verifyDocverLoginToken(parsed.credential.token, secret); + if (!verified.ok) { + return json( + { error: "ลิงก์เข้าสู่ระบบหมดอายุหรือไม่ถูกต้อง", reason: verified.reason }, + 401, + ); + } + identity = verified.claims; + } else { + // ค่า default "super_secret" ของ addinTicketSecret() เป็นที่รู้กันทั้ง repo/ + // .env.example — ใครก็ปลอม ticket ของ uid ใครก็ได้ จึงปฏิเสธใน production + // (fail-closed — กลุ่ม docver/add-in อนุญาต default ตาม requirement อยู่แล้ว + // คนละประตู) · **dev ยอมรับค่า default** เพื่อให้ทดสอบทาง ticket รอบเครื่องได้ + // โดยไม่ต้องตั้ง env ใหม่ — เครื่องมือ dev (make-login-link/make-ticket-docx/ + // dev-link) ก็ default ค่าเดียวกัน (review 2026-10-30 รอบสอง) + if (process.env.NODE_ENV === "production" && addinTicketSecret() === "super_secret") { + console.error("[sso] ticket secret ยังเป็นค่า default (ADDIN_TICKET_SECRET/SECRET) — ปฏิเสธทาง ticket"); + return json({ error: "ระบบยังไม่พร้อมให้เข้าสู่ระบบ" }, 500); + } + const verified = verifyAddinTicket( + parsed.credential.ticketData, + parsed.credential.hmacHex, + addinTicketSecret(), + ); + if (!verified.ok) { + return json({ error: "ลิงก์เข้าสู่ระบบหมดอายุหรือไม่ถูกต้อง", reason: "invalid" }, 401); + } + identity = { userid: verified.ticket.uid, username: verified.ticket.uname, fid: verified.ticket.fid }; + } + + // N8: ลงทะเบียนเอกสารตอนแลก token เลย — ฝั่ง server เชื่อ fid จาก credential + // ที่ verify แล้วเท่านั้น (trusted) ส่วน ?fileUrl พิมพ์เองบนหน้าเว็บไม่มีทาง + // เข้ามาถึงจุดนี้ · ล้ม = ยังออก token ให้ + fileError ให้หน้าแรกแสดง (คู่ขนาน + // พฤติกรรมสมัย cookie: session ตั้งก่อน แล้วค่อยแจ้ง error ของไฟล์) + let fileId: string | undefined; + let fileError: string | undefined; + if (parsed.fileUrl !== undefined) { + const result = await registerExternalFile(parsed.fileUrl, identity.fid, parsed.typeId, { + trusted: true, + }); + if (result.ok) fileId = result.fileId; + else fileError = result.error; + } + + const access_token = issueDocverToken( + { userid: identity.userid, username: identity.username, fid: identity.fid, template: null }, + secret, + NOTE_SESSION_TTL, + "login", + ); + // audit: เฉพาะ userid + เวลา — ห้าม log token/query ดิบ · strip อักขระควบคุม + // ก่อนพิมพ์ (claims ไม่จำกัด charset — กันฝัง \n ปลอมบรรทัด log) + const auditId = identity.userid.replace(/[\u0000-\u001f\u007f-\u009f]/g, ""); + console.log(`[sso] e-Court login ok: userid=${auditId}`); + return json({ + access_token, + token_type: "Bearer", + expires_in: NOTE_SESSION_TTL, + userid: identity.userid, + username: identity.username, + ...(fileId !== undefined ? { fileId } : {}), + ...(fileError !== undefined ? { fileError } : {}), + }); +} diff --git a/app/api/auth/token/route.ts b/app/api/auth/token/route.ts new file mode 100644 index 0000000..5a9241f --- /dev/null +++ b/app/api/auth/token/route.ts @@ -0,0 +1,113 @@ +// PLAN-DOCVER.md §auth — POST /api/auth/token (path สั้นของ POST /api/documents/token +// — โค้ดก้อนเดียวกัน copy มา 2026-11-05) ออก access_token ของกลุ่ม docver +// ทางเข้า 2 ทาง (PLAN-ADDIN-AUTH.md §3 — header x-api-key เป็นตัวตั้ง): +// 1) e-Court server-to-server: header "x-api-key" เทียบ env DOCVER_API_KEY +// (fail closed เมื่อ env ไม่ตั้ง/ว่าง) + body claims ตรง ๆ (เดิมทุกอย่าง) +// 2) add-in ใน Word: ไม่มี x-api-key + body {ticket_data, HmacSha256} — server +// ตรวจ HMAC ด้วย env ฝั่ง server (ADDIN_TICKET_SECRET → SECRET → +// "super_secret", lib/addin-ticket.ts) แล้วออก token จาก claims ของ ticket +// (secret ตัวเซ็น JWT ไม่มีทางไปอยู่ใน bundle ของ add-in — client ไม่ตรวจ HMAC +// เองแล้วตาม PLAN-SSO.md D13) · contract 2026-11-04: 400 = ฟอร์แม็ตพัง +// (รวม missing) / 401 = ลายเซ็นไม่ถูก / 403 = ลายเซ็นถูกแต่ ticket หมดอายุ +// (client ที่มี JWT เดิมของ fid นี้ใน session ใช้ต่อเอง — ไม่มีการออกซ้ำ +// จากหลักฐาน DB อีกต่อไป เลิก requirement ขั้น 7 ฉบับ 2026-10-28) +// ทั้งสองทางออก JWT HS256 เซ็นด้วย env DOCVER_TOKEN_SECRET อายุตาม env +// DOCVER_TOKEN_TTL_SECONDS (default 604800 = 7 วัน — token อายุยาวแทนระบบ +// refresh/renew) แล้วนำไปแนบ Authorization: Bearer กับ endpoint อื่นของกลุ่ม +// (lib/docver-token.ts) · segment คงที่ "token" ไม่ชน [docId] +// (Next ให้ static ชนะ) และ middleware ยกเว้น api/documents/* อยู่แล้ว +import { docverError } from "@/lib/document-versioning"; +import { + addinTicketSecret, + parseTicketTokenBody, + ticketToDocverClaims, + verifyAddinTicketHmac, +} from "@/lib/addin-ticket"; +import { isTicketExpired } from "@/utils/docver-ticket"; +import { + issueDocverToken, + parseTokenRequestBody, + requireDocverApiKey, + type DocverTokenClaims, +} from "@/lib/docver-token"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +/** 401 ของทางเข้า ticket — ข้อความเดียวทุกสาเหตุ (HMAC ผิด/รูปผิด) กัน oracle · หมดอายุแยกเป็น 403 */ +const TICKET_DENIED = "ticket ไม่ถูกต้อง"; +/** 403 ของทางเข้า ticket — ลายเซ็นถูกแต่หมดอายุ (client มี JWT เดิมใน session ก็ใช้ต่อได้) */ +const TICKET_EXPIRED = "ticket ของ e-Court หมดอายุ"; + +export async function POST(request: Request) { + return request.headers.get("x-api-key") !== null + ? postWithApiKey(request) + : postWithTicket(request); +} + +/** ทางเข้า 1 (เดิม) — e-Court server-to-server */ +async function postWithApiKey(request: Request): Promise { + const apiKeyFail = requireDocverApiKey(request); + if (apiKeyFail !== null) return apiKeyFail; + + let raw: unknown; + try { + raw = await request.json(); + } catch { + return docverError("body ต้องเป็น JSON", 400); + } + const parsed = parseTokenRequestBody(raw); + if (!parsed.ok) return docverError(parsed.error, 400); + return issueToken(parsed.claims); +} + +/** ทางเข้า 2 — add-in ส่ง ticket ที่ e-Court stamp มากับไฟล์ */ +async function postWithTicket(request: Request): Promise { + let raw: unknown; + try { + raw = await request.json(); + } catch { + return docverError("body ต้องเป็น JSON", 400); + } + const parsed = parseTicketTokenBody(raw); + if (!parsed.ok) { + // contract 2026-11-04: ฟอร์แม็ตพัง = 400 ทั้ง missing/invalid (เดิม missing เป็น 401) + return docverError(parsed.error, 400); + } + // ตรวจ HMAC ก่อนเสมอ (ตัวตนของผู้ถือไฟล์) — exp ไม่ใช่เกณฑ์ตัดสิทธิ์ ณ จุดนี้ + const verified = verifyAddinTicketHmac(parsed.ticketData, parsed.hmacHex, addinTicketSecret()); + if (!verified.ok) return docverError(TICKET_DENIED, 401); + + // ลายเซ็นถูกแต่ ticket หมดอายุ → 403 เสมอ (contract 2026-11-04 — เลิก requirement + // ขั้น 7 ฉบับ 2026-10-28: ไม่ตรวจ hasVersionByUser อีกต่อไป เพราะ ticket ฝังอยู่ใน + // ไฟล์ ใครได้ไฟล์ก็ได้ ticket การปล่อยให้ ticket เก่าแลก token ใหม่ได้ตลอดไป + // เท่ากันคือปล่อยสิทธิ์ตามการครอบครองไฟล์ — ฝั่ง client ที่มี JWT ของ fid นี้ + // ใน session ที่ยังไม่หมด (7 วัน) ใช้ต่อเอง ไม่มี = ขอไฟล์ใหม่จาก e-Court) + if (isTicketExpired(verified.ticket.exp)) { + return docverError(TICKET_EXPIRED, 403); + } + return issueToken(ticketToDocverClaims(verified.ticket)); +} + +/** ออก JWT ของ claims ที่ผ่านการยืนยันแล้วจากทั้งสองทาง — ส่วนกลาง */ +async function issueToken(claims: DocverTokenClaims): Promise { + // ความผิด config ฝั่ง server ไม่ใช่ความผิดของผู้เรียก — ไม่ตั้ง = 500 (ห้าม log ค่า secret) + const secret = process.env.DOCVER_TOKEN_SECRET?.trim(); + if (secret === undefined || secret === "") { + return docverError("DOCVER_TOKEN_SECRET ยังไม่ถูกตั้งค่า", 500); + } + const ttlRaw = Number(process.env.DOCVER_TOKEN_TTL_SECONDS); + // default 7 วัน (2026-11-04) — token อายุยาวแทนระบบ refresh/renew ของ add-in + const ttlSeconds = + Number.isFinite(ttlRaw) && ttlRaw > 0 ? Math.floor(ttlRaw) : 604_800; + + const accessToken = issueDocverToken(claims, secret, ttlSeconds); + return new Response( + JSON.stringify({ + access_token: accessToken, + token_type: "Bearer", + expires_in: ttlSeconds, + }), + { headers: { "content-type": "application/json; charset=utf-8" } }, + ); +} diff --git a/app/api/documents/token/route.ts b/app/api/documents/token/route.ts index 7385650..9542f01 100644 --- a/app/api/documents/token/route.ts +++ b/app/api/documents/token/route.ts @@ -5,11 +5,15 @@ // 2) add-in ใน Word: ไม่มี x-api-key + body {ticket_data, HmacSha256} — server // ตรวจ HMAC ด้วย env ฝั่ง server (ADDIN_TICKET_SECRET → SECRET → // "super_secret", lib/addin-ticket.ts) แล้วออก token จาก claims ของ ticket -// (secret ตัวเซ็น JWT ไม่มีทางไปอยู่ใน bundle ของ add-in) — ticket หมดอายุ -// ยังออกให้ต่อเมื่อ fid นี้เคยมี version โดย uid เดียวกัน (requirement ขั้น 7) +// (secret ตัวเซ็น JWT ไม่มีทางไปอยู่ใน bundle ของ add-in — client ไม่ตรวจ HMAC +// เองแล้วตาม PLAN-SSO.md D13) · contract 2026-11-04: 400 = ฟอร์แม็ตพัง +// (รวม missing) / 401 = ลายเซ็นไม่ถูก / 403 = ลายเซ็นถูกแต่ ticket หมดอายุ +// (client ที่มี JWT เดิมของ fid นี้ใน session ใช้ต่อเอง — ไม่มีการออกซ้ำ +// จากหลักฐาน DB อีกต่อไป เลิก requirement ขั้น 7 ฉบับ 2026-10-28) // ทั้งสองทางออก JWT HS256 เซ็นด้วย env DOCVER_TOKEN_SECRET อายุตาม env -// DOCVER_TOKEN_TTL_SECONDS (default 3600) แล้วนำไปแนบ Authorization: Bearer กับ -// endpoint อื่นของกลุ่ม (lib/docver-token.ts) · segment คงที่ "token" ไม่ชน [docId] +// DOCVER_TOKEN_TTL_SECONDS (default 604800 = 7 วัน — token อายุยาวแทนระบบ +// refresh/renew) แล้วนำไปแนบ Authorization: Bearer กับ endpoint อื่นของกลุ่ม +// (lib/docver-token.ts) · segment คงที่ "token" ไม่ชน [docId] // (Next ให้ static ชนะ) และ middleware ยกเว้น api/documents/* อยู่แล้ว import { docverError } from "@/lib/document-versioning"; import { @@ -18,7 +22,6 @@ import { ticketToDocverClaims, verifyAddinTicketHmac, } from "@/lib/addin-ticket"; -import { hasVersionByUser } from "@/lib/docver-store"; import { isTicketExpired } from "@/utils/docver-ticket"; import { issueDocverToken, @@ -30,8 +33,10 @@ import { export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -/** 401 ของทางเข้า ticket — ข้อความเดียวทุกสาเหตุ (HMAC ผิด/รูปผิด/หมดอายุ/ไม่ส่งมา) กัน oracle */ -const TICKET_DENIED = "ticket ไม่ถูกต้องหรือหมดอายุ"; +/** 401 ของทางเข้า ticket — ข้อความเดียวทุกสาเหตุ (HMAC ผิด/รูปผิด) กัน oracle · หมดอายุแยกเป็น 403 */ +const TICKET_DENIED = "ticket ไม่ถูกต้อง"; +/** 403 ของทางเข้า ticket — ลายเซ็นถูกแต่หมดอายุ (client มี JWT เดิมใน session ก็ใช้ต่อได้) */ +const TICKET_EXPIRED = "ticket ของ e-Court หมดอายุ"; export async function POST(request: Request) { return request.headers.get("x-api-key") !== null @@ -65,25 +70,20 @@ async function postWithTicket(request: Request): Promise { } const parsed = parseTicketTokenBody(raw); if (!parsed.ok) { - // missing = ไม่มี ticket มาเล่น ๆ (401 เดียวกับตอน verify ไม่ผ่าน) - // invalid = รูป body ผิดจริง ๆ ให้ 400 ตาม convention กัน typo เงียบ ๆ - return docverError(parsed.error, parsed.kind === "invalid" ? 400 : 401); + // contract 2026-11-04: ฟอร์แม็ตพัง = 400 ทั้ง missing/invalid (เดิม missing เป็น 401) + return docverError(parsed.error, 400); } // ตรวจ HMAC ก่อนเสมอ (ตัวตนของผู้ถือไฟล์) — exp ไม่ใช่เกณฑ์ตัดสิทธิ์ ณ จุดนี้ const verified = verifyAddinTicketHmac(parsed.ticketData, parsed.hmacHex, addinTicketSecret()); if (!verified.ok) return docverError(TICKET_DENIED, 401); - // ticket หมดอายุ → ยังออก token ให้ต่อเมื่อ "เอกสารนี้เคยทำงานโดย uid เดียวกัน" - // (requirement ขั้น 7: กลับมาทำเอกสารเดิมผ่านได้แม้ ticket หมดอายุ — พิสูจน์จาก - // document_versions ของกลุ่มนี้เอง) · DB พัง = fail closed เป็น 401 เหมือนไม่มีสิทธิ์ + // ลายเซ็นถูกแต่ ticket หมดอายุ → 403 เสมอ (contract 2026-11-04 — เลิก requirement + // ขั้น 7 ฉบับ 2026-10-28: ไม่ตรวจ hasVersionByUser อีกต่อไป เพราะ ticket ฝังอยู่ใน + // ไฟล์ ใครได้ไฟล์ก็ได้ ticket การปล่อยให้ ticket เก่าแลก token ใหม่ได้ตลอดไป + // เท่ากันคือปล่อยสิทธิ์ตามการครอบครองไฟล์ — ฝั่ง client ที่มี JWT ของ fid นี้ + // ใน session ที่ยังไม่หมด (7 วัน) ใช้ต่อเอง ไม่มี = ขอไฟล์ใหม่จาก e-Court) if (isTicketExpired(verified.ticket.exp)) { - let workedBefore = false; - try { - workedBefore = await hasVersionByUser(verified.ticket.fid, verified.ticket.uid); - } catch { - workedBefore = false; - } - if (!workedBefore) return docverError(TICKET_DENIED, 401); + return docverError(TICKET_EXPIRED, 403); } return issueToken(ticketToDocverClaims(verified.ticket)); } @@ -96,8 +96,9 @@ async function issueToken(claims: DocverTokenClaims): Promise { return docverError("DOCVER_TOKEN_SECRET ยังไม่ถูกตั้งค่า", 500); } const ttlRaw = Number(process.env.DOCVER_TOKEN_TTL_SECONDS); + // default 7 วัน (2026-11-04) — token อายุยาวแทนระบบ refresh/renew ของ add-in const ttlSeconds = - Number.isFinite(ttlRaw) && ttlRaw > 0 ? Math.floor(ttlRaw) : 3600; + Number.isFinite(ttlRaw) && ttlRaw > 0 ? Math.floor(ttlRaw) : 604_800; const accessToken = issueDocverToken(claims, secret, ttlSeconds); return new Response( diff --git a/components/wordaddin/TaskpaneApp.tsx b/components/wordaddin/TaskpaneApp.tsx index f3ec027..957e29a 100644 --- a/components/wordaddin/TaskpaneApp.tsx +++ b/components/wordaddin/TaskpaneApp.tsx @@ -114,6 +114,10 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) => /** ข้อความบนหน้า no-access — ต่างกันตามสาเหตุ (ไม่มี token ในไฟล์ / token หมดอายุ) */ const [noAccessNotice, setNoAccessNotice] = useState(null); + /** no-access ที่ "ลองใหม่" ช่วยได้ — เฉพาะเคสอ่านไฟล์ไม่สำเร็จ (เช่น office.js มาช้า + * บนเครื่องช้า หรือ Word สะดุดชั่วคราว) กดแล้ววน detect() ใหม่ · เคสอื่น (ticket + * หมดอายุ/ลายเซ็นไม่ผ่าน/ขอ token ไม่ได้) retry ไม่ช่วย ต้องขอไฟล์ใหม่/เน็ตกลับมา */ + const [noAccessRetryable, setNoAccessRetryable] = useState(false); const [starting, setStarting] = useState(false); const [saving, setSaving] = useState(false); @@ -183,6 +187,7 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) => const detect = useCallback(async () => { setPhase("checking"); setBootError(null); + setNoAccessRetryable(false); try { let properties: metadataService.TrackedDocumentProperties | null = null; try { @@ -191,7 +196,8 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) => properties = null; } if (!properties) { - setNoAccessNotice("อ่านข้อมูลเอกสารไม่สำเร็จ — ลองปิดแล้วเปิด task pane อีกครั้ง"); + setNoAccessNotice("อ่านข้อมูลเอกสารไม่สำเร็จ — ลองกด \"ลองใหม่\" หรือปิดแล้วเปิด task pane อีกครั้ง"); + setNoAccessRetryable(true); setPhase("no-access"); return; } @@ -206,6 +212,10 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) => } setUsername(authService.getUsername() ?? null); + // auto-open (2026-11-05) — sign-in ผ่าน = ไฟล์ e-Court: tag ให้ taskpane เด้งเองครั้งถัดไป + // ที่เปิดไฟล์นี้ · fire-and-forget (service กลืน error ทั้งหมด — ไฟล์ทั่วไปไม่มาทางนี้อยู่แล้ว) + void metadataService.enableAutoOpenTaskpane(); + const tracked = properties.savedByAddIn === metadataService.ADDIN_SIGNATURE_GUID; const id = tracked ? properties.documentTrackingId : undefined; if (!id) { @@ -469,6 +479,15 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>

{noAccessNotice ?? "ไม่พบ ticket ของ e-Court ในไฟล์ — ขณะนี้ add-in ใช้ได้กับเอกสารที่ส่งมาจาก e-Court เท่านั้น"}

+ {noAccessRetryable ? ( + + ) : null} ) : null} diff --git a/components/wordaddin/WordAddinGate.tsx b/components/wordaddin/WordAddinGate.tsx index 5df16c3..326a2d4 100644 --- a/components/wordaddin/WordAddinGate.tsx +++ b/components/wordaddin/WordAddinGate.tsx @@ -22,8 +22,41 @@ import TaskpaneApp from "./TaskpaneApp"; /** office.js self-host จาก postinstall scripts/fetch-office-js.mjs (intranet ไม่ใช้ CDN) */ const OFFICE_JS_SRC = withBasePath("/wordaddin/office-js/office.js"); -/** Office.onReady callback ไม่ยิงภายใน 5 วิหลังฉีด script ก็ proceed (กัน webview แปลก ๆ — โหลดสำเร็จแต่ onReady เงียบ) */ +/** + * ช่วง poll ของ fallback — ทุก 5 วิเช็คว่า global `Office` โผล่แล้วหรือยัง: + * โผล่แล้วแต่ onReady เงียบ (webview แปลก ๆ) = proceed ตามเจตนาเดิม · ยังไม่โผล่ + * (เครื่อง/เน็ตช้า — script ยังโหลดไม่เสร็จ) = รอต่อรอบหน้า ไม่ proceed บอด ๆ + */ const ONREADY_FALLBACK_MS = 5_000; +/** เพดานรวมหลัง inject script — ครบแล้วยังไม่มี Office = ไปหน้า error (มีปุ่มลองใหม่) */ +const ONREADY_HARD_CAP_MS = 60_000; + +/** + * office.js **ลบ `history.pushState`/`replaceState` ทิ้ง** ใน taskpane (set เป็น null — + * กัน add-in พา host ไปที่อื่น: office-js#429) แต่ App Router ของ Next ต้องใช้ History API + * ตอน mount ไม่งั้นพัง "window.history.replaceState is not a function" — เก็บตัวจริงไว้ + * ก่อนฉีด script แล้วคืนคืนหลัง office.js โหลด/พร้อม (workaround มาตรฐานของ SPA ใน + * add-in — taskpane เป็น iframe ของตัวเอง Next ใช้ history เฉพาะ SPA state ภายใน) + */ +const nativeHistory: { pushState?: History["pushState"]; replaceState?: History["replaceState"] } = + typeof window === "undefined" + ? {} + : { + pushState: typeof window.history.pushState === "function" ? window.history.pushState.bind(window.history) : undefined, + replaceState: + typeof window.history.replaceState === "function" ? window.history.replaceState.bind(window.history) : undefined, + }; + +/** คืน History API ที่ office.js ลบทิ้ง — idempotent (มีอยู่แล้ว = ไม่แตะ) */ +function restoreHistoryApi(): void { + if (typeof window === "undefined") return; + if (typeof window.history.pushState !== "function" && nativeHistory.pushState) { + window.history.pushState = nativeHistory.pushState; + } + if (typeof window.history.replaceState !== "function" && nativeHistory.replaceState) { + window.history.replaceState = nativeHistory.replaceState; + } +} /** * โหลด office.js ครั้งเดียวต่อ page เป็น singleton promise ระดับ module — @@ -39,16 +72,44 @@ function ensureOfficeJs(): Promise { if (!officeReadyPromise) { officeReadyPromise = new Promise((resolve) => { if (typeof Office !== "undefined") { - void Office.onReady(() => resolve(true)); + restoreHistoryApi(); // office.js evaluate ไปแล้ว — คืนก่อนให้ router ใช้ต่อได้ + void Office.onReady(() => { + restoreHistoryApi(); // host handshake อาจลบซ้ำตอน initialize + resolve(true); + }); return; } const script = document.createElement("script"); - const fallbackTimer = setTimeout(() => resolve(true), ONREADY_FALLBACK_MS); + const startedAt = Date.now(); + let fallbackTimer: ReturnType; + const armFallback = () => { + fallbackTimer = setTimeout(() => { + // Office โผล่แล้ว (script evaluate แล้ว) = proceed ได้เลย ไม่รอ onReady — + // กัน webview ที่โหลดสำเร็จแต่ callback เงียบ (เจตนาเดิมของ fallback) + if (typeof Office !== "undefined") { + restoreHistoryApi(); // office.js evaluate แล้วอาจลบ history ทิ้ง — คืนก่อน proceed + resolve(true); + return; + } + // ยังไม่มี Office global = เครื่องช้า script ยังไม่โหลดเสร็จ — รอต่อ (re-arm) + // จนครบเพดานค่อยยอมแพ้ไปหน้า error กัน proceed แล้ว Word.run พังกลางคัน + if (Date.now() - startedAt >= ONREADY_HARD_CAP_MS) { + officeReadyPromise = null; + script.remove(); + resolve(false); + return; + } + armFallback(); + }, ONREADY_FALLBACK_MS); + }; + armFallback(); // ตั้งก่อนเสียบ src เหมือนเดิม — ครอบทั้ง network fetch + evaluate + รอ onReady const onReady = () => { // เคลียร์ timer เฉพาะเมื่อ callback ของ Office.onReady ยิงจริง — ถ้าเคลียร์ตั้งแต่ onload // แล้ว onReady เงียบ fallback จะตายไปก่อนทำหน้าที่ (ค้าง loading ตลอดไป) + restoreHistoryApi(); // script evaluate แล้ว — office.js ลบ history ตรงนี้ คืนก่อน router ใช้ void Office.onReady(() => { clearTimeout(fallbackTimer); + restoreHistoryApi(); // handshake ของ host อาจลบซ้ำ — คืนอีกรอบก่อน mount TaskpaneApp resolve(true); }); }; diff --git a/docs/PLAN-ADDIN-AUTH.md b/docs/PLAN-ADDIN-AUTH.md index b2afb9f..e4097c6 100644 --- a/docs/PLAN-ADDIN-AUTH.md +++ b/docs/PLAN-ADDIN-AUTH.md @@ -1,6 +1,12 @@ # 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. บริบท/เป้าหมาย @@ -8,7 +14,9 @@ `token`/`owner` ลงไฟล์) เป็น **ticket** ตาม requirement: - e-Court stamp custom properties 2 ค่าในไฟล์ .docx: `ticket_data` (JSON) + `HmacSha256` -- addin ตรวจ HMAC เองฝั่ง client (gate เชิง UX — ตัดเคสไฟล์ถูกแก้ก่อนยิง network) +- 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 ยังไม่มีไฟล์จริงใช้ — @@ -20,10 +28,10 @@ |---|---|---| | 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 | ทำตาม requirement (sign เทียบเองก่อน) โดย secret มาจาก `NEXT_PUBLIC_ADDIN_TICKET_SECRET` — **ยอมรับว่าค่าใน bundle สกัดได้** จึงถือเป็นแค่ UX gate, ความมั่นคงจริงอยู่ที่ server verify | +| D3 | client-side HMAC | ~~ทำตาม requirement (sign เทียบเองก่อน) โดย secret มาจาก `NEXT_PUBLIC_ADDIN_TICKET_SECRET`~~ **แทนที่ 2026-11-04 ตาม PLAN-SSO.md D13**: client ไม่ตรวจ HMAC — เหลือ parse + exp เป็น UX gate การตรวจ "ไฟล์ถูกแก้" เกิดตอน server ตอบ 401 · ถอด `NEXT_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 หมดอายุ (TTL ~1 ชม.) แต่ ticket ยังไม่หมด | เก็บ `exp` ของ token (decode จาก JWT) — ก่อนใช้ถ้าหมดอายุและ ticket ยังไม่หมด → **renew อัตโนมัติ** · โดน 401 กลาง session → renew จาก ticket 1 ครั้งแล้ว retry ก่อนไป no-access | +| 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 ต่อ | @@ -47,8 +55,9 @@ HmacSha256 = = HMAC-SHA256(secret, ticket_data | `tid` | string | optional · ≤100 · template ต้นทาง (ไม่มี = `template: null`) | - field อื่นปนมา = reject (allowlist ตาม convention ของ repo) -- secret ปัจจุบัน = `"super_secret"` — **e-Court + `ADDIN_TICKET_SECRET` (server) + - `NEXT_PUBLIC_ADDIN_TICKET_SECRET` (client) ต้องตรงกันทั้งสาม** +- 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) @@ -59,13 +68,16 @@ 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) → map claims → `issueDocverToken` → response รูปเดียวกัน `{access_token, token_type, expires_in}` | -| ไม่มี `x-api-key` + body ไม่ใช่ object / มี field อื่นปน / field ไม่ใช่ string | 400 (รูปผิดก่อนถึงการ verify) | -| HMAC ผิด / parse ticket ไม่ผ่าน | 401 **ข้อความเดียวกันทุกสาเหตุ** (กัน oracle — addin รู้สาเหตุจริงจากการตรวจเองฝั่ง client อยู่แล้ว) | -| ticket **หมดอายุ** แต่ HMAC ถูก | แก้ 2026-10-28 (requirement ขั้น 7): ตรวจ DB ว่า `fid` นี้เคยมี version โดย `uid` เดียวกัน (`hasVersionByUser` ของ `lib/docver-store.ts`) → เคย = ออก token ให้ต่อ · ไม่เคย (หรือ DB พัง = fail closed) = 401 ข้อความเดียวกัน | -| ไม่มี `x-api-key` และไม่มี ticket fields เลย | 401 | +| ไม่มี `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_SECONDS` **default 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` — pattern `verifyDocverToken` ของ `lib/docver-token.ts` เป๊ะ - middleware ยกเว้น `api/documents/*` อยู่แล้ว — ไม่ต้องแต้ @@ -76,36 +88,37 @@ Branch ตาม header (ตัวตั้ง = `x-api-key`): 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}$ -export function timingSafeEqualHex(a: string, b: string): boolean // constant-time บน hex ที่ normalize แล้ว -export type HmacSha256Hex = (message: string, secret: string) => Promise // adapter ที่สองฝั่งฉีด +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 ตอนเปิดเอกสาร (map ขั้นตอน 1–7 ของ requirement) +## 5. ฝั่ง addin — flow ตอนเปิดเอกสาร (แก้ 2026-11-04 — pre-check + ยิง /token ทุกครั้ง) `TaskpaneApp.detect()` → `authService.signInWithTicket({ticketData, ticketHmac})`: ``` 1. อ่าน properties ticket_data + HmacSha256 (readTrackedProperties) -2. ขาดข้อใดข้อหนึ่ง / ค่ายาวเกิน 255 → no-access "ไม่พบ ticket ของ e-Court ในไฟล์" - parseDocverTicket ไม่ผ่าน → no-access "ไฟล์ถูกแก้ไข" - HMAC (Web Crypto + NEXT_PUBLIC_ADDIN_TICKET_SECRET) ไม่ตรง → no-access "ไฟล์ถูกแก้ไข" -3. session = localStorage["wordaddin-auth"] (fallback memory) -4. ไม่มี session หรือ uid ≠ session.userId (user ใหม่): - · ขอ token จาก /token เลย — **server เป็นคนตัดสิน exp** (แก้ 2026-10-28: ticket หมดอายุ - แต่ fid นี้เคยมี version โดย uid เดียวกัน = ออกให้ → คนที่กลับมาทำเอกสารเดิม - ผ่านได้แม้ session หาย/เคลียร์ · 401 + ticket หมดอายุ → no-access "ticket หมดอายุ") - · สำเร็จ → เคลียร์ session เก่า → เก็บ session ใหม่ {userId: uid, …} -5. uid ตรงกัน (คนเดิม): - · tokens[fid] มีและยังไม่หมดอายุ (กลับมาทำเอกสารเดิม) → ผ่านเลย ไม่ยิง /token - · tokens[fid] มีแต่ token หมดอายุ → ยิง renew เสมอ (ticket ยังไม่หมด = ออกให้เสมอ · - หมดแล้ว = ออกเฉพาะ "เคยทำงาน" ตามข้อ 4) → fail = เก็บ token เก่าไว้ - (ถ้าเน่าจริง apiClient จะ 401 → renew 1 ครั้ง → ยัง fail = no-access ตอนใช้งาน) - · tokens[fid] ไม่มี (เอกสารใหม่) → ขอ token เก็บเพิ่มเหมือนข้อ 4 -6. ตั้ง state ภายใน (fid/token/username/ticket ดิบไว้ renew) → ไหลต่อ phase +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) ไม่เปลี่ยน ``` @@ -123,22 +136,33 @@ export type HmacSha256Hex = (message: string, secret: string) => Promise - `lib/addin-ticket.ts` — server-only: `addinTicketSecret()` (env `ADDIN_TICKET_SECRET` trim หรือ default `"super_secret"`) + `verifyAddinTicket(ticketData, hmacHex, secret, nowSec?)` (HMAC `node:crypto` + timing-safe + parse + exp) + `ticketToDocverClaims(ticket)` — ห้าม import `@/db`/minio (unit test รันได้เปล่า ๆ) -- `wordaddin/services/ticketService.ts` — client: `verifyTicketHmacWeb` (crypto.subtle), session store +- `wordaddin/services/ticketService.ts` — client: ~~`verifyTicketHmacWeb` (crypto.subtle)~~ + (ถอด 2026-11-04), session store (`loadSession`/`saveSession`/`clearSession` — try/catch + memory fallback), `requestDocverToken` - (POST `withBasePath("/api/documents/token")` body ticket) + (POST `withBasePath("/api/documents/token")` body ticket — แก้ 2026-11-04: คืน discriminated + result แยก network/http status/malformed ไม่โยน Error) - `scripts/make-ticket-docx.mjs` — §9 - `tests/utils/docver-ticket.test.ts`, `tests/lib/addin-ticket.test.ts` (vitest — pattern `tests/lib/docver-token.test.ts`) **แต้** - `app/api/documents/token/route.ts` — branch ตาม §3 -- `wordaddin/config.ts` — `ticketSecret: process.env.NEXT_PUBLIC_ADDIN_TICKET_SECRET ?? "super_secret"` +- `wordaddin/config.ts` — ~~`ticketSecret: process.env.NEXT_PUBLIC_ADDIN_TICKET_SECRET ?? "super_secret"`~~ + (ถอด 2026-11-04 — เหลือ `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 +- `wordaddin/services/authService.ts` — rewrite ตาม §5 (แก้ 2026-11-04: pre-check + แยก status + 200/400/401/403 ผ่าน result ของ requestDocverToken) - `wordaddin/services/apiClient.ts` — renew-retry ตอน 401 -- `components/wordaddin/TaskpaneApp.tsx` — `detect()` เรียก `signInWithTicket` + ข้อความ no-access 3 แบบ -- `lib/openapi.ts` — POST /token มี 2 รูป body (oneOf: identity+x-api-key / ticket) +- `components/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 วิว่า global `Office` โผล่หรือยัง + (โผล่แล้ว = proceed ตามเจตนาเดิม · ยังไม่โผล่ = รอต่อ) จนถึง hard cap 60 วิ ค่อยไปหน้า error +- `lib/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` **ถอด** @@ -150,7 +174,7 @@ export type HmacSha256Hex = (message: string, secret: string) => Promise | 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` | `"super_secret"` | ฝั่ง addin (UX gate) — inline ลง bundle · ต้องตรงกับฝั่ง e-Court เช่นกัน | +| ~~`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 ตามตัวอักษรหลังเติม @@ -163,16 +187,24 @@ export type HmacSha256Hex = (message: string, secret: string) => Promise ## 8. Edge cases -- **property ยาวเกิน 255** → ไฟล์เสียหาย → no-access (Word ตัดค่าทิ้งจะ parse/HMAC ไม่ผ่านอยู่แล้ว — +- **property ยาวเกิน 255** → ไฟล์เสียหาย → no-access (Word ตัดค่าทิ้งจะ parse ไม่ผ่านอยู่แล้ว — เช็คความยาวเพื่อข้อความที่ชัดกว่า) -- **localStorage ถูก block** (webview บางตัว/private mode) → session อยู่ใน memory ต่อ session เดียว - (เปิดใหม่เริ่มใหม่ ขอ token ใหม่ — และหลังแก้ 2026-10-28 ขอใหม่ได้**แม้ ticket หมดอายุ** - ตราบใดที่เอกสารนี้เคยมี version โดย uid เดียวกัน: server ออกให้จากหลักฐานใน DB - ไม่ต้องพึ่ง token เก่าในเครื่องอีกต่อไป — ข้อจำกัดเหลือเฉพาะเอกสารที่เคยเปิดแต่ - **ยังไม่เคยสร้าง version** ซึ่งไม่มีร่องรอยฝั่ง server ต้องขอไฟล์ใหม่จาก e-Court) +- **เปิดเอกสาร = ต้อง 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 ครั้ง → retry → ยังไม่ผ่าน → no-access +- **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 เหมือนเดิม (ข้อความเดิมปรับคำ) @@ -191,17 +223,21 @@ Dev tool รันมือ `node scripts/make-ticket-docx.mjs [opti ## 10. Verify criteria -1. `npx vitest run` ผ่านทั้งชุด (ไฟล์ใหม่ + เดิม — โดยเฉพาะ `tests/lib/docver-token.test.ts` ไม่พัง) -2. `npx tsc --noEmit` ผ่าน +1. `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` ฉบับใหม่) +2. `npx tsc --noEmit` ผ่าน (error เดิมของ `piscina`/`docx-templates` ไม่เกี่ยวกับแผนนี้) 3. curl regression ที่ /token: (ก) `x-api-key` + identity body ออก token ได้เหมือนเดิม - (ข) ticket body ออก token ได้ (ค) HMAC ผิดกับ exp หมด = 401 ข้อความเดียวกัน (ง) body ปนกัน = 400 + (ข) ticket body ออก token ได้ `expires_in` = 604800 (ค) HMAC ผิด = 401 ข้อความเดียว + (ง) body ปนกัน/ไม่ส่ง ticket = 400 (จ) exp อดีตลายเซ็นถูก = **403** 4. ทดสอบใน Word จริง (sideload ตาม `docs/WORDADDIN-DEV.md`) ด้วยไฟล์จาก make-ticket-docx: - ไฟล์ถูกต้อง → ใช้ได้ ชื่อผู้ใช้ = uname · แก้ `ticket_data` 1 ตัวอักษร → no-access "ไฟล์ถูกแก้ไข" · - exp อดีตของเอกสารที่ไม่เคยสร้าง version → no-access "ticket หมดอายุ" · เปิดเอกสารเดิมซ้ำ → - ไม่ยิง /token ซ้ำ (ใช้ token เดิม) · **ticket หมดอายุ + เอกสารเคยสร้าง version ≥1 โดย uid เดียวกัน → - เปิดใหม่ใช้ต่อได้แม้เคลียร์ localStorage `wordaddin-auth` ก่อนเปิด (server ออก token ใหม่ — แก้ 2026-10-28)** · - ตั้ง `DOCVER_TOKEN_TTL_SECONDS` สั้น ๆ แล้วเปิดซ้ำหลังหมดอายุ → renew อัตโนมัติใช้ต่อได้ · - เปิดไฟล์ uid คนละคน → session เดิมถูกเคลียร์ + ไฟล์ถูกต้อง → ใช้ได้ ชื่อผู้ใช้ = uname · **dev log เห็น POST /token ทุกครั้งที่เปิด task pane** · + แก้ `ticket_data` 1 ตัวอักษร → no-access ข้อความลายเซ็น (server 401) · + **ticket หมดอายุ + มี session (JWT 7 วันยังไม่หมด) → ผ่านด้วย JWT เดิม (Network เห็น 403)** · + **ticket หมดอายุ + เคลียร์ localStorage `wordaddin-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 "อ่านข้อมูลเอกสารไม่สำเร็จ" + + กด "ลองใหม่" ได้ 5. รัน `plan-reviewer` + `code-reviewer` ตามกติกา `.claude/` ## 11. ลำดับงาน diff --git a/docs/PLAN-DOCVER.md b/docs/PLAN-DOCVER.md index 166d583..8dea3bc 100644 --- a/docs/PLAN-DOCVER.md +++ b/docs/PLAN-DOCVER.md @@ -62,7 +62,7 @@ JSON body (allowlist — key อื่น = 400 กัน typo): - env `DOCVER_TOKEN_SECRET` ไม่ตั้ง/ว่าง = **500** "DOCVER_TOKEN_SECRET ยังไม่ได้ตั้งค่า" (ความผิด config ฝั่ง server ไม่ใช่ความผิดของผู้เรียก — และห้าม log ค่า secret) - response 200: `{ "access_token": string, "token_type": "Bearer", "expires_in": number }` - — JWT HS256 stateless มี `iat`/`exp` (TTL env `DOCVER_TOKEN_TTL_SECONDS` default 3600) + — JWT HS256 stateless มี `iat`/`exp` (TTL env `DOCVER_TOKEN_TTL_SECONDS` default 604800 = 7 วัน — แก้ 2026-11-04 จาก 3600 ให้ token อายุยาวแทนระบบ refresh/renew ของ add-in) · verify บังคับ alg=HS256 (กัน alg-confusion) และเทียบ signature แบบ timing-safe - **endpoint อื่นของกลุ่มนี้ทั้งหมดต้องแนบ `Authorization: Bearer `** — ไม่มี header / รูปผิด / signature ผิด / หมดอายุ / secret ไม่ตั้ง = 401 ข้อความไทย @@ -261,7 +261,7 @@ read-only ของ N10 แต่ตัวนี้เขียนได้) | `DOCVER_LINK_TTL_SECONDS` | `900` | อายุ presigned URL | | `DOCVER_API_KEY` | — บังคับ | API key ของ `POST /api/documents/token` (header `x-api-key`) — ไม่ตั้ง/ว่าง = 401 fail closed · เทียบ timing-safe · ห้าม log ค่าดิบ · GET /latest ใช้ตัวนี้**หรือ** `EXTERNAL_API_KEY` ตัวใดตัวหนึ่ง | | `DOCVER_TOKEN_SECRET` | — บังคับ | secret เซ็น/ตรวจ JWT HS256 — ไม่ตั้ง: /token = 500 · endpoint อื่น = 401 · ห้าม log ค่าดิบ | -| `DOCVER_TOKEN_TTL_SECONDS` | `3600` | อายุ access_token (วินาที) | +| `DOCVER_TOKEN_TTL_SECONDS` | `604800` | อายุ access_token (วินาที) — default 7 วัน (แก้ 2026-11-04 จาก 3600) | | `TICKET_SEC` | `300` | TTL (วินาที) ของ `exp` ที่ stamp ตอน GET /latest type=docx — env เดียวกับกลุ่ม template (`app/api/template/*`) | | `PRIVATE_KEY_HEX` | — optional | private key Ed25519 (hex seed) — ตั้งแล้ว GET /latest type=docx จะ stamp prop `Ed25519` เพิ่ม · ไม่ตั้ง = ไม่ใส่ (chain ของ add-in verify แค่ `HmacSha256`) · env เดียวกับกลุ่ม template | | `ADDIN_TICKET_SECRET` | chain → `SECRET` → `"super_secret"` | secret ของ HMAC ที่ stamp ที่ /latest — ใช้ `addinTicketSecret()` ให้ตรงฝั่ง verify ของ /token ทาง ticket (ดู `docs/PLAN-ADDIN-AUTH.md`) | diff --git a/docs/PLAN-SSO.md b/docs/PLAN-SSO.md index 28b9731..821c480 100644 --- a/docs/PLAN-SSO.md +++ b/docs/PLAN-SSO.md @@ -25,7 +25,7 @@ middleware รวมเป็น**หน้าแรก `/note` ที่เป ยิงเส้น mint ใหม่ ส่วนทาง B ใช้ตัว stamp เดียวกับ .docx ของ add-in - ticket (B) เก็บไว้เพราะ mint ได้**แบบ offline** — ครอบคลุมเคสที่ e-Court สร้างลิงก์ล่วงหน้า/ ไม่ยิง API ตอน render · แลกมาด้วยเงื่อนไขบังคับ **D13** (ตัด secret ออกจาก bundle ของ add-in - ก่อน — ไม่งั้นใครแกะ bundle ก็ปลอม ticket เข้า login ได้) + ก่อน — ไม่งั้นใครแกะ bundle ก็ปลอม ticket เข้า login ได้ · **ทำแล้ว 2026-11-04**) **ผลพลอยได้:** ไม่มีเงื่อนไขค้างกับทีม e-Court อีกต่อไป — ทางไหนที่เขาทำได้ตามบริบทหน้านั้น ๆ ก็ใช้ได้ (แนะนำ A เมื่อ mint real-time ได้, B เมื่อสร้างลิงก์ล่วงหน้า) · ลิงก์ทั้งสองแบบ @@ -69,7 +69,7 @@ credential อายุสั้น ฝังอีเมล/แชท/เก็ | D10 | เครื่องมือทดสอบ | `scripts/make-login-link.mjs` mint token ออฟไลน์ (node:crypto HS256 จาก `DOCVER_TOKEN_SECRET`) แล้วพิมพ์ลิงก์เต็ม — pattern เดียวกับ `make-ticket-docx.mjs` | | D11 | fid | route ใช้ fid จาก credential เป็น `fileId` ของ N8 **เสมอ** (ค่าที่เซ็นแล้ว มาจาก token/ticket ไม่ใช่ param) — ลิงก์ไม่ต้องส่ง `fileId` และ param นี้ถ้าส่งมาก็ถูกแทนที่ กัน mismatch ระหว่างค่าเซ็นกับค่าปลอมแปลงได้ | | D12 | สองทางเข้าพร้อมกัน | route แยกตาม param: มี `t` = ทาง JWT · ไม่มี `t` แต่มี `ticket_data`+`HmacSha256` ครบ = ทาง ticket · **มีทั้งสองชุด = `sso=invalid`** (fail closed กันกำกวม) · ทั้งสองทางจบที่ cookie + redirect เดียวกัน · ทาง ticket ไม่ใช้ `tid` (template ต้นทาง — ไม่เกี่ยวกับ login) | -| D13 | เงื่อนไขบังคับของทาง ticket | **ตัด client HMAC check + `NEXT_PUBLIC_ADDIN_TICKET_SECRET` ออกจาก add-in ก่อนเปิดทาง ticket** — secret ตัวเดียวกันจะกลายเป็นจุดอ่อนของ login โดยตรง (แกะ bundle = ปลอม ticket เข้าได้) · add-in เหลือ parse + exp เป็น UX gate, การตรวจ "ไฟล์ถูกแก้" เกิดตอน server ตอบ 401 (แทนที่ D3 ของ PLAN-ADDIN-AUTH.md) · โบนัส: ทางเข้า ticket ที่ `/token` ของ add-in ก็แข็งขึ้นตาม | +| D13 | เงื่อนไขบังคับของทาง ticket | **ตัด client HMAC check + `NEXT_PUBLIC_ADDIN_TICKET_SECRET` ออกจาก add-in ก่อนเปิดทาง ticket** — secret ตัวเดียวกันจะกลายเป็นจุดอ่อนของ login โดยตรง (แกะ bundle = ปลอม ticket เข้าได้) · add-in เหลือ parse + exp เป็น UX gate, การตรวจ "ไฟล์ถูกแก้" เกิดตอน server ตอบ 401 (แทนที่ D3 ของ PLAN-ADDIN-AUTH.md) · โบนัส: ทางเข้า ticket ที่ `/token` ของ add-in ก็แข็งขึ้นตาม · **✅ ทำแล้ว 2026-11-04** (พร้อมปรับ contract ทาง ticket ของ `/token` เป็น 400/401/403 + TTL 7 วัน — ดู PLAN-ADDIN-AUTH.md หัวไฟล์) | | D14 | หน้าแรก `/note` ใหม่ (2026-10-29) | เปลี่ยนจากรายการไฟล์แบบการ์ด เป็น**หน้าเลือกเอกสารแบบ dropdown 2 ระดับ** คล้าย `app/tmpl/page.tsx` (กลุ่ม → ไฟล์) + ปุ่ม "เปิดเอกสาร" เข้า annotate — ถอดรายการการ์ด N9 + ฟอร์มอัปโหลด N4 + การ์ดจำลองลิงก์ N8 ออกจากหน้า · N8 `?fileUrl=` ยังทำงานที่หน้านี้ **เฉพาะเมื่อมี session ที่ verify แล้ว** (ไม่มี = redirect `/note?sso=required` โดยไม่แตะ DB — ดู §1.1) · รายละเอียด §1.1 | | D15 | บังคับ session (2026-10-29) | `/note` **public** — middleware ยกเว้น `note` เพิ่ม (คนไม่มี cookie เห็นหน้าเลือกเอกสารได้) ส่วนหน้าอื่น (annotate/summary/groups) ยังบังคับ cookie — ไม่มี = redirect `/note?sso=required` (ไม่มี `next` แล้ว: ไม่มี login ให้วนกลับ ผู้ใช้ต้องกลับ e-Court คลิกลิงก์ใหม่) · action `logout` คงไว้ ปลายทางเปลี่ยนเป็น `/note` | | D16 | เซ็น session cookie (2026-10-29 รอบสอง — code review ข้อ 3) | `oacCase` เดิม**ไม่มีลายเซ็น** (`parseOacCase` อ่าน query string เปล่า — ใครก็ปลอม `u_userid` เป็นคนอื่นได้ token/ticket ทั้งแผนจะไม่เพิ่มความปลอดภัยจริง) → เก็บรูป `oacCase` เดิมไว้ + **cookie ที่สอง `oacCaseSig`** = `.` เซ็นครอบ **payload เต็มของ `oacCase` + `.` + exp** (แก้ exp เฉย ๆ ไม่ได้) ด้วย secret ใหม่ `NOTE_SESSION_SECRET` — **แยกจาก `DOCVER_TOKEN_SECRET`** (ไม่แตะของกลุ่ม documents: session ของแอป note ไม่ผูก secret กลุ่ม docver หมุนพร้อมกันไม่ได้ โดยเฉพาะตอน pre-production ที่ต้อง rotate DOCVER อยู่ด้วย) · ไม่ตั้ง/ว่าง = **fail closed** (verify ไม่ผ่านทุกกรณี = ไม่มี session, SSO route ตอบ `sso=error` — ไม่มี default ที่รู้กันหมด) · ตรวจ**ทุกจุดที่อ่าน identity**: middleware (async + Web Crypto — edge runtime ไม่มี node:crypto) + `getSessionUser()` (เป็น async + verify หน้า caller `await`) · cookie ทั้งสอง httpOnly/sameSite lax/path `/`/maxAge 8h + **`secure` เมื่อ production** · exp ฝังในของที่เซ็น = อายุจริงวัดฝั่ง server (maxAge เป็นแค่ cleanup ฝั่ง browser) | @@ -233,7 +233,7 @@ readNoteSession() / saveNoteSession() / clearNoteSession() / noteAuthFetch(path, - `app/note/annotate/[id]/page.tsx` — `await getSessionUser()` ตาม signature ใหม่ - `app/actions/auth.ts` — action `logout` เคลียร์**ทั้งสอง cookie** (`oacCase` + `oacCaseSig`) + ปลายทาง redirect เปลี่ยนจากหน้า login เป็น `/note` - `utils/user.ts` — เพิ่ม `OAC_SIG_COOKIE` + `SESSION_MAX_AGE` (§4) -- **add-in (D13):** `wordaddin/services/authService.ts` ถอดการเรียก `verifyTicketHmacWeb` (เหลือ parse + exp เป็น UX gate) · `wordaddin/services/ticketService.ts` ถอด `verifyTicketHmacWeb`/`hmacSha256HexWeb` · `wordaddin/config.ts` ถอด `ticketSecret` · `components/wordaddin/TaskpaneApp.tsx` ข้อความ no-access ปรับ (ไม่มีสาเหตุ HMAC ฝั่ง client — รวมเป็น "ticket ไม่ถูกต้อง" ตอน server 401) · `docs/PLAN-ADDIN-AUTH.md` (D3/D8 ถูกแทนที่) + `docs/WORDADDIN-DEV.md` +- **add-in (D13):** `wordaddin/services/authService.ts` ถอดการเรียก `verifyTicketHmacWeb` (เหลือ parse + exp เป็น UX gate) · `wordaddin/services/ticketService.ts` ถอด `verifyTicketHmacWeb`/`hmacSha256HexWeb` · `wordaddin/config.ts` ถอด `ticketSecret` · `components/wordaddin/TaskpaneApp.tsx` ข้อความ no-access ปรับ (ไม่มีสาเหตุ HMAC ฝั่ง client — รวมเป็น "ticket ไม่ถูกต้อง" ตอน server 401) · `docs/PLAN-ADDIN-AUTH.md` (D3/D8 ถูกแทนที่) + `docs/WORDADDIN-DEV.md` — **✅ ทำแล้ว 2026-11-04** - `lib/openapi.ts` — เพิ่ม path ใหม่ `POST /api/note/auth/token` (field ตาม §3.1) · รายการ `/api/documents/token` **คงเดิมไม่แต้** - `.env.example` — เช็คว่ามี `DOCVER_TOKEN_SECRET` + `NOTE_SESSION_SECRET` (ค่า dev อะไรก็ได้) — dev ไม่มีสองค่านี้ = เข้า annotate ไม่ได้ (ทางเข้า session เดียวคือลิงก์ SSO ส่วนหน้าแรก public) - `CLAUDE.md` — สถานะ auth ของแอป note (mock login ถอด, SSO 2 ทางเข้า, **ไม่มีหน้า login · `/note` public + หน้าเลือกเอกสารใหม่ · N4 ถอด**) + add-in ถอด client HMAC (D13) + invariant #4 ไม่เกี่ยว (route นี้ไม่แตะ DB ไม่ publish SSE) @@ -247,7 +247,7 @@ readNoteSession() / saveNoteSession() / clearNoteSession() / noteAuthFetch(path, - `MOCK_USERS` ใน `utils/user.ts` + comment "ใช้ร่วมกันสองที่" ที่ตายตาม (`OAC_CASE_COOKIE`/`parseOacCase`/`buildOacCaseCookie`/`SESSION_MAX_AGE` คงไว้ทั้งหมด — auth route ใช้อยู่) - `components/UploadFileForm.tsx` + action `uploadFile` ใน `app/actions/files.ts` (N4 — ไม่มีทางเข้า UI อีกตาม D14) - `components/EcourtLinkForm.tsx` (ทางทดสอบ N8 เหลือสคริปต์ D10) -- `NEXT_PUBLIC_ADDIN_TICKET_SECRET` ใน `.env.example` และทุกจุดอ้าง (D13 — secret ออกจาก bundle) +- ~~`NEXT_PUBLIC_ADDIN_TICKET_SECRET` ใน `.env.example` และทุกจุดอ้าง (D13 — secret ออกจาก bundle)~~ **✅ ถอดแล้ว 2026-11-04** ## 7. Env @@ -298,7 +298,7 @@ readNoteSession() / saveNoteSession() / clearNoteSession() / noteAuthFetch(path, ## 10. ลำดับงาน -0. **add-in D13** — ถอด client HMAC check + `NEXT_PUBLIC_ADDIN_TICKET_SECRET` ออกจาก bundle (+ ปรับ tests/docs) → verify 9.3 (ฐ) +0. **add-in D13** — ~~ถอด client HMAC check + `NEXT_PUBLIC_ADDIN_TICKET_SECRET` ออกจาก bundle (+ ปรับ tests/docs)~~ **✅ ทำแล้ว 2026-11-04** (พร้อม refactor ใหญ่ของ PLAN-ADDIN-AUTH.md: contract 400/401/403 + TTL 7 วัน + fallback onReady) → verify 9.3 (ฐ) 1. `lib/docver-token.ts` (purpose + ttl) + tests → verify 9.1 บางส่วน 2. `lib/note-auth.ts` + tests (รวม `signOacSession`/`verifyOacSession` ของ D16) 3. `app/api/note/auth/token/route.ts` (mint) → verify 9.3 (ก)(จ) แล้ว `app/api/note/auth/e-court/route.ts` → verify 9.3 (ข–ง) diff --git a/docs/WORDADDIN-DEV.md b/docs/WORDADDIN-DEV.md index 252aa0f..399c81e 100644 --- a/docs/WORDADDIN-DEV.md +++ b/docs/WORDADDIN-DEV.md @@ -24,7 +24,8 @@ Taskpane ของ Word add-in "e-Court Documents Versioning" พอร์ตม |---|---|---| | `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 ต้องตั้งค่าจริงทั้งสองฝั่ง** | + +(~~`NEXT_PUBLIC_ADDIN_TICKET_SECRET`~~ — ถอดออก 2026-11-04 ตาม PLAN-SSO.md D13: add-in ไม่ตรวจ HMAC เองแล้ว ไม่มี secret ใน bundle ฝั่ง client) ## รัน dev ด้วย HTTPS (จำเป็นสำหรับ sideload ใน Word) @@ -87,7 +88,7 @@ Word for Mac ไม่มี Trust Center แบบ Windows — สแกน add ## ทดสอบ 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` แยกข้อความตามสาเหตุ +ไฟล์ที่ 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` (แก้ 2026-11-04: pre-check ฟอร์แม็ต/exp ฝั่ง client → ยิง `/docedit/api/documents/token` **ทุกครั้งที่เปิดเอกสาร** ให้ server ตรวจลายเซ็นแล้วแยกตาม status 200/400/401/403 → เก็บ token ตาม fid · ต้อง online ตอนเปิดไฟล์เสมอ) · ไฟล์ที่ไม่มีคู่นี้ / ฟอร์แม็ตพัง / ลายเซ็นไม่ผ่าน (server 401) / ticket หมดอายุ = phase `no-access` แยกข้อความตามสาเหตุ ทดสอบเองใน Word โดยไม่ต้องรอ e-Court — สร้างไฟล์ทดสอบด้วยสคริปต์ (คำนวณ HMAC ตรงกับตัว stamp จริงของกลุ่ม template): @@ -100,13 +101,13 @@ node scripts/make-ticket-docx.mjs path/to/file.ticket.docx --list 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 นาทีแล้วเปิด**เอกสารที่ยังไม่เคยสร้าง version** → no-access "ticket หมดอายุ" · เปิดเอกสารเดิมซ้ำหลัง ticket หมดอายุ → ยังใช้ได้ตาม requirement ขั้น 7: add-in ยิงขอ token ใหม่แล้ว server ตรวจว่า fid นี้เคยมี version โดย uid เดียวกัน → ออกให้ต่อ (แก้ 2026-10-28 — ใช้ได้แม้ token เก่าหมดอายุหรือ localStorage `wordaddin-auth` โดนเคลียร์ เพราะไม่พึ่ง cache ในเครื่องอีกต่อไป · ข้อจำกัด: เอกสารที่เคยเปิดแต่ยังไม่เคยสร้าง version ไม่มีหลักฐานฝั่ง server ต้องขอไฟล์ใหม่จาก e-Court) +แล้วเปิดไฟล์ใน Word ที่ sideload แล้ว → ต้องเข้าเฟส untracked (หรือ tracking เมื่อ `--tracked`) ทันที ชื่อผู้ใช้ = `uname` · ทดสอบ fail-case (แก้ 2026-11-04 ตาม contract ใหม่): แก้ `ticket_data` ผ่าน **File → Info → Properties → Advanced Properties → Custom** สัก 1 ตัวอักษร → no-access ข้อความลายเซ็นไม่ผ่าน (server 401) · สร้างด้วย `--exp +60` รอหมดอายุแล้วเปิดใหม่ — **มี session (JWT อายุ 7 วันยังไม่หมด)** → ยังใช้ได้ (Network เห็น /token ตอบ 403 แต่ UI ไหลต่อด้วย JWT เดิม) · **เคลียร์ localStorage `wordaddin-auth` ก่อนเปิด** → no-access "ticket หมดอายุ" (ต้องขอไฟล์ใหม่จาก e-Court — เลิกตรวจ "เคยมี version" จาก DB ตาม requirement ขั้น 7 เดิมแล้ว) · หยุด dev server แล้วเปิด task pane (ticket ยังไม่หมด) → no-access "ขอ token ไม่สำเร็จ" (ยืนยันว่าต้อง online ตอนเปิดเอกสาร) หมายเหตุ: การขอ 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` ของสคริปต์ ## การรันกับ backend จริง (ปลด mock แล้ว 2026-10-27) -- **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) +- **Auth:** ไม่มี login — **ยกเลิก Keycloak ถาวร 2026-10-27** · auth ของ add-in คือ ticket ของ e-Court ผ่าน `signInWithTicket` (ticket_data + HmacSha256 ในไฟล์ — แผนเต็ม `docs/PLAN-ADDIN-AUTH.md` แก้ 2026-11-04): add-in **ไม่ตรวจ HMAC เอง** (secret เป็นของ server อย่างเดียว — PLAN-SSO.md D13) — pre-check ฟอร์แม็ต/exp ฝั่ง client แล้วยิงขอ access_token ที่ `/api/documents/token` **ทุกครั้งที่เปิดเอกสาร**: 200 = ได้ token ใหม่ (อายุ default 7 วัน) · 401 = ลายเซ็นไม่ถูก · 403 = ลายเซ็นถูกแต่ ticket หมดอายุ (มี JWT เดิมใน session ที่ยังไม่หมด = ใช้ต่อ) · โดน 401 กลาง flow = ลอง renew จาก ticket 1 ครั้ง ยังไม่ผ่าน = `AuthRequiredError` → หน้า `no-access` (ticket ต่ออายุไม่ได้ — ต้องได้ไฟล์ใหม่จาก e-Court) - **Upload/download ของจริงเป็น default แล้ว:** `wordaddin/services/versioningService.ts` ยิงกลุ่ม `/api/documents/*` ผ่าน `apiClient` (Bearer จาก ticket) — **ไม่ต้องตั้ง env อะไรเพิ่ม** เพราะ `apiClient.toUrl` ใช้ same-origin `withBasePath("/api/documents/…")` เมื่อ `NEXT_PUBLIC_ADDIN_API_BASE_URL` ว่าง (API ของกลุ่ม docver อยู่แอปนี้เอง) · ตั้งค่า env นั้นเฉพาะวันที่ deploy add-in กับ API คนละ origin (ต้องรวม basePath เช่น `https://host/docedit` และเป็น HTTPS) - **เอกสารถูกระบุด้วย fid ใน token** — พารามิเตอร์ `documentId` (GUID ที่ stamp ในไฟล์) ของ uploadVersion/getVersions/getDownloadLink เหลือไว้ตาม signature เดิม ไม่ถูกส่งถึง server · templateId ก็เช่นกัน (มาจาก claims ของ token — ส่งใน form = 400) - **สิ่งที่ต้องรันฝั่ง server ก่อนใช้จริง:** MinIO (`docker compose up -d minio` — bucket `docver` app สร้างเอง) + env `DOCVER_API_KEY`/`DOCVER_TOKEN_SECRET`/`MINIO_*` ตาม `.env.example` ของกลุ่ม docver (docs/PLAN-DOCVER.md §6) · ทดสอบวงเต็ม: สร้างไฟล์ด้วย `make-ticket-docx.mjs` → เปิดใน Word → Save & Version → version ขึ้น MinIO จริง → กดดาวน์โหลด/PDF เปิด presigned URL ในเบราว์เซอร์ (โฮสต์ MinIO ต้องเข้าถึงได้จากเครื่องผู้ใช้) diff --git a/docs/addin-ui-flow.md b/docs/addin-ui-flow.md index 520bdfb..2162442 100644 --- a/docs/addin-ui-flow.md +++ b/docs/addin-ui-flow.md @@ -12,10 +12,10 @@ Task pane ทั้งหมดเป็น state machine เดียว **ไ ตั้งแต่ 2026-10-19 taskpane มี **2 แท็บ** (toggle กลุ่มปุ่ม `aria-pressed` ใต้ header — แสดงเฉพาะ phase `untracked`/`tracking`): **Versioning** คือทุกอย่างในเอกสารนี้ตามเดิม ส่วน **เครื่องมือ Template** (ดูหัวข้อ 10) คือเครื่องมือจัดการ Content Controls ฝั่ง Office.js ล้วน ไม่ยิง backend จึงใช้ได้ทั้งสอง phase ปุ่ม toggle **ทั้งสองปิดระหว่าง flow ที่เขียนเอกสาร** — `saving || starting` (versioning), `saveBusy` (บันทึก local/ปิดเอกสาร), `templateBusy` (operation ของแท็บ template รายงานผ่าน `onBusyChange` — กันสลับกลาง operation แล้ว flow versioning มา stamp/capture ซ้อน หรือกลับกัน; code review 2026-10-20) · สถานะแท็บรีเซ็ตกลับ `versioning` ตอนเข้า `no-access` กัน surprise · banner auto-save error (FR-17) ยกไว้เหนือ toggle ให้เห็นทั้งสองแท็บ -**ไม่มี login ตั้งแต่ 2026-10-27** (ยกเลิก Keycloak ตามคำขอผู้ใช้) — auth ทางเข้าเดียวคือ **ticket ของ e-Court**: `detect()` อ่าน custom properties `ticket_data` + `HmacSha256` ที่ e-Court stamp มากับไฟล์ แล้วเรียก `authService.signInWithTicket()` — ตรวจ HMAC ฝั่ง client → ตรวจ exp → ดู session ใน localStorage ตาม uid/fid → ขอ access_token ที่ `/api/documents/token` เก็บตาม fid (flow เต็ม: `docs/requirement/addin-workflow.md` + แผน `docs/PLAN-ADDIN-AUTH.md`) +**ไม่มี login ตั้งแต่ 2026-10-27** (ยกเลิก Keycloak ตามคำขอผู้ใช้) — auth ทางเข้าเดียวคือ **ticket ของ e-Court**: `detect()` อ่าน custom properties `ticket_data` + `HmacSha256` ที่ e-Court stamp มากับไฟล์ แล้วเรียก `authService.signInWithTicket()` — pre-check ฟอร์แม็ต/exp ฝั่ง client (UX gate) → ยิง `/api/documents/token` **ทุกครั้งที่เปิดเอกสาร** ให้ server ตรวจลายเซ็น (ไม่มี secret ฝั่ง client ตาม PLAN-SSO.md D13 — แก้ 2026-11-04) → แยกทางตาม status 200/400/401/403 → เก็บ access_token ใน localStorage ตาม fid (flow เต็ม: `docs/requirement/addin-workflow.md` + แผน `docs/PLAN-ADDIN-AUTH.md`) ``` -checking ──(ไม่มี ticket / HMAC ไม่ตรง / ticket หมดอายุ / ขอ token ไม่สำเร็จ)──→ no-access +checking ──(ไม่มี ticket / ฟอร์แม็ตพัง / ลายเซ็นไม่ผ่าน(401) / ticket หมดอายุ / ขอ token ไม่สำเร็จ)──→ no-access │ ├─(ticket ผ่าน + ไม่ใช่ tracked doc)──→ untracked └─(ticket ผ่าน + tracked doc)────────→ tracking @@ -25,7 +25,7 @@ checking ──(ไม่มี ticket / HMAC ไม่ตรง / ticket หม - `untracked` → (Start Versioning สำเร็จ) → `tracking` - ทุก phase → (ไม่มี ticket ตอน detect ใหม่ / 401 หลังพยายาม renew จาก ticket แล้ว = ticket หมดอายุไม่มีทางต่ออายุ) → `no-access` -- ปุ่ม "ลองใหม่" มีจริงแค่ 2 จุด: `bootError` ใน `checking` และ history error ใน `VersionHistoryPanel` — `untracked`/`tracking` **ไม่มี**ปุ่มลองใหม่แยกต่างหาก (เริ่ม flow ใหม่ด้วยการกดปุ่ม flow ซ้ำ) +- ปุ่ม "ลองใหม่" มีจริงแค่ 3 จุด: `bootError` ใน `checking`, history error ใน `VersionHistoryPanel` และ (เพิ่ม 2026-11-04) no-access เคส "อ่านข้อมูลเอกสารไม่สำเร็จ" — `untracked`/`tracking` **ไม่มี**ปุ่มลองใหม่แยกต่างหาก (เริ่ม flow ใหม่ด้วยการกดปุ่ม flow ซ้ำ) State ที่ UI ถือนอกเหนือจาก phase: @@ -36,7 +36,7 @@ State ที่ UI ถือนอกเหนือจาก phase: | `documentId` | tracking id ของเอกสาร (มีเฉพาะ `tracking`) | | `documentTemplateId` | template ต้นทางของ**ไฟล์ร่าง** (มีเฉพาะ `untracked` ที่เป็นไฟล์ร่าง — FR-16) | | `versions` + `historyLoading` + `historyError` | version history ของ `documentId` | -| `noAccessNotice` | ข้อความบนหน้า `no-access` — ต่างกันตามสาเหตุ (ไม่มี ticket ในไฟล์ / HMAC ไม่ตรง = ไฟล์ถูกแก้ / ticket หมดอายุ / ขอ token ไม่สำเร็จ) | +| `noAccessNotice` | ข้อความบนหน้า `no-access` — ต่างกันตามสาเหตุ (ไม่มี ticket ในไฟล์ / ฟอร์แม็ตพัง / ลายเซ็นไม่ผ่าน = ไฟล์ถูกแก้ / ticket หมดอายุ / ขอ token ไม่สำเร็จ) | | `starting`, `saving`, `progressMessage` | busy + ข้อความ progress ของ flow Start Versioning / Save & Version | | `confirmOpen`, `startConfirmOpen` | เปิด dialog ยืนยันของ flow ไหน | | `flowError`, `flowWarning`, `flowSuccess` | MessageBar 3 แบบของ flow หลัก (แสดงเฉพาะ `tracking`) | @@ -64,12 +64,17 @@ State ที่ UI ถือนอกเหนือจาก phase: ### `no-access` - ข้อความ: "เอกสารนี้ยังใช้งาน add-in ไม่ได้" -- คำอธิบายจางๆ (`noAccessNotice` ตามสาเหตุ): +- คำอธิบายจางๆ (`noAccessNotice` ตามสาเหตุ — ข้อความฉบับ 2026-11-04): - ไม่มี ticket ในไฟล์: "ไม่พบ ticket ของ e-Court ในไฟล์ — add-in ใช้ได้กับเอกสารที่ส่งมาจาก e-Court เท่านั้น" - - ticket รูปผิด / HMAC ไม่ตรง: "ตรวจสอบ ticket ไม่ผ่าน — ไฟล์ถูกแก้ไขหรือเสียหาย" - - ticket หมดอายุ (ตอนเปิด หรือโดน 401 กลาง flow หลังพยายาม renew): "ticket ของ e-Court หมดอายุ — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง" + - ฟอร์แม็ตพัง (parse ฝั่ง client หรือ server ตอบ 400): "รูปแบบ ticket ในไฟล์ไม่ถูกต้อง — ไฟล์ถูกแก้ไขหรือเสียหาย โปรดขอไฟล์ใหม่จาก e-Court" + - ลายเซ็นไม่ผ่าน (server 401): "ตรวจสอบลายเซ็น (HMAC) ของ ticket กับระบบไม่ผ่าน — ไฟล์ถูกแก้ไขหรือเสียหาย โปรดขอไฟล์ใหม่จาก e-Court" + - ticket หมดอายุ (pre-check / server 403 และไม่มี JWT ที่ยังใช้ได้ใน session / โดน 401 กลาง flow หลังพยายาม renew): "ticket ของ e-Court หมดอายุ — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง" - ขอ token ไม่สำเร็จ (server ลง/network): "ขอ token จากระบบไม่สำเร็จ — ตรวจการเชื่อมต่อแล้วเปิดเอกสารอีกครั้ง" -- **ไม่มีปุ่มใด ๆ** — ไม่มี login ให้กด (ยกเลิก Keycloak 2026-10-27; ขั้นตอนสำหรับเอกสารทั่วไปจะมีในอนาคต) ผู้ใช้ได้ไฟล์ใหม่จาก e-Court แล้วเปิดใหม่เท่านั้น + - อ่านไฟล์ไม่ได้ (Word.run พัง เช่นเปิดใน browser ธรรมดา): "อ่านข้อมูลเอกสารไม่สำเร็จ — ลองกด \"ลองใหม่\" หรือปิดแล้วเปิด task pane อีกครั้ง" +- **ปุ่ม "ลองใหม่" (เพิ่ม 2026-11-04) แสดงเฉพาะเคสอ่านไฟล์ไม่ได้** (`noAccessRetryable`) — กดแล้วรัน + `detect()` ใหม่ (ช่วยเคส office.js มาช้า/Word สะดุดชั่วคราว) · เคสอื่นไม่มีปุ่ม — ไม่มี login ให้กด + (ยกเลิก Keycloak 2026-10-27; ขั้นตอนสำหรับเอกสารทั่วไปจะมีในอนาคต) ผู้ใช้ได้ไฟล์ใหม่จาก e-Court + แล้วเปิดใหม่ หรือรอเน็ตกลับมาเท่านั้น ### `untracked` @@ -167,7 +172,7 @@ Dialog เดียวใช้ 2 flow (ปรับข้อความด้ ### Detect (FR-1) — ตอนเปิด task pane / กดลองใหม่ 1. อ่าน custom properties ครั้งเดียวทั้งชุด: `ticket_data`, `HmacSha256`, `SavedByAddIn`, `DocumentTrackingId`, `LastVersionId`, `LastVersionTimestamp`, `TemplateId` -2. ticket auth (`signInWithTicket` — PLAN-ADDIN-AUTH.md §5): อ่านไฟล์ไม่ได้ / ไม่มี ticket / HMAC ไม่ตรง / ticket หมดอายุ (ตามเงื่อนไข uid/fid ของ session) / ขอ token ไม่สำเร็จ → `no-access` แยกข้อความตามสาเหตุ · ผ่าน = มี access_token ของ fid นี้เก็บใน localStorage `wordaddin-auth` แล้วไปต่อ +2. ticket auth (`signInWithTicket` — PLAN-ADDIN-AUTH.md §5 ฉบับ 2026-11-04): อ่านไฟล์ไม่ได้ / ไม่มี ticket / ฟอร์แม็ตพัง (client หรือ server 400) / ลายเซ็นไม่ผ่าน (server 401) / ticket หมดอายุ (pre-check หรือ server 403 โดยไม่มี JWT ที่ยังใช้ได้ใน session) / ขอ token ไม่สำเร็จ → `no-access` แยกข้อความตามสาเหตุ · ผ่าน = มี access_token ของ fid นี้เก็บใน localStorage `wordaddin-auth` แล้วไปต่อ 3. `tracked = (SavedByAddIn === GUID ของ add-in)`: - tracked **และมี** `DocumentTrackingId` → `tracking` + โหลด history - ไม่ track หรือ track แต่ id หาย → `untracked` (เก็บ `TemplateId` ไว้ใช้ตอน Start Versioning — ไฟล์ร่างอยู่กลุ่มนี้) @@ -255,7 +260,7 @@ response: `{ url: string, expiresAt: string }` — UI เปิด `url` ใน ### Auth — e-Court ticket (ไม่มี login) -ยกเลิก Keycloak OIDC/PKCE แล้ว (2026-10-27) — ไม่มี Office Dialog ไม่มีหน้า sign-in: e-Court stamp custom properties **`ticket_data`** (JSON `exp`/`fid`/`tid`/`uid`/`uname`) + **`HmacSha256`** (HMAC-SHA256 ของ string ดิบ ด้วย secret ที่ config ได้ — ปัจจุบัน `super_secret`) ลงไฟล์ตอนสร้าง แล้ว `signInWithTicket()` ใช้ได้ทันที · flow ตาม `docs/requirement/addin-workflow.md` (แผนเต็ม `docs/PLAN-ADDIN-AUTH.md`): ตรวจ HMAC ฝั่ง client (UX gate) → ตรวจ exp → session ใน localStorage `wordaddin-auth` ตาม uid (user ใหม่ = เคลียร์ session เก่าทั้งก้อน) → **ขอ access_token ที่ `POST /api/documents/token`** แบบ body `{ticket_data, HmacSha256}` (server ตรวจ HMAC+exp อีกชั้นด้วย secret ฝั่ง server) → เก็บ token ตาม fid ("คีย์คือ fid") — กลับมาเปิดเอกสารเดิม = ใช้ token เดิมไม่ต้องขอใหม่ · token หมดอายุกลางใช้งาน (โดน 401) = ลองขอใหม่จาก ticket 1 ครั้ง (ticket ต้องยังไม่หมดอายุ) ยังไม่ผ่าน = `AuthRequiredError` → หน้า `no-access` ให้ขอไฟล์ใหม่จาก e-Court · token นี้คือสิ่งที่ `apiClient` แนบเป็น `Authorization: Bearer` ทุก request ตอนต่อ backend จริง (กลุ่ม docver บังคับ Bearer — docs/PLAN-DOCVER.md) +ยกเลิก Keycloak OIDC/PKCE แล้ว (2026-10-27) — ไม่มี Office Dialog ไม่มีหน้า sign-in: e-Court stamp custom properties **`ticket_data`** (JSON `exp`/`fid`/`tid`/`uid`/`uname`) + **`HmacSha256`** (HMAC-SHA256 ของ string ดิบ ด้วย secret ที่ config ได้ — ปัจจุบัน `super_secret`) ลงไฟล์ตอนสร้าง แล้ว `signInWithTicket()` ใช้ได้ทันที · flow ตาม `docs/requirement/addin-workflow.md` (แผนเต็ม `docs/PLAN-ADDIN-AUTH.md` — แก้ 2026-11-04): **client ไม่ตรวจ HMAC เอง** (ถอด secret ออกจาก bundle ตาม PLAN-SSO.md D13) — pre-check ฟอร์แม็ต + exp (หมดอายุและไม่มี JWT ของ fid ใน session = จบเลย) → **ยิง `POST /api/documents/token` ทุกครั้งที่เปิดเอกสาร** แบบ body `{ticket_data, HmacSha256}` ให้ server ตรวจลายเซ็นแล้วแยกตาม contract: 200 = ได้ token ใหม่ (user ใหม่ = เคลียร์ session เก่าทั้งก้อน) · 400 = ฟอร์แม็ตพัง · 401 = ลายเซ็นไม่ถูก (ไฟล์ถูกแก้) · 403 = ลายเซ็นถูกแต่ ticket หมดอายุ (มี JWT ของ fid ใน session ที่ยังไม่หมด = ใช้ต่อ) → เก็บ token ตาม fid ("คีย์คือ fid") อายุ 7 วัน (default ใหม่ของ `DOCVER_TOKEN_TTL_SECONDS`) · token หมดอายุกลางใช้งาน (โดน 401) = ลองขอใหม่จาก ticket 1 ครั้ง (ticket หมดแล้ว server ตอบ 403) ยังไม่ผ่าน = `AuthRequiredError` → หน้า `no-access` ให้ขอไฟล์ใหม่จาก e-Court · token นี้คือสิ่งที่ `apiClient` แนบเป็น `Authorization: Bearer` ทุก request ตอนต่อ backend จริง (กลุ่ม docver บังคับ Bearer — docs/PLAN-DOCVER.md) > **หมายเหตุสถานะปัจจุบัน:** UI นี้ถูก implement แล้วใน repo นี้ที่ route `/docedit/wordaddin` (ดู `components/wordaddin/` + `wordaddin/`) — auth เป็น ticket auth จริงครบ flow (`wordaddin/services/authService.ts` + `ticketService.ts` + ทางเข้า ticket ของ `/api/documents/token`) และ **API ทั้งสามข้างบนเป็นของจริงแล้ว** (`wordaddin/services/versioningService.ts` ยิงกลุ่ม `/api/documents/*` ผ่าน Bearer — ปลด mock 2026-10-27; เหลือ mock เฉพาะโหมด `?addindemo`) · รายละเอียดการรันกับ backend: `docs/WORDADDIN-DEV.md` · สร้างไฟล์ทดสอบพร้อม ticket: `node scripts/make-ticket-docx.mjs` diff --git a/lib/addin-ticket.ts b/lib/addin-ticket.ts index 8ca876b..f0b9e57 100644 --- a/lib/addin-ticket.ts +++ b/lib/addin-ticket.ts @@ -33,10 +33,9 @@ export function addinTicketSecret(): string { } /** - * ตรวจ ticket แบบไม่สน exp (HMAC → parse) — ใช้เมื่อ route จะตัดสิน "ticket หมดอายุ - * แต่เอกสารนี้เคยทำงานโดย uid เดียวกัน" เองผ่าน DB (requirement ขั้น 7: กลับมาทำ - * เอกสารเดิมผ่านได้แม้ ticket หมดอายุ) — ตัวตนของผู้ถือยังยึด HMAC ของ e-Court - * เหมือนเดิม แค่ไม่ใช้อายุ ticket เป็นเกณฑ์ตัดสิทธิ์ ณ จุดนี้ + * ตรวจ ticket แบบไม่สน exp (HMAC → parse) — route เป็นคนตัดสิน exp เองภายหลัง + * (ลายเซ็นถูกแต่หมดอายุ = 403, contract 2026-11-04) — ตัวตนของผู้ถือยึด HMAC + * ของ e-Court เหมือนเดิม แค่ไม่ใช้อายุ ticket เป็นเกณฑ์ตัดสิทธิ์ ณ จุดนี้ */ export function verifyAddinTicketHmac( ticketData: string, @@ -89,7 +88,7 @@ export type TicketTokenBodyResult = /** * parse body ของ POST /api/documents/token ทางเข้า ticket - * - `missing` = ไม่มี ticket มาเลย (route ตอบ 401 — คำเดียวกับตอน verify ไม่ผ่าน) + * - `missing` = ไม่มี ticket มาเลย (route ตอบ 400 — ฟอร์แม็ตพัง, contract 2026-11-04) * - `invalid` = รูปผิด (route ตอบ 400 ตาม convention กัน typo เงียบ ๆ) */ export function parseTicketTokenBody(raw: unknown): TicketTokenBodyResult { diff --git a/lib/docver-store.ts b/lib/docver-store.ts index 11083cd..4069f51 100644 --- a/lib/docver-store.ts +++ b/lib/docver-store.ts @@ -252,22 +252,6 @@ export async function findLatestVersionForDoc( return row?.version ?? null; } -/** - * เอกสาร (docId) นี้เคยมี version ที่สร้างโดย userId นี้หรือยัง — ใช้เป็นหลักฐาน - * "เคยทำงานกับเอกสารนี้" ตอนยอมรับ ticket ที่หมดอายุ (requirement ขั้น 7: - * กลับมาทำเอกสารเดิมผ่านได้แม้ ticket หมดอายุ — ตัวตนยังยึด HMAC ของ e-Court - * ทางเข้า /api/documents/token ของ add-in) · อ่านอย่างเดียว ไม่แตะ MinIO - */ -export async function hasVersionByUser(docId: string, userId: string): Promise { - const [row] = await db - .select({ one: sql`1` }) - .from(documentVersions) - .innerJoin(documents, eq(documentVersions.documentId, documents.id)) - .where(and(eq(documents.docId, docId), eq(documentVersions.userId, userId))) - .limit(1); - return row !== undefined; -} - export type InsertVersionInput = { /** กำหนดเอง (ไม่ปล่อย defaultRandom) เพราะ object ใน MinIO เขียนด้วย uuid นี้ไปก่อนแล้ว */ versionId: string; diff --git a/lib/openapi.ts b/lib/openapi.ts index 50b8cfe..c9ef256 100644 --- a/lib/openapi.ts +++ b/lib/openapi.ts @@ -1,6 +1,7 @@ // OpenAPI 3.0 spec ของ API ทดสอบทั้งหมดของ judes-note — เส้นเดียวกับที่ Swagger UI // แสดงผล: กลุ่ม template (DOCX Content Control) + N10 external API + docver -// (จัด version เอกสาร) +// (จัด version เอกสาร) + auth (ทางเข้า path สั้น /api/auth/* — alias ของ +// /api/documents/token กับ /api/note/auth/session เพิ่ม 2026-11-05) // route handler GET /api/swagger (app/api/swagger/route.ts) เป็นคนเดียวที่ใช้ // — inline เป็น JSON ลงใน HTML ตอน respond (ไม่มี endpoint spec แยก) // @@ -25,7 +26,8 @@ export const openApiSpec = { version: "1.0.0", description: "API ทดสอบของระบบ Judge's Note — กลุ่ม Template (DOCX Content Control), " + - "API ภายนอกสำหรับ backend e-Court (N10) และ API จัด version เอกสาร (docver)", + "API ภายนอกสำหรับ backend e-Court (N10), API จัด version เอกสาร (docver) " + + "และทางเข้า auth แบบ path สั้น (/api/auth/*)", }, servers: [{ url: BASE_PATH, description: "origin เดียวกับหน้านี้ (basePath /docedit)" }], tags: [ @@ -59,6 +61,15 @@ export const openApiSpec = { "server-to-server ดึง annotations/bookmarks ของเอกสารที่เคยเปิดในระบบนี้ · " + "SSO: เส้น mint login-token สำหรับประกอบลิงก์เข้าสู่ระบบ (PLAN-SSO.md)", }, + { + name: "auth", + description: + "ทางเข้า auth แบบ **path สั้น** `/api/auth/*` (เพิ่ม 2026-11-05) — endpoint " + + "ก้อนเดียวกับของเดิมคนละ URL เลือกใช้ทางใดทางหนึ่งได้:\n" + + "- `POST /api/auth/token` = `POST /api/documents/token` — ออก access_token ของกลุ่ม documents (docver)\n" + + "- `POST /api/auth/session` = `POST /api/note/auth/session` — แลก credential ของลิงก์ e-Court " + + "เป็น session token ของแอป note", + }, ], paths: { "/api/template/test": { @@ -687,23 +698,27 @@ export const openApiSpec = { }, }, }, - "/api/documents/token": { + "/api/auth/token": { post: { - tags: ["documents"], - summary: "ขอ access_token ของกลุ่ม documents (docver)", + tags: ["auth"], + summary: "ขอ access_token ของกลุ่ม documents (docver) — path สั้นของ /api/documents/token", description: + "ทางเข้า **path สั้น** ของ `POST /api/documents/token` — endpoint ก้อนเดียวกัน " + + "พฤติกรรมเหมือนกันทุกประการ (เพิ่ม 2026-11-05)\n\n" + "ออก `access_token` (JWT HS256) ไว้เรียก endpoint อื่นของกลุ่ม documents — " + "token ฝัง claims `{userid, username, fid, template}` อายุตาม env " + - "`DOCVER_TOKEN_TTL_SECONDS` (default 3600 วินาที)\n\n" + + "`DOCVER_TOKEN_TTL_SECONDS` (default 604800 วินาที = 7 วัน — token อายุยาวแทน" + + "ระบบ refresh/renew ของ add-in)\n\n" + "**ทางเข้า 2 แบบ** — เลือกตามว่าใครเป็นคนเรียก (ดูตัวอย่างใน Request body ได้ทั้งคู่):\n\n" + "1. **ระบบภายนอก (server-to-server)** — แนบ header `x-api-key` (ค่าจาก env " + "`DOCVER_API_KEY`) แล้วส่ง claims ตรง ๆ ใน body ตาม schema `TokenRequest`\n" + "2. **add-in ใน Word** — ไม่ต้องมี `x-api-key` ส่ง body `{ticket_data, HmacSha256}` " + "ตาม schema `TicketTokenRequest` (คู่ค่าที่ e-Court stamp ไว้ใน custom properties " + "ของไฟล์ .docx) — server ตรวจลายเซ็น HMAC-SHA256 ด้วย env `ADDIN_TICKET_SECRET` " + - '(ไม่ตั้ง → env `SECRET` → `"super_secret"`) แล้วออก token จาก claims ที่อยู่ในตัว ticket เอง · ' + - "**ticket หมดอายุ** ยังออกให้ต่อเมื่อเอกสารนี้ (`fid`) เคยมี version ที่สร้างโดย `uid` เดียวกัน " + - "(ผู้ที่เคยทำงานกับเอกสารเดิมกลับมาใช้ต่อได้แม้ ticket หมดอายุ — ไม่เคยทำงาน = 401)\n\n" + + '(ไม่ตั้ง → env `SECRET` → `"super_secret"`) แล้วออก token จาก claims ที่อยู่ในตัว ticket เอง ' + + "(contract 2026-11-04: ฟอร์แม็ตพัง/ไม่ครบ = 400 · ลายเซ็นไม่ถูก = 401 · " + + "**ลายเซ็นถูกแต่ ticket หมดอายุ = 403** — add-in ที่มี access_token เดิมของ `fid` " + + "นี้ใน session ยังใช้ต่อได้ ไม่มี = ขอไฟล์ใหม่จาก e-Court)\n\n" + "ได้ token แล้วแนบเป็น `Authorization: Bearer ` กับทุก request ถัดไป", security: [{ ApiKeyAuth: [] }], requestBody: { @@ -753,7 +768,190 @@ export const openApiSpec = { 401: { description: "ทางเข้า x-api-key: ไม่ได้ส่ง header หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY) · " + - "ทางเข้า ticket: ticket ไม่ถูกต้อง/หมดอายุ หรือไม่ได้ส่งมา (ข้อความเดียวกันทุกสาเหตุ)", + "ทางเข้า ticket: ลายเซ็น HMAC ไม่ถูก (ข้อความเดียวกันทุกสาเหตุ)", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + 403: { + description: + "เฉพาะทางเข้า ticket: ลายเซ็นถูกแต่ ticket หมดอายุ — ใช้ access_token เดิมของ " + + "fid นี้ใน session ต่อ หรือขอไฟล์ใหม่จาก e-Court (contract 2026-11-04)", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + 500: { $ref: "#/components/responses/ServerError" }, + }, + }, + }, + "/api/auth/session": { + post: { + tags: ["auth"], + summary: "แลก credential ของลิงก์ e-Court เป็น session token ของแอป note (SSO)", + description: + "ทางเข้า **path สั้น** ของ `POST /api/note/auth/session` — endpoint ก้อนเดียวกัน " + + "พฤติกรรมเหมือนกันทุกประการ (เพิ่ม 2026-11-05) · flow SSO ของแอป note " + + "(PLAN-SSO.md §3.2 ฉบับ token): ลิงก์ e-Court พา browser มาหน้า `/note` พร้อม " + + "credential → client เรียกเส้นนี้แลกเป็น access_token เก็บ localStorage " + + '("note-auth") แล้วแนบ `Authorization: Bearer` กับทุก API/Server Action ของแอป\n\n' + + "- ส่ง credential **ทางเดียว**: `t` (login-token จาก `POST /api/note/auth/token`) " + + "หรือ `ticket_data`+`HmacSha256` (คู่ค่าชุดเดียวกับที่ e-Court stamp ใน custom " + + "properties ของไฟล์ .docx ของ add-in) — สองชุดพร้อมกัน = 400 (D12)\n" + + "- token ที่ได้เป็นชนิด `purpose=login` อายุ 8 ชม. (28800 วินาที) — ใช้ได้กับแอป " + + "note เท่านั้น เอาไปยิง `/api/documents/*` ของกลุ่ม docver ไม่ได้ (401)\n" + + "- ส่ง `fileUrl` มาด้วย = ลงทะเบียนเอกสาร N8 แบบ trusted ฝั่ง server ทันที " + + "(fid จาก credential ที่ verify แล้ว) — ล้มก็ยังได้ token พร้อม `fileError` " + + "ให้หน้าแรกแสดงสาเหตุ · `fileUrl` ต้องผ่าน allowlist ของ N8 " + + "(admincourt.go.th + env `EXTERNAL_FILE_HOSTS`) หรือเป็น path `/samples/…`\n" + + "- **ทาง ticket ใน production**: secret ต้องตั้งจริง (env `ADDIN_TICKET_SECRET` " + + '→ `SECRET`) — ค่า default `"super_secret"` ที่รู้กันทั้งระบบถูกปฏิเสธ (500)', + requestBody: { + required: true, + content: { + "application/json": { + schema: { $ref: "#/components/schemas/SessionExchangeRequest" }, + examples: { + "ทาง JWT (param t ของลิงก์ e-Court)": { + value: { + t: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdXJwb3NlIjoibG9naW4iLCJ1c2VyaWQiOiJvb20iLCJ1c2VybmFtZSI6IuC4geC4m+C4tOC5iOC4quC4lOC5iCIsImZpZCI6IkYxMjMifQ.….….…", + fileUrl: "https://service.admincourt.go.th/uploads/F123.pdf", + typeId: "T12", + }, + }, + "ทาง ticket (คู่ค่าเดียวกับไฟล์ .docx ของ add-in)": { + value: { + ticket_data: + '{"exp":1793067433,"fid":"T12_FD155_3s-long_ZZZ","tid":"T12","uid":"oom","uname":"สรวิชญ์"}', + HmacSha256: + "1dc53fbd48410845c895ddb841a8fe4ad93da413126fae9c688202e57f2baaa9", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + description: + "session token ของแอป note (พร้อม fileId ถ้าลงทะเบียน fileUrl สำเร็จ)", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/SessionResponse" }, + }, + }, + }, + 400: { $ref: "#/components/responses/BadRequest" }, + 401: { + description: + "credential หมดอายุหรือไม่ถูกต้อง — ทาง JWT แยก `reason: expired` (หมดอายุ) กับ " + + "`invalid` · ทาง ticket รวมเป็น `invalid` ทั้งลายเซ็นผิดและหมดอายุ", + content: { + "application/json": { + schema: { + allOf: [ + { $ref: "#/components/schemas/Error" }, + { + type: "object", + properties: { + reason: { + type: "string", + enum: ["expired", "invalid"], + }, + }, + }, + ], + }, + }, + }, + }, + 500: { $ref: "#/components/responses/ServerError" }, + }, + }, + }, + "/api/documents/token": { + post: { + tags: ["documents"], + summary: "ขอ access_token ของกลุ่ม documents (docver)", + description: + "ออก `access_token` (JWT HS256) ไว้เรียก endpoint อื่นของกลุ่ม documents — " + + "token ฝัง claims `{userid, username, fid, template}` อายุตาม env " + + "`DOCVER_TOKEN_TTL_SECONDS` (default 604800 วินาที = 7 วัน — token อายุยาวแทน" + + "ระบบ refresh/renew ของ add-in)\n\n" + + "**ทางเข้า 2 แบบ** — เลือกตามว่าใครเป็นคนเรียก (ดูตัวอย่างใน Request body ได้ทั้งคู่):\n\n" + + "1. **ระบบภายนอก (server-to-server)** — แนบ header `x-api-key` (ค่าจาก env " + + "`DOCVER_API_KEY`) แล้วส่ง claims ตรง ๆ ใน body ตาม schema `TokenRequest`\n" + + "2. **add-in ใน Word** — ไม่ต้องมี `x-api-key` ส่ง body `{ticket_data, HmacSha256}` " + + "ตาม schema `TicketTokenRequest` (คู่ค่าที่ e-Court stamp ไว้ใน custom properties " + + "ของไฟล์ .docx) — server ตรวจลายเซ็น HMAC-SHA256 ด้วย env `ADDIN_TICKET_SECRET` " + + '(ไม่ตั้ง → env `SECRET` → `"super_secret"`) แล้วออก token จาก claims ที่อยู่ในตัว ticket เอง ' + + "(contract 2026-11-04: ฟอร์แม็ตพัง/ไม่ครบ = 400 · ลายเซ็นไม่ถูก = 401 · " + + "**ลายเซ็นถูกแต่ ticket หมดอายุ = 403** — add-in ที่มี access_token เดิมของ `fid` " + + "นี้ใน session ยังใช้ต่อได้ ไม่มี = ขอไฟล์ใหม่จาก e-Court)\n\n" + + "ได้ token แล้วแนบเป็น `Authorization: Bearer ` กับทุก request ถัดไป", + security: [{ ApiKeyAuth: [] }], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + oneOf: [ + { $ref: "#/components/schemas/TokenRequest" }, + { $ref: "#/components/schemas/TicketTokenRequest" }, + ], + }, + examples: { + "server-to-server ด้วย x-api-key (ครบทุก claim)": { + value: { + userid: "123", + username: "Sorawit Bholsithi", + fid: "3", + template: "T12", + }, + }, + "server-to-server ขั้นต่ำ (template เป็นทางเลือก)": { + value: { userid: "123", username: "Sorawit Bholsithi", fid: "3" }, + }, + "add-in ด้วย ticket จากไฟล์ (ไม่ต้องมี x-api-key)": { + value: { + ticket_data: + '{"exp":1793067433,"fid":"T12_FD155_3s-long_ZZZ","tid":"T12","uid":"oom","uname":"สรวิชญ์"}', + HmacSha256: + "1dc53fbd48410845c895ddb841a8fe4ad93da413126fae9c688202e57f2baaa9", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + description: "access_token พร้อมใช้ (แนบเป็น Authorization: Bearer ทุก request ถัดไป)", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/TokenResponse" }, + }, + }, + }, + 400: { $ref: "#/components/responses/BadRequest" }, + 401: { + description: + "ทางเข้า x-api-key: ไม่ได้ส่ง header หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY) · " + + "ทางเข้า ticket: ลายเซ็น HMAC ไม่ถูก (ข้อความเดียวกันทุกสาเหตุ)", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + 403: { + description: + "เฉพาะทางเข้า ticket: ลายเซ็นถูกแต่ ticket หมดอายุ — ใช้ access_token เดิมของ " + + "fid นี้ใน session ต่อ หรือขอไฟล์ใหม่จาก e-Court (contract 2026-11-04)", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" }, @@ -945,7 +1143,7 @@ export const openApiSpec = { "- `type=pdf` → คืนไบต์ pdf ตรง ๆ (version ล่าสุดไม่มีสำเนา PDF = 404)\n" + "- `type=docx` → server เติม `exp` ใหม่ (now + `TICKET_SEC` default 300 วิ) + " + "คำนวณ `HmacSha256` (secret ตาม chain `ADDIN_TICKET_SECRET` → `SECRET` → " + - "`\"super_secret\"` ให้ตรงฝั่ง verify ของ add-in) และ `Ed25519` (เฉพาะเมื่อ " + + "`\"super_secret\"` ให้ตรงฝั่ง verify ของ server — ทาง ticket ของ /token) และ `Ed25519` (เฉพาะเมื่อ " + "`PRIVATE_KEY_HEX` ตั้ง) แล้วเขียน custom properties ลง**สำเนาในหน่วยความจำ** " + "ก่อนคืนไฟล์ — object ใน MinIO ไม่ถูกแต่ (non-destructive) และเขียนทับ stamp " + "เดิมของ template ได้\n" + @@ -1127,13 +1325,80 @@ export const openApiSpec = { }, TokenResponse: { type: "object", - description: "access_token ของกลุ่ม documents (JWT HS256 อายุสั้น)", + description: "access_token ของกลุ่ม documents (JWT HS256)", properties: { access_token: { type: "string", description: "JWT พร้อมใช้แนบเป็น Bearer" }, token_type: { type: "string", example: "Bearer" }, expires_in: { type: "integer", - description: "อายุวินาที (env DOCVER_TOKEN_TTL_SECONDS, default 3600)", + description: "อายุวินาที (env DOCVER_TOKEN_TTL_SECONDS, default 604800 = 7 วัน)", + }, + }, + }, + SessionExchangeRequest: { + type: "object", + description: + "credential ของลิงก์ e-Court ที่จะแลกเป็น session token — ส่งทางเดียว: `t` " + + "**หรือ** `ticket_data`+`HmacSha256` (สองชุดพร้อมกัน = 400 ตาม D12) · " + + "allowlist 5 field เป๊ะ — ส่ง field อื่น = 400", + properties: { + t: { + type: "string", + description: + "login-token (JWT purpose=login) จาก POST /api/note/auth/token — ≤ 8192 ตัวอักษร", + }, + ticket_data: { + type: "string", + description: + "JSON string ของ ticket ชุดเดียวกับที่ e-Court stamp ในไฟล์ .docx — " + + "≤ 2048 ตัวอักษร (ส่งตรง ๆ ไม่แก้ string — HMAC ผูกกับค่าดิบ)", + }, + HmacSha256: { + type: "string", + description: "hex 64 ตัว ของ HMAC-SHA256(secret, ticket_data ดิบ) — ≤ 128 ตัวอักษร", + }, + fileUrl: { + type: "string", + description: + "URL เอกสารบน e-Court — ต้องผ่าน allowlist ของ N8 (admincourt.go.th + env " + + "EXTERNAL_FILE_HOSTS) · ส่งมา = ลงทะเบียนเอกสารตอนแลก token (≤ 2048 ตัวอักษร)", + }, + typeId: { + type: "string", + description: "รหัสกลุ่มเอกสารของ e-Court (≤ 200 ตัวอักษร)", + }, + }, + }, + SessionResponse: { + type: "object", + description: + "session ของแอป note — client เก็บใน localStorage (\"note-auth\") แล้วแนบ " + + "Authorization: Bearer กับทุก API/Server Action ของแอป", + properties: { + access_token: { + type: "string", + description: "JWT purpose=login อายุ 8 ชม. — ใช้ได้กับแอป note เท่านั้น", + }, + token_type: { type: "string", example: "Bearer" }, + expires_in: { + type: "integer", + description: "อายุวินาที (NOTE_SESSION_TTL = 28800 = 8 ชม.)", + example: 28800, + }, + userid: { type: "string", description: "รหัสผู้ใช้จาก credential (u_userid ของ e-Court)" }, + username: { type: "string", description: "ชื่อแสดงผลจาก credential" }, + fileId: { + type: "string", + format: "uuid", + description: + "มีเมื่อส่ง fileUrl และลงทะเบียนสำเร็จ — id ของเอกสารในระบบนี้ " + + "(ใช้เข้าหน้าอ่าน /note/annotate/)", + }, + fileError: { + type: "string", + description: + "มีเมื่อลงทะเบียน fileUrl ไม่สำเร็จ (ไม่ผ่าน allowlist ฯลฯ) — token ยังออกให้ " + + "หน้าแรกแสดงสาเหตุ", }, }, }, diff --git a/package-lock.json b/package-lock.json index d6d21b9..0cc0cf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,7 @@ "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", @@ -83,6 +84,7 @@ "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -92,7 +94,8 @@ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@apidevtools/swagger-parser": { "version": "10.1.1", @@ -100,6 +103,7 @@ "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@apidevtools/json-schema-ref-parser": "11.7.2", "@apidevtools/openapi-schemas": "^2.1.0", @@ -170,6 +174,7 @@ "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -183,6 +188,7 @@ "integrity": "sha512-Vu011o3/bikQNwtjouwmUJud+Z6Brcjij2D0omPWClRGg8i5gBfOYSpDkFGkHbhGlaky4fgvfkxD0uHGq34uYA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", @@ -202,6 +208,7 @@ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.2.0" }, @@ -215,6 +222,7 @@ "integrity": "sha512-JbZtIqfEulsIA0rC3zM7jfF4KkOnye9aKcaO/jJqxJRm/gM6lAjEv7sL4njW8D+35l50P1f+UuH5OqN+UKJqNA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", @@ -234,6 +242,7 @@ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.2.0" }, @@ -247,6 +256,7 @@ "integrity": "sha512-J2jmTPv8ZraSHDTz9l2Bx8gNL3ktfDDWo2mxWfzarn64O9Fjhb+l85YWyubGy2xUdeGuZPKzvQLltGv8bSu8eQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", @@ -266,6 +276,7 @@ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.2.0" }, @@ -279,6 +290,7 @@ "integrity": "sha512-DR/H2nfKtHNqfpuJ4L/B4irX1nX77QizulmfrxcLNZmkfinm0SdZpypXSvzaI5rHZSXfhXNUfBvMfi+jMkjWtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", @@ -298,6 +310,7 @@ "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", @@ -333,6 +346,7 @@ "integrity": "sha512-BoSmXPx2er1Ai+wKlDvj29jIQespCNBwEmKyZVHO2kEFsWbGjAjwMCGzug3DJM5/QYIV3vej0S1zcU5bq9fa8w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2" }, @@ -350,6 +364,7 @@ "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", @@ -366,6 +381,7 @@ "integrity": "sha512-7GEAoIsaoBr6KELNRb8nypowCqvk8dnCHFCYg4XD4lOQGY2GqjQg5IhkRjyBFRO18CGSMq05PaNqSOE9GQro3g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -379,6 +395,7 @@ "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=22.0.0" } @@ -409,6 +426,7 @@ "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -422,6 +440,7 @@ "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", @@ -437,6 +456,7 @@ "integrity": "sha512-e7lX/dk//F6Qf7BB6PTY4+p2yuOQtyOeHGyapYHNwqSp2OnYpwQt49A/Nin2XmKBQ69pwagR4k/lQBq8lbHQkA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-xml-parser": "^5.5.9", "tslib": "^2.8.1" @@ -451,6 +471,7 @@ "integrity": "sha512-HHuuFrmN1uuF9D5A6aAo5eHzNFNX4Q+P4SksZqDKse5YaJFFy/zDLG3wXHEV2FVGgetZ3QM8jGURNBtnQYf1NQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", @@ -499,6 +520,7 @@ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -512,6 +534,7 @@ "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", @@ -533,6 +556,7 @@ "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" @@ -547,6 +571,7 @@ "integrity": "sha512-NDW2/bz2Pz71EJAlhSw+jg1kpRIWtrzj0gCzcHuG1u7iLvSOHpUc5lICkDA9x0bbeityxTneiunSBluvS+/y4w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/msal-common": "16.14.2" }, @@ -560,6 +585,7 @@ "integrity": "sha512-HOBR0Gfo39TMrkxNG8k6cfjRkrRjXZc3JQs3ebMWQc9S4cihuknMYra3sgmzbZB752oQBqBN9+WF7c826p2vIQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.0" } @@ -570,6 +596,7 @@ "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/msal-common": "16.14.1", "jsonwebtoken": "^9.0.0" @@ -585,6 +612,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/msal-common": "15.17.0", "@azure/msal-node-runtime": "^0.20.0", @@ -600,6 +628,7 @@ "integrity": "sha512-VQ5/gTLFADkwue+FohVuCqlzFPUq4xSrX8jeZe+iwZuY6moliNC8xt86qPVNYdtbQfELDf2Nu6LI+demFPHGgw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.0" } @@ -609,7 +638,8 @@ "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.20.6.tgz", "integrity": "sha512-89Al7l5c8sOEA954d35WZOg1sCa2WKfgf7KyTbN9vIkKsoZrXdJ5+CPjy8+tQqWcsJTAMbQD4OWr7V09/xN8KA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { "version": "16.14.1", @@ -617,6 +647,7 @@ "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.0" } @@ -627,6 +658,7 @@ "integrity": "sha512-cXDTCAMwDx8Ng9yUe82kOOd2kPnZ8AQqe4FkLMcpaQ3Mvr3/PXVdvRhyP0FtY5/U5Oh6JGk3EcEibaFOymRLFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", @@ -654,6 +686,7 @@ "integrity": "sha512-bttzuhQiCIwrkzjPDA+AtAR7dg19L/CC6ztcqJ5LfvWpXuys9mHp0UQ0udYnoUvv9SCT9KTR5kqFvFr0e6k0lQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", @@ -675,6 +708,7 @@ "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -699,6 +733,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.1.90" } @@ -791,7 +826,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -840,7 +874,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1753,7 +1786,8 @@ "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@feathersjs/hooks": { "version": "0.6.5", @@ -1761,6 +1795,7 @@ "integrity": "sha512-WtcEoG/imdHRvC3vofGi/OcgH+cjHHhO0AfEeTlsnrKLjVKKBXV6aoIrB2nHZPpE7iW5sA7AZMR6bPD8ytxN+w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10" } @@ -1771,6 +1806,7 @@ "integrity": "sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20" }, @@ -1953,6 +1989,7 @@ "integrity": "sha512-w3PMZH5rahrukn8/I7P9Ihil+twgLTUHDZtJlJyBbUKyPaOSSQjLZkb0PpncVhin1gCaMgOFXy6iNPgcZUoo2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -1979,6 +2016,7 @@ "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1988,7 +2026,8 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@inquirer/editor": { "version": "4.2.23", @@ -2691,6 +2730,7 @@ "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mute-stream": "^1.0.0" }, @@ -2704,6 +2744,7 @@ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "minipass": "^7.0.4" }, @@ -2738,6 +2779,7 @@ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2766,7 +2808,8 @@ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@microsoft/app-manifest": { "version": "1.1.3", @@ -2790,6 +2833,7 @@ "integrity": "sha512-OayhehwI+CnO0Wr53e29ZJZWGsNA5yVG7r54qmZSLc5HxA5Cozk4hP7EbYDCXkxh4NbQoT1dhTzC8bkRo+wWXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer": "^5.2.1", "debug": "^4.1.1", @@ -2802,6 +2846,7 @@ "integrity": "sha512-wGuFEzvRiWZmDxQMGKEjOKhEIVnLiG6vRUuM9Hwqxpe/kbiyA2WiUyEVpniNPaaw8gDHTf9zJHnPNNj0JiL5mA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@microsoft/dev-tunnels-contracts": ">1.1.8", "axios": "^1.6.2", @@ -2816,6 +2861,7 @@ "integrity": "sha512-YlAI2jC0grK2p2fvxDV9Hin5EtRXL6yOMhLKqtjRVlH8Q0+hI9UWFl7RhLWSZjsbDyl+0LIlI2HjlcbsUhXGsQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "adm-zip": "^0.5.17", "original-fs": "^1.2.0", @@ -2829,6 +2875,7 @@ "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12.0" } @@ -2843,6 +2890,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "peer": true, "bin": { "uuid": "dist-node/bin/uuid" } @@ -2853,6 +2901,7 @@ "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=14.0.0" } @@ -2863,6 +2912,7 @@ "integrity": "sha512-mih5trnmvIKpjLgqzsoxqiWFvG5eMeETPCAdtFvDaXyferWzFt9UrdARfRoYboPhfFjYRYw8EoF3EA4yFVWCsQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@apidevtools/swagger-parser": "^10.1.1", "@microsoft/app-manifest": "1.1.3", @@ -2881,6 +2931,7 @@ "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2945,6 +2996,7 @@ "integrity": "sha512-cSVQHqK7oYG+KvSR8y+nvOiXcLNU/t/8ihDyXbdJtZCnRft7ZtHpnt8yxL96xdFe2UUHLSipHbZSJBotQrIB+g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/core-auth": "^1.4.0", "@microsoft/app-manifest": "1.1.3", @@ -2960,6 +3012,7 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -2970,6 +3023,7 @@ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -2989,6 +3043,7 @@ "integrity": "sha512-156FXuXXl8DZ6aPU/GR0qFpo7JIHK9sBTqhUBOiO0ljRl+asP/S9nREQG4Vg4lPWL1UCCd6WeTpTcOPyyYQdJQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", "@azure/arm-appservice": "^13.0.0", @@ -3059,6 +3114,7 @@ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -3072,6 +3128,7 @@ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -3082,6 +3139,7 @@ "integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", @@ -3123,6 +3181,7 @@ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -3137,6 +3196,7 @@ "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^2.1.0", @@ -3162,6 +3222,7 @@ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3176,6 +3237,7 @@ "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3190,6 +3252,7 @@ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.6.0" } @@ -3200,6 +3263,7 @@ "integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.3.0", @@ -3244,6 +3308,7 @@ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3258,6 +3323,7 @@ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -3280,6 +3346,7 @@ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -3290,6 +3357,7 @@ "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" }, @@ -3304,6 +3372,7 @@ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3317,6 +3386,7 @@ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -3327,6 +3397,7 @@ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -3344,6 +3415,7 @@ "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "content-type": "^2.1.0" }, @@ -3361,6 +3433,7 @@ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3375,6 +3448,7 @@ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", @@ -3402,6 +3476,7 @@ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -3422,6 +3497,7 @@ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", @@ -3441,6 +3517,7 @@ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -4210,6 +4287,7 @@ "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/oxc-project" } @@ -4254,6 +4332,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4271,6 +4350,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4288,6 +4368,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4305,6 +4386,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4322,6 +4404,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4339,6 +4422,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4356,6 +4440,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4373,6 +4458,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4390,6 +4476,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4407,6 +4494,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4424,6 +4512,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4441,6 +4530,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4458,6 +4548,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4475,6 +4566,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4492,6 +4584,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4501,7 +4594,8 @@ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@scarf/scarf": { "version": "1.4.0", @@ -4567,7 +4661,8 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/jsonfile": { "version": "6.1.4", @@ -4585,6 +4680,7 @@ "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -4629,7 +4725,6 @@ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4661,7 +4756,8 @@ "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@typespec/ts-http-runtime": { "version": "0.3.9", @@ -4669,6 +4765,7 @@ "integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", @@ -4731,6 +4828,7 @@ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -4745,6 +4843,7 @@ "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4758,6 +4857,7 @@ "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -4778,6 +4878,7 @@ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 14" } @@ -4838,6 +4939,7 @@ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "type-fest": "^0.21.3" }, @@ -4913,6 +5015,7 @@ "integrity": "sha512-Hxe6Agq26gQOM954qpzYSllJBPJl+e16U5CkfuMUhLrNba+5nKkttIVlflaovN6oaTratqMGAO8H5u/aNhmHWQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@types/node": "^25.2.0", "flatbuffers": "^25.1.24", @@ -4929,6 +5032,7 @@ "integrity": "sha512-b4e2xxj/yMeT2hNlD6x7zNFiQK2Dlmkb9CQ6v3/1G2RezKN4YS2lcEv8lzGm1mhs4f2lM4yNAMI/zUDBZBBt7w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -4938,7 +5042,8 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/applicationinsights": { "version": "1.8.10", @@ -4946,6 +5051,7 @@ "integrity": "sha512-ZLDA7mShh4mP2Z/HlFolmvhBPX1LfnbIWXrselyYVA7EKjHhri1fZzpu2EiWAmfbRxNBY6fRjoPJWbx5giKy4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cls-hooked": "^4.2.2", "continuation-local-storage": "^3.2.1", @@ -4965,21 +5071,24 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "Python-2.0" + "license": "Python-2.0", + "peer": true }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/array-timsort": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/assertion-error": { "version": "2.0.1", @@ -5003,6 +5112,7 @@ "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "stack-chain": "^1.3.7" }, @@ -5016,6 +5126,7 @@ "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "semver": "^5.3.0", "shimmer": "^1.1.0" @@ -5030,6 +5141,7 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver" } @@ -5040,6 +5152,7 @@ "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.1" } @@ -5049,7 +5162,8 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/at-least-node": { "version": "1.0.0", @@ -5057,6 +5171,7 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -5104,6 +5219,7 @@ "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", @@ -5117,6 +5233,7 @@ "integrity": "sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4", "is-retry-allowed": "^2.2.0" @@ -5128,6 +5245,7 @@ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "4" }, @@ -5141,6 +5259,7 @@ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -5155,6 +5274,7 @@ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "18 || 20 || >=22" } @@ -5178,7 +5298,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/baseline-browser-mapping": { "version": "2.11.21", @@ -5222,6 +5343,7 @@ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -5243,6 +5365,7 @@ "integrity": "sha512-FISIQxV5u91Y9gvuFq3q/tA/qTqLJn3AhRcEvyozyhJTh5OBFFtv827AdHU7Rbyct4r+Ba1/99oFa1Y3yuFsdw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", @@ -5268,6 +5391,7 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -5278,6 +5402,7 @@ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -5290,7 +5415,8 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/body-parser/node_modules/raw-body": { "version": "2.5.3", @@ -5298,6 +5424,7 @@ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", @@ -5314,6 +5441,7 @@ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -5328,6 +5456,7 @@ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^4.0.2" }, @@ -5374,7 +5503,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", @@ -5409,6 +5537,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -5428,7 +5557,8 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/buffer-from": { "version": "1.1.2", @@ -5443,6 +5573,7 @@ "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "run-applescript": "^7.0.0" }, @@ -5470,6 +5601,7 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -5480,6 +5612,7 @@ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -5494,6 +5627,7 @@ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -5510,7 +5644,8 @@ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/camelcase-css": { "version": "2.0.1", @@ -5558,6 +5693,7 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5582,6 +5718,7 @@ "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": "*" } @@ -5592,6 +5729,7 @@ "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "get-func-name": "^2.0.2" }, @@ -5643,6 +5781,7 @@ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "engines": { "node": ">=18" } @@ -5653,6 +5792,7 @@ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" }, @@ -5666,6 +5806,7 @@ "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "string-width": "^4.2.0" }, @@ -5698,6 +5839,7 @@ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -5713,6 +5855,7 @@ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -5731,6 +5874,7 @@ "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "async-hook-jl": "^1.7.6", "emitter-listener": "^1.0.1", @@ -5746,6 +5890,7 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver" } @@ -5776,6 +5921,7 @@ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5799,6 +5945,7 @@ "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-timsort": "^1.0.3", "esprima": "^4.0.1" @@ -5813,6 +5960,7 @@ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -5826,6 +5974,7 @@ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -5836,6 +5985,7 @@ "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "async-listener": "^0.6.0", "emitter-listener": "^1.1.1" @@ -5847,6 +5997,7 @@ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -5856,7 +6007,8 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/core-util-is": { "version": "1.0.3", @@ -5870,6 +6022,7 @@ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "object-assign": "^4", "vary": "^1" @@ -5888,6 +6041,7 @@ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5903,6 +6057,7 @@ "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": "*" } @@ -5912,7 +6067,8 @@ "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.4.0.tgz", "integrity": "sha512-9jpMU9HMt1vhMUqNO+MPuGEpbh/f7HHZdxrd6L2DMwTuYGyt9pgUJfQyTS1Ei4/sn7qPM4FkjxUoiW79k0x8sA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/css-select": { "version": "7.0.0", @@ -5920,6 +6076,7 @@ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "boolbase": "^2.0.0", "css-what": "^8.0.0", @@ -5955,6 +6112,7 @@ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -5981,7 +6139,8 @@ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/csstype": { "version": "3.2.3", @@ -6020,6 +6179,7 @@ "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4.0" } @@ -6030,6 +6190,7 @@ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -6064,6 +6225,7 @@ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -6080,7 +6242,8 @@ "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/deep-eql": { "version": "4.1.4", @@ -6088,6 +6251,7 @@ "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "type-detect": "^4.0.0" }, @@ -6101,6 +6265,7 @@ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4.0.0" } @@ -6111,6 +6276,7 @@ "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" @@ -6128,6 +6294,7 @@ "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -6141,6 +6308,7 @@ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -6151,6 +6319,7 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.4.0" } @@ -6161,6 +6330,7 @@ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -6171,6 +6341,7 @@ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -6182,6 +6353,7 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8" } @@ -6192,6 +6364,7 @@ "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "address": "^1.0.1", "debug": "4" @@ -6210,6 +6383,7 @@ "integrity": "sha512-6eb9YRrimz8oTr5+JDzGmSYnXy5V7YnK5y/hd8AUDK1MssHjQKm9LlD6NSrHx4vMDF3+e/spI2hmWTviElgWZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "semver": "^5.3.0" } @@ -6220,6 +6394,7 @@ "integrity": "sha512-l126t01d2ZS9EreskvEtZPrcgstuvH3rbKy82oUhUrVmBaGx4hO9wECdl3cvZbKDYjMF3QJDB5z5dL9yWAjvZQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "diagnostic-channel": "*" } @@ -6230,6 +6405,7 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver" } @@ -6273,6 +6449,7 @@ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", @@ -6298,6 +6475,7 @@ } ], "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -6308,6 +6486,7 @@ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "domelementtype": "^3.0.0" }, @@ -6325,6 +6504,7 @@ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", @@ -6344,6 +6524,7 @@ "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=10" } @@ -6979,6 +7160,7 @@ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -6994,6 +7176,7 @@ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" } @@ -7003,7 +7186,8 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/electron-to-chromium": { "version": "1.5.423", @@ -7018,6 +7202,7 @@ "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "shimmer": "^1.2.0" } @@ -7035,6 +7220,7 @@ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -7045,6 +7231,7 @@ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "once": "^1.4.0" } @@ -7068,6 +7255,7 @@ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" } @@ -7095,6 +7283,7 @@ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -7108,6 +7297,7 @@ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -7123,7 +7313,8 @@ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/esbuild": { "version": "0.28.2", @@ -7182,7 +7373,8 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/escape-string-regexp": { "version": "1.0.5", @@ -7200,6 +7392,7 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -7224,6 +7417,7 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -7240,6 +7434,7 @@ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -7250,6 +7445,7 @@ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "eventsource-parser": "^3.0.1" }, @@ -7263,6 +7459,7 @@ "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" } @@ -7273,6 +7470,7 @@ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, "license": "(MIT OR WTFPL)", + "peer": true, "engines": { "node": ">=6" } @@ -7341,6 +7539,7 @@ "integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" @@ -7361,6 +7560,7 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -7370,7 +7570,8 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -7414,7 +7615,8 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-uri": { "version": "3.1.9", @@ -7512,6 +7714,7 @@ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -7550,6 +7753,7 @@ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -7569,6 +7773,7 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -7578,21 +7783,24 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/flatbuffers": { "version": "25.9.23", "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/flatted": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/follow-redirects": { "version": "1.16.2", @@ -7606,6 +7814,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=4.0" }, @@ -7621,6 +7830,7 @@ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7651,6 +7861,7 @@ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -7675,6 +7886,7 @@ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -7684,7 +7896,8 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fs-extra": { "version": "9.1.0", @@ -7692,6 +7905,7 @@ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -7733,6 +7947,7 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -7743,6 +7958,7 @@ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -7753,6 +7969,7 @@ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -7778,6 +7995,7 @@ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -7804,7 +8022,8 @@ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/glob": { "version": "13.0.6", @@ -7812,6 +8031,7 @@ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", @@ -7843,6 +8063,7 @@ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -7862,6 +8083,7 @@ "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.8", "neo-async": "^2.6.2", @@ -7884,6 +8106,7 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7894,6 +8117,7 @@ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -7907,6 +8131,7 @@ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -7959,7 +8184,8 @@ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/htmlparser2": { "version": "10.1.0", @@ -7974,6 +8200,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -7987,6 +8214,7 @@ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -8002,6 +8230,7 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=0.12" }, @@ -8020,7 +8249,8 @@ "url": "https://github.com/sponsors/fb55" } ], - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/htmlparser2/node_modules/domhandler": { "version": "5.0.3", @@ -8028,6 +8258,7 @@ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -8044,6 +8275,7 @@ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -8059,6 +8291,7 @@ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=0.12" }, @@ -8072,6 +8305,7 @@ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", @@ -8093,6 +8327,7 @@ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -8106,7 +8341,8 @@ "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/https-proxy-agent": { "version": "7.0.6", @@ -8114,6 +8350,7 @@ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -8158,7 +8395,8 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/ignore": { "version": "5.3.2", @@ -8166,6 +8404,7 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -8187,7 +8426,8 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/inquirer": { "version": "12.11.1", @@ -8288,6 +8528,7 @@ "integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 12" } @@ -8298,6 +8539,7 @@ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.10" } @@ -8320,7 +8562,8 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/is-core-module": { "version": "2.16.2", @@ -8344,6 +8587,7 @@ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "is-docker": "cli.js" }, @@ -8393,6 +8637,7 @@ "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20" }, @@ -8406,6 +8651,7 @@ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-docker": "^3.0.0" }, @@ -8425,6 +8671,7 @@ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "is-docker": "cli.js" }, @@ -8457,7 +8704,8 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/is-retry-allowed": { "version": "2.2.0", @@ -8465,6 +8713,7 @@ "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -8490,6 +8739,7 @@ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-docker": "^2.0.0" }, @@ -8508,7 +8758,8 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/jiti": { "version": "1.21.7", @@ -8516,7 +8767,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -8527,6 +8777,7 @@ "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -8536,7 +8787,8 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.9.4.tgz", "integrity": "sha512-PtOMXpEGuP0RRiRXsjzHzl44dMHxSu2CPvAhinupR1tBa88me+1DPqsobl7eupn5UukjLkln1ZnAOAOXOrYG0Q==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/js-tokens": { "version": "4.0.0", @@ -8560,6 +8812,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -8573,7 +8826,6 @@ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", @@ -8628,14 +8880,16 @@ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/json-with-bigint": { "version": "3.5.12", "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.12.tgz", "integrity": "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jsonfile": { "version": "6.2.1", @@ -8656,6 +8910,7 @@ "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -8666,6 +8921,7 @@ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", @@ -8741,6 +8997,7 @@ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -8753,6 +9010,7 @@ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" @@ -8765,6 +9023,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" @@ -8776,6 +9035,7 @@ "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.9" } @@ -8795,6 +9055,7 @@ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -8832,6 +9093,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8853,6 +9115,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8874,6 +9137,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8895,6 +9159,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8916,6 +9181,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8937,6 +9203,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8958,6 +9225,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8979,6 +9247,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9000,6 +9269,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9021,6 +9291,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9042,6 +9313,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9076,6 +9348,7 @@ "integrity": "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "css-select": "^7.0.0", "cssom": "^0.5.0", @@ -9106,49 +9379,56 @@ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/log4js": { "version": "6.9.1", @@ -9156,6 +9436,7 @@ "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", @@ -9185,6 +9466,7 @@ "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "get-func-name": "^2.0.1" } @@ -9224,6 +9506,7 @@ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" } @@ -9234,6 +9517,7 @@ "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -9253,6 +9537,7 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -9263,6 +9548,7 @@ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -9283,6 +9569,7 @@ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -9307,6 +9594,7 @@ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "mime": "cli.js" }, @@ -9341,6 +9629,7 @@ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -9354,6 +9643,7 @@ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "brace-expansion": "^5.0.8" }, @@ -9370,6 +9660,7 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9413,6 +9704,7 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -9423,6 +9715,7 @@ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minipass": "^7.1.2" }, @@ -9462,14 +9755,16 @@ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/mustache": { "version": "4.2.0", @@ -9477,6 +9772,7 @@ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "mustache": "bin/mustache" } @@ -9487,6 +9783,7 @@ "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -9526,7 +9823,8 @@ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/negotiator": { "version": "0.6.3", @@ -9534,6 +9832,7 @@ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -9543,14 +9842,16 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/neverthrow": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-3.2.0.tgz", "integrity": "sha512-AINA32QbYO83L+3CBI6I5lH4LpBSlLwWteJ+uI25s4AQy6g/xz3RZuedmuNo91lLw2rY+AbPEPQdxd7mg1rXoQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next": { "version": "14.2.35", @@ -9636,6 +9937,7 @@ "integrity": "sha512-lLdwkeg2XUVPTHqWjPltTxH81zbQNbUWC+AEb9ni50KjxkUzAAx1HGtMT21sbntzfUGfIphhcklvs3imJeqDXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "semver": "^7.3.5" }, @@ -9648,7 +9950,8 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/node-domexception": { "version": "1.0.0", @@ -9696,6 +9999,7 @@ "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "http2-client": "^1.2.5" }, @@ -9718,7 +10022,8 @@ "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/node-readfiles": { "version": "0.2.0", @@ -9726,6 +10031,7 @@ "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es6-promise": "^3.2.1" } @@ -9763,6 +10069,7 @@ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "boolbase": "^2.0.0" }, @@ -9780,6 +10087,7 @@ "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "fast-safe-stringify": "^2.0.7" } @@ -9790,6 +10098,7 @@ "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@exodus/schemasafe": "^1.0.0-rc.2", "should": "^13.2.1", @@ -9805,6 +10114,7 @@ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 6" } @@ -9815,6 +10125,7 @@ "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "node-fetch-h2": "^2.3.0", "oas-kit-common": "^1.0.8", @@ -9835,6 +10146,7 @@ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 6" } @@ -9845,6 +10157,7 @@ "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "funding": { "url": "https://github.com/Mermade/oas-kit?sponsor=1" } @@ -9855,6 +10168,7 @@ "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "call-me-maybe": "^1.0.1", "oas-kit-common": "^1.0.8", @@ -9875,6 +10189,7 @@ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 6" } @@ -9905,6 +10220,7 @@ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -10554,6 +10870,7 @@ "integrity": "sha512-kxV247eRDdGhBKOZYBhAvgzm/PveXdUXvZR5bi0mDYWFeY6wHoDuCSjRw00khRR///1ZvgixfvS4CqSfezyptQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@microsoft/app-manifest": "^1.0.2", "adm-zip": "0.5.16", @@ -10575,6 +10892,7 @@ "integrity": "sha512-2eOdCCYJ5bhCe2p9KKETdg1UNshsKaT0lDU/jNopAg3t7zC1WxwvofTSO/+4Log5L4Re+wUdV8MqrQikZBa7+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@xmldom/xmldom": "^0.8.5", "commander": "^9.0.0", @@ -10590,6 +10908,7 @@ "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" } @@ -10600,6 +10919,7 @@ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -10610,6 +10930,7 @@ "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12.0" } @@ -10620,6 +10941,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -10633,6 +10955,7 @@ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -10648,6 +10971,7 @@ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-name": "1.1.3" } @@ -10657,7 +10981,8 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/office-addin-manifest/node_modules/commander": { "version": "13.1.0", @@ -10665,6 +10990,7 @@ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -10675,6 +11001,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -10690,6 +11017,7 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -10700,6 +11028,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -10710,6 +11039,7 @@ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -10731,6 +11061,7 @@ "integrity": "sha512-MUbcqOodtlOUNhLKjSdqeLBHFVd7Qh5pUODqmQe2VLBKk7PuRgl4dCk6bp2dU79vl6BcDyrRERpkyVnQg6zdFg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "commander": "^13.0.0" }, @@ -10744,6 +11075,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -10756,7 +11088,8 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/office-addin-manifest/node_modules/universalify": { "version": "0.1.2", @@ -10764,6 +11097,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -10778,6 +11112,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "peer": true, "bin": { "uuid": "dist-node/bin/uuid" } @@ -10787,7 +11122,8 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/office-addin-manifest/node_modules/whatwg-url": { "version": "5.0.0", @@ -10795,6 +11131,7 @@ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -10878,6 +11215,7 @@ "integrity": "sha512-WjthzxXY5gl78+jh91LczM9zg26zU8tc3afLi2bUcrMaPc6LaRaKJhw6NsyRB73kVDx76DGTe6Ws6sxufghOdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "adm-zip": "0.5.12", "commander": "^13.0.0", @@ -10897,6 +11235,7 @@ "integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -10907,6 +11246,7 @@ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -10917,6 +11257,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -10932,6 +11273,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -10942,6 +11284,7 @@ "integrity": "sha512-MUbcqOodtlOUNhLKjSdqeLBHFVd7Qh5pUODqmQe2VLBKk7PuRgl4dCk6bp2dU79vl6BcDyrRERpkyVnQg6zdFg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "commander": "^13.0.0" }, @@ -10955,6 +11298,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -10988,6 +11332,7 @@ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -11001,6 +11346,7 @@ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "wrappy": "1" } @@ -11011,6 +11357,7 @@ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -11036,7 +11383,8 @@ "resolved": "https://registry.npmjs.org/original-fs/-/original-fs-1.2.0.tgz", "integrity": "sha512-IGo+qFumpIV65oDchJrqL0BOk9kr82fObnTesNJt8t3YgP6vfqcmRs0ofPzg3D9PKMeBHt7lrg1k/6L+oFdS8g==", "dev": true, - "license": "Unlicense" + "license": "Unlicense", + "peer": true }, "node_modules/pako": { "version": "1.0.11", @@ -11063,6 +11411,7 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -11088,6 +11437,7 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -11105,6 +11455,7 @@ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" @@ -11121,7 +11472,8 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/pathval": { "version": "1.1.1", @@ -11129,6 +11481,7 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -11235,6 +11588,7 @@ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.20.0" } @@ -11268,7 +11622,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", @@ -11417,7 +11770,6 @@ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", "license": "Unlicense", - "peer": true, "engines": { "node": ">=12" }, @@ -11432,6 +11784,7 @@ "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20" }, @@ -11446,6 +11799,7 @@ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -11479,6 +11833,7 @@ "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", @@ -11490,7 +11845,8 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/proxy-addr": { "version": "2.0.8", @@ -11498,6 +11854,7 @@ "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -11516,6 +11873,7 @@ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -11526,6 +11884,7 @@ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -11547,6 +11906,7 @@ "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" @@ -11603,6 +11963,7 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -11613,6 +11974,7 @@ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", @@ -11629,6 +11991,7 @@ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "peer": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -11644,7 +12007,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11657,7 +12019,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11719,7 +12080,8 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/reftools": { "version": "1.1.9", @@ -11727,6 +12089,7 @@ "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "funding": { "url": "https://github.com/Mermade/oas-kit?sponsor=1" } @@ -11737,6 +12100,7 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11789,6 +12153,7 @@ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -11809,7 +12174,8 @@ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/rolldown": { "version": "1.2.16", @@ -11817,6 +12183,7 @@ "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/types": "=0.156.0", "@rolldown/pluginutils": "^1.0.0" @@ -11851,6 +12218,7 @@ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -11868,6 +12236,7 @@ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" @@ -11879,6 +12248,7 @@ "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -11892,6 +12262,7 @@ "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.12.0" } @@ -11994,6 +12365,7 @@ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -12007,6 +12379,7 @@ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -12032,6 +12405,7 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -12041,7 +12415,8 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/send/node_modules/mime": { "version": "1.6.0", @@ -12049,6 +12424,7 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "mime": "cli.js" }, @@ -12062,6 +12438,7 @@ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -12083,7 +12460,8 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/shebang-command": { "version": "2.0.0", @@ -12091,6 +12469,7 @@ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -12104,6 +12483,7 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -12114,6 +12494,7 @@ "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -12126,7 +12507,8 @@ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/should": { "version": "13.2.3", @@ -12134,6 +12516,7 @@ "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "should-equal": "^2.0.0", "should-format": "^3.0.3", @@ -12148,6 +12531,7 @@ "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "should-type": "^1.4.0" } @@ -12158,6 +12542,7 @@ "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "should-type": "^1.3.0", "should-type-adaptors": "^1.0.1" @@ -12168,7 +12553,8 @@ "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/should-type-adaptors": { "version": "1.1.0", @@ -12176,6 +12562,7 @@ "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "should-type": "^1.3.0", "should-util": "^1.0.0" @@ -12186,7 +12573,8 @@ "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/side-channel": { "version": "1.1.1", @@ -12194,6 +12582,7 @@ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", @@ -12214,6 +12603,7 @@ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" @@ -12231,6 +12621,7 @@ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12250,6 +12641,7 @@ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12296,7 +12688,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/simple-get": { "version": "4.0.1", @@ -12318,6 +12711,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -12368,7 +12762,8 @@ "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/statuses": { "version": "2.0.2", @@ -12376,6 +12771,7 @@ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -12408,6 +12804,7 @@ "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", @@ -12423,6 +12820,7 @@ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", @@ -12438,6 +12836,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -12448,6 +12847,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -12525,6 +12925,7 @@ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12596,6 +12997,7 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -12631,6 +13033,7 @@ "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "call-me-maybe": "^1.0.1", "node-fetch": "^2.6.1", @@ -12659,6 +13062,7 @@ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -12679,14 +13083,16 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/swagger2openapi/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/swagger2openapi/node_modules/whatwg-url": { "version": "5.0.0", @@ -12694,6 +13100,7 @@ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -12705,6 +13112,7 @@ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 6" } @@ -12760,6 +13168,7 @@ "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -12777,6 +13186,7 @@ "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -12789,7 +13199,8 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/tar-stream": { "version": "2.2.0", @@ -12797,6 +13208,7 @@ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -12814,6 +13226,7 @@ "integrity": "sha512-UDdUF9kV2hYdlv+7AgqP2kXarVSUhjK7tg1BUflIRGEgND0/QoNpN64rcEuhEcM8AIbW65yrCopJWqRhLZ3m8w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "axios": "^1.6.1" } @@ -12824,6 +13237,7 @@ "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -12842,7 +13256,8 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/thenify": { "version": "3.3.1", @@ -12937,7 +13352,6 @@ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12971,6 +13385,7 @@ "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=14.14" } @@ -12994,6 +13409,7 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.6" } @@ -13030,6 +13446,7 @@ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "tree-kill": "cli.js" } @@ -13072,6 +13489,7 @@ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -13085,6 +13503,7 @@ "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -13095,6 +13514,7 @@ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -13108,6 +13528,7 @@ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -13121,7 +13542,8 @@ "resolved": "https://registry.npmjs.org/typedi/-/typedi-0.10.0.tgz", "integrity": "sha512-v3UJF8xm68BBj6AF4oQML3ikrfK2c9EmZUyLOfShpJuItAqVBHWP/KtpGinkSsIiP6EZyyb6Z3NXyW9dgS9X1w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/typescript": { "version": "5.9.3", @@ -13144,6 +13566,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -13156,14 +13579,16 @@ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/underscore": { "version": "1.13.8", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/undici": { "version": "7.30.1", @@ -13198,6 +13623,7 @@ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -13254,6 +13680,7 @@ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -13268,6 +13695,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "peer": true, "bin": { "uuid": "dist/esm/bin/uuid" } @@ -13278,6 +13706,7 @@ "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.10" } @@ -13288,6 +13717,7 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -13298,6 +13728,7 @@ "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", @@ -13376,6 +13807,7 @@ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13485,6 +13917,7 @@ "integrity": "sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8.0.0 || >=10.0.0" } @@ -13553,6 +13986,7 @@ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -13588,7 +14022,8 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/wrap-ansi": { "version": "6.2.0", @@ -13610,7 +14045,8 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/ws": { "version": "7.5.13", @@ -13640,6 +14076,7 @@ "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.2.0" @@ -13657,6 +14094,7 @@ "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-inside-container": "^1.0.0" }, @@ -13736,6 +14174,7 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=10" } @@ -13746,6 +14185,7 @@ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "engines": { "node": ">=18" } @@ -13756,6 +14196,7 @@ "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -13772,6 +14213,7 @@ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -13791,6 +14233,7 @@ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -13825,6 +14268,7 @@ "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "dev": true, "license": "ISC", + "peer": true, "peerDependencies": { "zod": "^3.25.28 || ^4" } diff --git a/public/wordaddin/icons/icon-16.png b/public/wordaddin/icons/icon-16.png index b650979..082ea38 100644 Binary files a/public/wordaddin/icons/icon-16.png and b/public/wordaddin/icons/icon-16.png differ diff --git a/public/wordaddin/icons/icon-32.png b/public/wordaddin/icons/icon-32.png index dcf56db..1b1c7d5 100644 Binary files a/public/wordaddin/icons/icon-32.png and b/public/wordaddin/icons/icon-32.png differ diff --git a/public/wordaddin/icons/icon-64.png b/public/wordaddin/icons/icon-64.png index 41051fc..9d03756 100644 Binary files a/public/wordaddin/icons/icon-64.png and b/public/wordaddin/icons/icon-64.png differ diff --git a/public/wordaddin/icons/icon-80.png b/public/wordaddin/icons/icon-80.png index 5e63769..f6dc3af 100644 Binary files a/public/wordaddin/icons/icon-80.png and b/public/wordaddin/icons/icon-80.png differ diff --git a/public/wordaddin/manifest.template.xml b/public/wordaddin/manifest.template.xml index c637200..ebe0cfc 100644 --- a/public/wordaddin/manifest.template.xml +++ b/public/wordaddin/manifest.template.xml @@ -5,7 +5,7 @@ --> 4a1e184a-ab31-4fa4-b3c7-9a2283051c3f - 1.0.0.2 + 1.0.0.4 e-Court Documents Versioning en-US @@ -67,8 +67,14 @@ + - ButtonId1 + Office.AutoShowTaskpaneWithDocument diff --git a/tests/app/api/documents/token.test.ts b/tests/app/api/documents/token.test.ts new file mode 100644 index 0000000..803a271 --- /dev/null +++ b/tests/app/api/documents/token.test.ts @@ -0,0 +1,215 @@ +// POST /api/documents/token — contract ทางเข้า ticket ฉบับ 2026-11-04 + regression ทางเข้า x-api-key +// route pure ล้วนหลังถอด hasVersionByUser (ไม่ import @/db/minio อีกต่อไป) จึง unit test +// ได้โดยไม่ต้องตั้ง DATABASE_URL/MINIO_* — เคส 403 ผ่านโดยไม่มี DB เลย = พิสูจน์ว่า route +// ไม่แตะ DB (เลิก "ตรวจเคยมี version" ของ requirement ขั้น 7 ฉบับ 2026-10-28) +import { createHmac } from "node:crypto"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { POST } from "@/app/api/documents/token/route"; + +/** secret ตาม chain ของ addinTicketSecret — ลบ env ทั้งสองตัวใน setup เพื่อใช้ค่า default */ +const SECRET_DEFAULT = "super_secret"; +const TOKEN_SECRET = "unit-test-token-secret"; +const API_KEY = "unit-test-api-key"; + +const SAVED_ENV: Record = {}; + +function mintTicket( + exp: number, + overrides?: { fid?: string; uid?: string; uname?: string; tid?: string }, +): { ticketData: string; ticketHmac: string } { + const ticket = { + exp, + fid: overrides?.fid ?? "F1", + ...(overrides?.tid !== undefined ? { tid: overrides.tid } : { tid: "T12" }), + uid: overrides?.uid ?? "oom", + uname: overrides?.uname ?? "สรวิชญ์", + }; + const ticketData = JSON.stringify(ticket); + const ticketHmac = createHmac("sha256", SECRET_DEFAULT).update(ticketData, "utf8").digest("hex"); + return { ticketData, ticketHmac }; +} + +function futureExp(): number { + return Math.floor(Date.now() / 1000) + 3_600; +} + +function makeRequest(body: BodyInit, headers?: Record): Request { + return new Request("http://localhost/docedit/api/documents/token", { + method: "POST", + headers: { "content-type": "application/json", ...headers }, + body, + }); +} + +function ticketBody(ticketData: string, ticketHmac: string): string { + return JSON.stringify({ ticket_data: ticketData, HmacSha256: ticketHmac }); +} + +/** decode JWT payload (base64url) โดยไม่ตรวจ signature — ใช้ assert claims/exp เท่านั้น */ +function jwtPayload(token: string): Record { + const payload = token.split(".")[1]; + if (!payload) throw new Error("token ไม่มี payload"); + return JSON.parse( + Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"), + ) as Record; +} + +beforeEach(() => { + for (const key of [ + "ADDIN_TICKET_SECRET", + "SECRET", + "DOCVER_API_KEY", + "DOCVER_TOKEN_SECRET", + "DOCVER_TOKEN_TTL_SECONDS", + ]) { + SAVED_ENV[key] = process.env[key]; + } + process.env.DOCVER_TOKEN_SECRET = TOKEN_SECRET; + delete process.env.ADDIN_TICKET_SECRET; // ใช้ default "super_secret" ของ addinTicketSecret + delete process.env.SECRET; + delete process.env.DOCVER_TOKEN_TTL_SECONDS; +}); + +afterEach(() => { + for (const [key, value] of Object.entries(SAVED_ENV)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } +}); + +describe("POST /api/documents/token — ทางเข้า ticket (contract 2026-11-04)", () => { + it("ticket ถูกทั้งลายเซ็น+อายุ → 200 + JWT claims จาก ticket + expires_in 604800 (default 7 วัน)", async () => { + const { ticketData, ticketHmac } = mintTicket(futureExp(), { fid: "F9", uid: "u1", uname: "นายทดสอบ" }); + const response = await POST(makeRequest(ticketBody(ticketData, ticketHmac))); + + expect(response.status).toBe(200); + const data = (await response.json()) as { access_token: string; token_type: string; expires_in: number }; + expect(data.token_type).toBe("Bearer"); + expect(data.expires_in).toBe(604_800); + + const payload = jwtPayload(data.access_token); + expect(payload.userid).toBe("u1"); + expect(payload.username).toBe("นายทดสอบ"); + expect(payload.fid).toBe("F9"); + expect(payload.template).toBe("T12"); + expect(payload.purpose).toBe("docver"); + expect((payload.exp as number) - (payload.iat as number)).toBe(604_800); + }); + + it("body ไม่ใช่ JSON → 400", async () => { + const response = await POST(makeRequest("not-json{")); + expect(response.status).toBe(400); + }); + + it("ไม่ส่ง ticket มาเลย / ค่าว่าง → 400 (ย้ายจาก 401 ตาม contract ฟอร์แม็ตพัง)", async () => { + for (const body of ["{}", JSON.stringify({ ticket_data: "" , HmacSha256: "ab".repeat(32) })]) { + const response = await POST(makeRequest(body)); + expect(response.status, body).toBe(400); + } + }); + + it("field นอก allowlist / ผิดชนิด / ยาวเกิน cap → 400", async () => { + for (const body of [ + JSON.stringify({ ticket_data: "x", HmacSha256: "y", oops: 1 }), + JSON.stringify({ ticket_data: 42, HmacSha256: "y" }), + JSON.stringify({ ticket_data: "x".repeat(2049), HmacSha256: "y" }), + ]) { + const response = await POST(makeRequest(body)); + expect(response.status, body).toBe(400); + } + }); + + it("ลายเซ็นผิด (แก้ ticket_data / digest เพี้ยน / secret ของ server ต่าง) → 401 ข้อความเดียวกันทุกเคส", async () => { + const { ticketData, ticketHmac } = mintTicket(futureExp()); + const tampered = ticketData.replace("F1", "F2"); // แก้ 1 ตัว — HMAC ไม่ตรง + + const cases: { label: string; body: string; secretEnv?: [string, string] }[] = [ + { label: "แก้ ticket_data", body: ticketBody(tampered, ticketHmac) }, + { label: "digest ไม่ใช่ hex", body: ticketBody(ticketData, "zz".repeat(32)) }, + { + label: "secret ฝั่ง server ต่าง", + body: ticketBody(ticketData, ticketHmac), + secretEnv: ["ADDIN_TICKET_SECRET", "different-secret"], + }, + ]; + const messages = new Set(); + for (const { label, body, secretEnv } of cases) { + if (secretEnv) process.env[secretEnv[0]] = secretEnv[1]; + const response = await POST(makeRequest(body)); + expect(response.status, label).toBe(401); + messages.add((await response.json()).error); + if (secretEnv) delete process.env[secretEnv[0]]; + } + expect(messages.size).toBe(1); // ข้อความเดียว กัน oracle + expect(Array.from(messages)[0]).toBe("ticket ไม่ถูกต้อง"); + }); + + it("ลายเซ็นถูกแต่ exp อดีต → 403 \"ticket ของ e-Court หมดอายุ\" (ไม่ตรวจ DB — route ไม่มี DB)", async () => { + const { ticketData, ticketHmac } = mintTicket(Math.floor(Date.now() / 1000) - 60); + const response = await POST(makeRequest(ticketBody(ticketData, ticketHmac))); + + expect(response.status).toBe(403); + expect((await response.json()).error).toBe("ticket ของ e-Court หมดอายุ"); + }); + + it("HMAC ถูกแต่ ticket_data ผิดกติกา (field หลุด allowlist) → 401", async () => { + const invalid = JSON.stringify({ exp: futureExp(), fid: "f", uid: "u", uname: "n", oops: 1 }); + const hmac = createHmac("sha256", SECRET_DEFAULT).update(invalid, "utf8").digest("hex"); + const response = await POST(makeRequest(ticketBody(invalid, hmac))); + expect(response.status).toBe(401); + }); + + it("DOCVER_TOKEN_SECRET ไม่ตั้ง → 500 (ความผิด config ฝั่ง server ไม่ใช่ของผู้เรียก)", async () => { + delete process.env.DOCVER_TOKEN_SECRET; + const { ticketData, ticketHmac } = mintTicket(futureExp()); + const response = await POST(makeRequest(ticketBody(ticketData, ticketHmac))); + expect(response.status).toBe(500); + }); + + it("DOCVER_TOKEN_TTL_SECONDS=120 → expires_in 120 (env override default)", async () => { + process.env.DOCVER_TOKEN_TTL_SECONDS = "120"; + const { ticketData, ticketHmac } = mintTicket(futureExp()); + const response = await POST(makeRequest(ticketBody(ticketData, ticketHmac))); + expect(response.status).toBe(200); + expect(((await response.json()) as { expires_in: number }).expires_in).toBe(120); + }); +}); + +describe("POST /api/documents/token — ทางเข้า x-api-key (regression ไม่เปลี่ยน)", () => { + it("key ถูก + body claims → 200 ได้ token อายุ default ใหม่ 604800", async () => { + process.env.DOCVER_API_KEY = API_KEY; + const response = await POST( + makeRequest(JSON.stringify({ userid: "u1", username: "นายทดสอบ", fid: "F1" }), { + "x-api-key": API_KEY, + }), + ); + expect(response.status).toBe(200); + const data = (await response.json()) as { access_token: string; expires_in: number }; + expect(data.expires_in).toBe(604_800); + const payload = jwtPayload(data.access_token); + expect(payload.userid).toBe("u1"); + expect(payload.fid).toBe("F1"); + expect(payload.purpose).toBe("docver"); + }); + + it("key ผิด → 401", async () => { + process.env.DOCVER_API_KEY = API_KEY; + const response = await POST( + makeRequest(JSON.stringify({ userid: "u1", username: "n", fid: "F1" }), { + "x-api-key": "wrong-key", + }), + ); + expect(response.status).toBe(401); + }); + + it("มี header x-api-key แต่ env ไม่ตั้ง → 401 fail closed", async () => { + delete process.env.DOCVER_API_KEY; + const response = await POST( + makeRequest(JSON.stringify({ userid: "u1", username: "n", fid: "F1" }), { + "x-api-key": API_KEY, + }), + ); + expect(response.status).toBe(401); + }); +}); diff --git a/tests/lib/addin-ticket.test.ts b/tests/lib/addin-ticket.test.ts index d5003ff..9986e89 100644 --- a/tests/lib/addin-ticket.test.ts +++ b/tests/lib/addin-ticket.test.ts @@ -75,8 +75,7 @@ describe("verifyAddinTicket", () => { }); // verifyAddinTicketHmac = ตัวเดียวกันแต่ไม่ตรวจ exp — route ใช้ตรวจ HMAC ก่อน -// แล้วตัดสิน "ticket หมดอายุ + เอกสารนี้เคยทำงานโดย uid เดียวกัน" จาก DB เอง -// (requirement ขั้น 7 — แก้บั๊ก 2026-10-28) +// แล้วตัดสิน exp เองภายหลัง (ลายเซ็นถูกแต่หมดอายุ = 403 — contract 2026-11-04) describe("verifyAddinTicketHmac", () => { it("exp อดีต + HMAC ถูก = ผ่าน (ได้ ticket ครบ) — ต่างจาก verifyAddinTicket ที่ไม่ผ่าน", () => { const expired = JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n" }); @@ -122,7 +121,7 @@ describe("parseTicketTokenBody", () => { } }); - it("ขาด field (ไม่ส่ง/ว่าง/null) = missing (→ 401 กัน oracle)", () => { + it("ขาด field (ไม่ส่ง/ว่าง/null) = missing (→ 400 — ฟอร์แม็ตพัง, contract 2026-11-04)", () => { for (const raw of [{}, { ticket_data: "" }, { HmacSha256: "y" }, { ticket_data: null, HmacSha256: "y" }]) { const result = parseTicketTokenBody(raw); expect(result.ok, JSON.stringify(raw)).toBe(false); diff --git a/tests/lib/openapi.test.ts b/tests/lib/openapi.test.ts new file mode 100644 index 0000000..bd9610e --- /dev/null +++ b/tests/lib/openapi.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; + +import { openApiSpec } from "@/lib/openapi"; + +// spec ถูก inline เป็น JSON ลงหน้า Swagger UI ตรง ๆ (app/api/swagger/route.ts) — +// $ref ที่ชี้หาย (แก้ชื่อ schema ใน lib/openapi.ts แล้วลืมแก้อีกฝั่ง) จะไม่มี error +// ตอน build เพราะเป็นแค่ string กลาง object literal ที่ serialize ได้ปกติ — จับได้ที่นี่ก่อน +// เห็นเป็นช่องว่าง ๆ บนหน้า /api/swagger + +function collectRefs(node: unknown, out: string[] = []): string[] { + if (Array.isArray(node)) { + for (const item of node) collectRefs(item, out); + } else if (node !== null && typeof node === "object") { + for (const [key, value] of Object.entries(node)) { + if (key === "$ref" && typeof value === "string") out.push(value); + else collectRefs(value, out); + } + } + return out; +} + +/** แกะ JSON pointer (#/a/b/c) — null = ชี้ไปที่ไม่มีจริง */ +function resolveRef(spec: object, ref: string): unknown { + if (!ref.startsWith("#/")) return null; + let node: unknown = spec; + for (const part of ref.slice(2).split("/")) { + if (node === null || typeof node !== "object") return null; + const key = part.replace(/~1/g, "/").replace(/~0/g, "~"); + node = (node as Record)[key]; + if (node === undefined) return null; + } + return node; +} + +describe("openApiSpec", () => { + it("ทุก $ref ใน spec ชี้ถึงจุดที่มีจริง", () => { + const refs = collectRefs(openApiSpec); + expect(refs.length).toBeGreaterThan(0); + const broken = refs.filter((ref) => resolveRef(openApiSpec, ref) === null); + expect(broken).toEqual([]); + }); + + it("กลุ่ม auth: path สั้น /api/auth/* ครบทั้งคู่ และ path เดิมที่เป็นต้นทางยังอยู่", () => { + const paths = openApiSpec.paths as Record; + expect(paths["/api/auth/token"]).toBeDefined(); + expect(paths["/api/auth/session"]).toBeDefined(); + // path เดิม (ที่ /api/auth/* alias มา) ต้องยัง document อยู่ + expect(paths["/api/documents/token"]).toBeDefined(); + expect(paths["/api/note/auth/token"]).toBeDefined(); + }); +}); diff --git a/tests/utils/docver-ticket.test.ts b/tests/utils/docver-ticket.test.ts index 7d09612..3d1bb78 100644 --- a/tests/utils/docver-ticket.test.ts +++ b/tests/utils/docver-ticket.test.ts @@ -5,7 +5,6 @@ import { normalizeHexDigest, parseDocverTicket, parseTicketDataParam, - timingSafeEqualHex, } from "@/utils/docver-ticket"; // PLAN-ADDIN-AUTH.md §4 — pure ล้วนไม่แตะ env/DB อะไรเลย @@ -116,15 +115,6 @@ describe("normalizeHexDigest", () => { }); }); -describe("timingSafeEqualHex", () => { - it("ค่าเท่ากัน = true ต่างกัน = false · ความยาวไม่เท่า = false ทันที", () => { - const a = "ab".repeat(32); - expect(timingSafeEqualHex(a, a)).toBe(true); - expect(timingSafeEqualHex(a, "cd".repeat(32))).toBe(false); - expect(timingSafeEqualHex(a, "ab")).toBe(false); - }); -}); - // parseTicketDataParam — query param `ticket_data` ของ GET /api/documents/latest: // ticket ก่อนเติม exp (server เติมตอน stamp) แต่บังคับ fid/uid/uname เพราะ // parseDocverTicket บังคับทั้งสามตอน add-in redeem — ขาด = ไฟล์ที่คืนไปเปิดไม่ได้ diff --git a/tests/wordaddin/metadataService.test.ts b/tests/wordaddin/metadataService.test.ts index 080c3a5..ebbddd7 100644 --- a/tests/wordaddin/metadataService.test.ts +++ b/tests/wordaddin/metadataService.test.ts @@ -93,3 +93,103 @@ describe("readContentControlFields", () => { expect(await readContentControlFields()).toEqual({}); }); }); + +// ─── enableAutoOpenTaskpane (auto-open 2026-11-05) ────────────────────────── +// function มี module-level memo (กัน double-call ของ detect) — แยกเคสด้วย +// vi.resetModules + dynamic import ให้ทุกเคสได้ module ใหม่ ไม่ให้ memo รั่ยข้ามเคส +interface StubbedSettings { + get: ReturnType; + set: ReturnType; + saveAsync: ReturnType; +} + +/** stub Office.context.document.settings + Word.run ของ saveDocumentVerified */ +function stubAutoOpen(options?: { existing?: boolean; saveOk?: boolean; nativeSaveThrows?: boolean }): { + settings: StubbedSettings; + saveFn: ReturnType; +} { + const settings = { + get: vi.fn(() => (options?.existing === true ? true : undefined)), + set: vi.fn(), + saveAsync: vi.fn((callback: (result: { status: string }) => void) => + callback({ status: options?.saveOk === false ? "failed" : "succeeded" }), + ), + }; + const saveFn = vi.fn(() => { + if (options?.nativeSaveThrows) throw new Error("read-only"); + }); + vi.stubGlobal("Office", { + AsyncResultStatus: { Succeeded: "succeeded", Failed: "failed" }, + context: { document: { settings } }, + }); + vi.stubGlobal("Word", { + run: async (batch: (context: unknown) => Promise) => + batch({ + document: { save: saveFn, load: vi.fn(), saved: true }, + sync: async () => undefined, + }), + }); + return { settings, saveFn }; +} + +async function importFreshMetadata() { + vi.resetModules(); + return await import("@/wordaddin/services/metadataService"); +} + +describe("enableAutoOpenTaskpane", () => { + it("ยังไม่มี setting → set(true) + saveAsync + native save ครบ คืน true", async () => { + const { settings, saveFn } = stubAutoOpen(); + const metadata = await importFreshMetadata(); + + const ok = await metadata.enableAutoOpenTaskpane(); + + expect(ok).toBe(true); + expect(metadata.AUTO_OPEN_SETTING).toBe("Office.AutoShowTaskpaneWithDocument"); + expect(settings.set).toHaveBeenCalledWith("Office.AutoShowTaskpaneWithDocument", true); + expect(settings.saveAsync).toHaveBeenCalledTimes(1); + expect(saveFn).toHaveBeenCalled(); // saveDocumentVerified ตามหลัง saveAsync + }); + + it("มี setting = true อยู่แล้ว → no-op idempotent ไม่ set ไม่ saveAsync", async () => { + const { settings, saveFn } = stubAutoOpen({ existing: true }); + const metadata = await importFreshMetadata(); + + expect(await metadata.enableAutoOpenTaskpane()).toBe(true); + expect(settings.set).not.toHaveBeenCalled(); + expect(settings.saveAsync).not.toHaveBeenCalled(); + expect(saveFn).not.toHaveBeenCalled(); + }); + + it("นอก host Word (ไม่มี Office global) → คืน false ไม่ throw", async () => { + const metadata = await importFreshMetadata(); // ไม่ stub อะไรเลย + expect(await metadata.enableAutoOpenTaskpane()).toBe(false); + }); + + it("settings.saveAsync ล้ม → คืน false ไม่ throw", async () => { + stubAutoOpen({ saveOk: false }); + const metadata = await importFreshMetadata(); + expect(await metadata.enableAutoOpenTaskpane()).toBe(false); + }); + + it("native save ล้ม (ไฟล์ read-only) → ยังคืน true (setting เข้า memory doc แล้ว) + warn ไม่ throw", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined); + stubAutoOpen({ nativeSaveThrows: true }); + const metadata = await importFreshMetadata(); + + expect(await metadata.enableAutoOpenTaskpane()).toBe(true); + expect(warn).toHaveBeenCalled(); + }); + + it("memo: ล้มครั้งแรกแล้วล้าง — call ถัดไปยิงใหม่ได้ (สำเร็จในครั้งที่สอง)", async () => { + const { settings } = stubAutoOpen({ saveOk: false }); + const metadata = await importFreshMetadata(); + + expect(await metadata.enableAutoOpenTaskpane()).toBe(false); + settings.saveAsync.mockImplementation( + (callback: (result: { status: string }) => void) => callback({ status: "succeeded" }), + ); + expect(await metadata.enableAutoOpenTaskpane()).toBe(true); + expect(settings.saveAsync).toHaveBeenCalledTimes(2); + }); +}); diff --git a/tests/wordaddin/ticket-auth-flow.test.ts b/tests/wordaddin/ticket-auth-flow.test.ts index 0ab6938..b9e0bc0 100644 --- a/tests/wordaddin/ticket-auth-flow.test.ts +++ b/tests/wordaddin/ticket-auth-flow.test.ts @@ -1,15 +1,16 @@ // @vitest-environment jsdom -// ticket auth ของ add-in (PLAN-ADDIN-AUTH.md §5) — คุมพฤติกรรม service layer: -// happy-path ขอ token ครั้งเดียวต่อ fid, เปิดเอกสารเดิมซ้ำใช้ token เดิมไม่ยิง /token ซ้ำ, -// memory fallback ของ session, ทุก fail path ต้องคืน result ไม่ใช่ throw และ -// ticket หมดอายุ + เอกสารที่เคยทำงาน (requirement ขั้น 7 — บั๊ก 2026-10-28: -// client ยิงขอ token เสมอ server เป็นคนตัดสินเรื่อง "เคยทำงาน" จาก DB) -// (jsdom ไม่มี crypto.subtle — ฉีด webcrypto ของ node แทน secure context ของ webview จริง) -import { createHmac, webcrypto } from "node:crypto"; +// ticket auth ของ add-in (PLAN-ADDIN-AUTH.md §5 — contract 2026-11-04) — คุมพฤติกรรม +// service layer: pre-check ฝั่ง client (missing/ฟอร์แม็ต/หมดอายุ+ไม่มี session ไม่ยิง API), +// ยิง /api/auth/token ทุกครั้งที่เปิดเอกสารแล้วแยกทางตาม status (200/400/401/403), +// memory fallback ของ session, ทุก fail path ต้องคืน result ไม่ใช่ throw +// (ไม่มีการตรวจ HMAC ฝั่ง client อีกต่อไป — PLAN-SSO.md D13: secret เป็นของ server +// อย่างเดียว "ไฟล์ถูกแก้" ตรวจจาก 400/401 ของ server) +import { createHmac } from "node:crypto"; import { afterEach, describe, expect, it, vi } from "vitest"; import * as authService from "@/wordaddin/services/authService"; import * as ticketService from "@/wordaddin/services/ticketService"; +import { withBasePath } from "@/utils/base-path"; // จับ error ทุกทาง (window error / unhandled rejection) — flow ต้องไม่ปล่อยอะไรหลุดเลย const errors: unknown[] = []; @@ -18,110 +19,25 @@ function trapErrors() { window.addEventListener("unhandledrejection", (event) => errors.push(event.reason)); } -function makeTicket(expSec?: number): { ticketData: string; ticketHmac: string } { +const FID = "DBG_FID_001"; + +function makeTicket(overrides?: { exp?: number; fid?: string; uid?: string; uname?: string }): { + ticketData: string; + ticketHmac: string; +} { const ticket = { - exp: expSec ?? Math.floor(Date.now() / 1000) + 300, - fid: "DBG_FID_001", + exp: overrides?.exp ?? Math.floor(Date.now() / 1000) + 300, + fid: overrides?.fid ?? FID, tid: "T12", - uid: "oom", - uname: "สรวิชญ์", + uid: overrides?.uid ?? "oom", + uname: overrides?.uname ?? "สรวิชญ์", }; const ticketData = JSON.stringify(ticket); + // HMAC ยัง mint จริงตามตัว stamp (server ใน test อื่นจะเทียบค่านี้ — ฝั่ง client ไม่แตะแล้ว) const ticketHmac = createHmac("sha256", "super_secret").update(ticketData, "utf8").digest("hex"); return { ticketData, ticketHmac }; } -afterEach(() => { - window.localStorage.clear(); - // memory fallback ของ session เป็น module state — เคลียร์ด้วยไม่งั้นค้างข้ามเคส - ticketService.clearSession(); - authService.signInForDemo(); // รีเซ็ต state ของ authService (username/fid/ticket) - vi.restoreAllMocks(); - vi.unstubAllGlobals(); - errors.length = 0; -}); - -describe("signInWithTicket (service layer)", () => { - it("B: signInWithTicket happy-path (มี crypto.subtle + fetch ตอบ 200) ไม่ต้อง throw", async () => { - trapErrors(); - // jsdom ไม่มี crypto.subtle — ใส่ webcrypto ของ node แทน (secure context ของ webview จริงมีอยู่แล้ว) - vi.stubGlobal("crypto", webcrypto); - - const { ticketData, ticketHmac } = makeTicket(); - const fetchMock = vi.fn( - async () => - new Response( - JSON.stringify({ - access_token: [ - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", - Buffer.from( - JSON.stringify({ userid: "oom", fid: "DBG_FID_001", exp: Math.floor(Date.now() / 1000) + 3600 }), - ).toString("base64url"), - "sig", - ].join("."), - token_type: "Bearer", - expires_in: 3600, - }), - { status: 200, headers: { "content-type": "application/json" } }, - ), - ); - vi.stubGlobal("fetch", fetchMock); - - const first = await authService.signInWithTicket({ ticketData, ticketHmac }); - expect(first.ok, JSON.stringify(first)).toBe(true); - - // เปิดเอกสารเดิมซ้ำ — ต้องไม่ยิง /token ซ้ำ (ใช้ token เดิมจาก localStorage) - await authService.signInWithTicket({ ticketData, ticketHmac }); - expect(fetchMock).toHaveBeenCalledTimes(1); - - expect(errors).toEqual([]); - expect(window.localStorage.getItem("wordaddin-auth")).toBeTruthy(); - }); - - it("C: signInWithTicket แบบไม่มี crypto.subtle (webview เก่า) ต้องจบเป็น no-access ไม่ใช่ throw", async () => { - trapErrors(); - const noSubtle = { getRandomValues: () => new Uint8Array(0) }; - vi.stubGlobal("crypto", noSubtle); // ไม่มี .subtle เลย - const { ticketData, ticketHmac } = makeTicket(); - const result = await authService.signInWithTicket({ ticketData, ticketHmac }); - expect(result.ok).toBe(false); - expect(errors).toEqual([]); - }); - - it("D: ticket ผิดทุกแบบ (ขาด/hex เพี้ยน/parse ไม่ผ่าน/หมดอายุ/fetch 401) ต้องคืน result ไม่ใช่ throw", async () => { - trapErrors(); - vi.stubGlobal("crypto", webcrypto); - vi.stubGlobal( - "fetch", - vi.fn(async () => new Response(JSON.stringify({ error: "no" }), { status: 401 })), - ); - const { ticketData, ticketHmac } = makeTicket(); - const bad = [ - { ticketData: undefined, ticketHmac: undefined }, - { ticketData: "not-json", ticketHmac }, - { ticketData, ticketHmac: "zz".repeat(32) }, - { - ticketData: JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n" }), // หมดอายุ + HMAC ถูก - ticketHmac: createHmac("sha256", "super_secret") - .update(JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n" }), "utf8") - .digest("hex"), - }, - { ticketData, ticketHmac }, // ถูก แต่ server 401 → token-request-failed - ] satisfies { ticketData?: string; ticketHmac?: string }[]; - for (const input of bad) { - const result = await authService.signInWithTicket(input); - expect(result.ok, JSON.stringify(input)).toBe(false); - if (!result.ok) expect(typeof result.message).toBe("string"); - } - expect(errors).toEqual([]); - }); -}); - -// ─── ticket หมดอายุ + เอกสารที่เคยทำงาน (requirement ขั้น 7 — บั๊ก 2026-10-28) ── -// เกณฑ์: uid ตรงกัน + เคยสร้าง token ของ fid เดิม → ไม่ต้องสน expire ของ ticket -// ฝั่ง client ยิงขอ token เสมอแล้วให้ server ตัดสิน (200 = เคยทำงาน / 401 = ไม่เคย) -const FID = "DBG_FID_001"; - function tokenResponse(token: string): Response { return new Response( JSON.stringify({ @@ -133,95 +49,325 @@ function tokenResponse(token: string): Response { ); } -function seedSession(token: string, tokenExpSec: number): void { +function errorResponse(status: number): Response { + return new Response(JSON.stringify({ error: "stub" }), { + status, + headers: { "content-type": "application/json" }, + }); +} + +function seedSession(token: string, tokenExpSec: number, userId = "oom"): void { ticketService.saveSession({ - userId: "oom", - userName: "สรวิชญ์", + userId, + userName: "ชื่อเก่า", tokens: { [FID]: { token, exp: tokenExpSec } }, }); } -describe("ticket หมดอายุ + เอกสารที่เคยทำงาน", () => { - it("session เดิม + token ยังไม่หมด → ผ่านเลย ไม่ยิง /token", async () => { +afterEach(() => { + window.localStorage.clear(); + // memory fallback ของ session เป็น module state — เคลียร์ด้วยไม่งั้นค้างข้ามเคส + ticketService.clearSession(); + authService.signInForDemo(); // รีเซ็ต state ของ authService (username/fid/ticket) + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + errors.length = 0; +}); + +describe("signInWithTicket — pre-check ฝั่ง client (เตะโดยไม่ยิง API)", () => { + it("ไม่มี ticket / ยาวเกิน 255 → reason missing", async () => { trapErrors(); - vi.stubGlobal("crypto", webcrypto); const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); - seedSession("cached-token", Math.floor(Date.now() / 1000) + 1800); + const { ticketData, ticketHmac } = makeTicket(); - const { ticketData, ticketHmac } = makeTicket(1); // ticket หมดอายุตั้งแต่ 1970 - const result = await authService.signInWithTicket({ ticketData, ticketHmac }); - - expect(result.ok, JSON.stringify(result)).toBe(true); + const bad = [ + { ticketData: undefined, ticketHmac: undefined }, + { ticketData: "", ticketHmac }, + { ticketData: "x".repeat(256), ticketHmac }, + { ticketData, ticketHmac: "x".repeat(256) }, + ] satisfies { ticketData?: string; ticketHmac?: string }[]; + for (const input of bad) { + const result = await authService.signInWithTicket(input); + expect(result.ok, JSON.stringify(input)).toBe(false); + if (!result.ok) expect(result.reason).toBe("missing"); + } expect(fetchMock).not.toHaveBeenCalled(); expect(errors).toEqual([]); }); - it("session เดิม + token หมดด้วย → ยิงขอใหม่ (server ตัดสิน) ได้ token ใหม่", async () => { + it("parse ฟอร์แม็ตไม่ผ่าน → reason tampered ไม่ยิง fetch", async () => { trapErrors(); - vi.stubGlobal("crypto", webcrypto); - const fetchMock = vi.fn(async () => tokenResponse("token-fresh")); + const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); - seedSession("token-stale", Math.floor(Date.now() / 1000) - 60); - const { ticketData, ticketHmac } = makeTicket(1); - const result = await authService.signInWithTicket({ ticketData, ticketHmac }); - - expect(result.ok, JSON.stringify(result)).toBe(true); - expect(fetchMock).toHaveBeenCalledTimes(1); - const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}"); - expect(stored.tokens[FID].token).toBe("token-fresh"); + const result = await authService.signInWithTicket({ ticketData: "not-json", ticketHmac: "aa".repeat(32) }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toBe("tampered"); + expect(fetchMock).not.toHaveBeenCalled(); expect(errors).toEqual([]); }); - it("ไม่มี session (localStorage หาย) + server 401 (ยังไม่เคยทำงาน) → expired ไม่ใช่ crash", async () => { + it("ticket หมดอายุ + ไม่มี session → reason expired ไม่ยิง fetch", async () => { trapErrors(); - vi.stubGlobal("crypto", webcrypto); - vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ error: "no" }), { status: 401 }))); + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); - const { ticketData, ticketHmac } = makeTicket(1); + const { ticketData, ticketHmac } = makeTicket({ exp: 1 }); const result = await authService.signInWithTicket({ ticketData, ticketHmac }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toBe("expired"); + expect(fetchMock).not.toHaveBeenCalled(); + expect(errors).toEqual([]); + }); + it("ticket หมดอายุ + session เป็น uid คนอื่น → reason expired ไม่ยิง fetch", async () => { + trapErrors(); + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + seedSession("other-token", Math.floor(Date.now() / 1000) + 1800, "someone-else"); + + const { ticketData, ticketHmac } = makeTicket({ exp: 1 }); + const result = await authService.signInWithTicket({ ticketData, ticketHmac }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toBe("expired"); + expect(fetchMock).not.toHaveBeenCalled(); + expect(errors).toEqual([]); + }); + + it("ticket หมดอายุ + uid ตรงแต่ไม่มี entry ของ fid นี้ → reason expired ไม่ยิง fetch", async () => { + trapErrors(); + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + // session ของ oom แต่เก็บ token ของ fid อื่น + ticketService.saveSession({ + userId: "oom", + userName: "สรวิชญ์", + tokens: { OTHER_FID: { token: "other", exp: Math.floor(Date.now() / 1000) + 1800 } }, + }); + + const { ticketData, ticketHmac } = makeTicket({ exp: 1 }); + const result = await authService.signInWithTicket({ ticketData, ticketHmac }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toBe("expired"); + expect(fetchMock).not.toHaveBeenCalled(); + expect(errors).toEqual([]); + }); +}); + +describe("signInWithTicket — ยิง /token ทุกครั้งที่เปิดเอกสาร", () => { + it("happy-path: ไม่มี session + 200 → ok และสร้าง session ใหม่ครบ userId/userName/tokens[fid]", async () => { + trapErrors(); + const fetchMock = vi.fn(async (_url: string | URL | Request) => tokenResponse("token-1")); + vi.stubGlobal("fetch", fetchMock); + + const { ticketData, ticketHmac } = makeTicket(); + const result = await authService.signInWithTicket({ ticketData, ticketHmac }); + expect(result.ok, JSON.stringify(result)).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + // endpoint ที่ยิงจริง = path สั้น /api/auth/token (เปลี่ยนจาก /api/documents/token 2026-11-05) + expect(fetchMock.mock.calls[0][0]).toBe(withBasePath("/api/auth/token")); + + const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}"); + expect(stored.userId).toBe("oom"); + expect(stored.userName).toBe("สรวิชญ์"); + expect(stored.tokens[FID].token).toBe("token-1"); + expect(errors).toEqual([]); + }); + + it("เปิดเอกสารเดิมซ้ำ → ยิง /token อีกครั้ง (ตรวจลายเซ็นที่ server ทุกครั้ง) แล้วแทน entry ด้วย token ใหม่", async () => { + trapErrors(); + const fetchMock = vi.fn(async () => tokenResponse("token-2")); + vi.stubGlobal("fetch", fetchMock); + + const { ticketData, ticketHmac } = makeTicket(); + await authService.signInWithTicket({ ticketData, ticketHmac }); + await authService.signInWithTicket({ ticketData, ticketHmac }); + + expect(fetchMock).toHaveBeenCalledTimes(2); + const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}"); + expect(stored.tokens[FID].token).toBe("token-2"); + expect(errors).toEqual([]); + }); + + it("user ใหม่ (session uid ต่าง) + 200 → เคลียร์ session เก่าทั้งก้อนแล้วสร้างใหม่", async () => { + trapErrors(); + const fetchMock = vi.fn(async () => tokenResponse("token-new-user")); + vi.stubGlobal("fetch", fetchMock); + seedSession("old-user-token", Math.floor(Date.now() / 1000) + 1800, "someone-else"); + + const { ticketData, ticketHmac } = makeTicket(); + const result = await authService.signInWithTicket({ ticketData, ticketHmac }); + expect(result.ok, JSON.stringify(result)).toBe(true); + + const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}"); + expect(stored.userId).toBe("oom"); + expect(stored.tokens[FID].token).toBe("token-new-user"); + expect(JSON.stringify(stored)).not.toContain("old-user-token"); // ของเดิมถูกเคลียร์ทิ้งจริง + expect(errors).toEqual([]); + }); + + it("คนเดิมแต่ uname เปลี่ยน → session.userName อัปเดตตามไฟล์ล่าสุด", async () => { + trapErrors(); + const fetchMock = vi.fn(async () => tokenResponse("token-keep")); + vi.stubGlobal("fetch", fetchMock); + + await authService.signInWithTicket(makeTicket({ uname: "ชื่อเดิม" })); + await authService.signInWithTicket(makeTicket({ uname: "ชื่อใหม่" })); + + const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}"); + expect(stored.userName).toBe("ชื่อใหม่"); + expect(errors).toEqual([]); + }); +}); + +describe("signInWithTicket — แยกทางตาม status ของ /token (contract 2026-11-04)", () => { + it("400 (ฟอร์แม็ตพัง) → reason tampered", async () => { + trapErrors(); + const fetchMock = vi.fn(async () => errorResponse(400)); + vi.stubGlobal("fetch", fetchMock); + + const result = await authService.signInWithTicket(makeTicket()); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toBe("tampered"); + expect(errors).toEqual([]); + }); + + it("401 (ลายเซ็นไม่ถูก) → reason tampered", async () => { + trapErrors(); + vi.stubGlobal("fetch", vi.fn(async () => errorResponse(401))); + + const result = await authService.signInWithTicket(makeTicket()); expect(result.ok).toBe(false); if (!result.ok) { - expect(result.reason).toBe("expired"); - expect(typeof result.message).toBe("string"); + expect(result.reason).toBe("tampered"); + expect(result.message).toContain("ลายเซ็น"); } expect(errors).toEqual([]); }); - it("ไม่มี session + เคยทำงาน (server 200) → สร้าง session ใหม่ได้แม้ ticket หมดอายุ", async () => { + it("403 (ลายเซ็นถูกแต่หมดอายุ) + มี JWT ของ fid ใน session ที่ยังไม่หมด → ใช้ entry เดิมต่อ ไม่แทนที่", async () => { trapErrors(); - vi.stubGlobal("crypto", webcrypto); - const fetchMock = vi.fn(async () => tokenResponse("token-rebuilt")); + const fetchMock = vi.fn(async () => errorResponse(403)); vi.stubGlobal("fetch", fetchMock); + seedSession("cached-token", Math.floor(Date.now() / 1000) + 1800); - const { ticketData, ticketHmac } = makeTicket(1); + const { ticketData, ticketHmac } = makeTicket({ exp: 1, uname: "ชื่อใหม่" }); const result = await authService.signInWithTicket({ ticketData, ticketHmac }); - expect(result.ok, JSON.stringify(result)).toBe(true); expect(fetchMock).toHaveBeenCalledTimes(1); + const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}"); - expect(stored.userId).toBe("oom"); - expect(stored.tokens[FID].token).toBe("token-rebuilt"); + expect(stored.tokens[FID].token).toBe("cached-token"); // ไม่ถูกแทนที่ + expect(stored.userName).toBe("ชื่อใหม่"); // refresh ชื่อตามไฟล์ล่าสุด expect(errors).toEqual([]); }); - it("renewTokenFromTicket กับ ticket หมดอายุ + server 200 → ยังยิงขอ (server ตัดสิน) คืน true", async () => { + it("403 + entry ของ fid หมดอายุแล้ว → reason expired", async () => { + trapErrors(); + const fetchMock = vi.fn(async () => errorResponse(403)); + vi.stubGlobal("fetch", fetchMock); + seedSession("token-stale", Math.floor(Date.now() / 1000) - 60); + + const { ticketData, ticketHmac } = makeTicket({ exp: 1 }); + const result = await authService.signInWithTicket({ ticketData, ticketHmac }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toBe("expired"); + expect(fetchMock).toHaveBeenCalledTimes(1); // pre-check ปล่อยผ่าน (มี entry) แล้ว server ตอบ 403 + expect(errors).toEqual([]); + }); + + it("500 / network / 200 แต่ body เสีย → reason token-request-failed (ไม่ใช่ความผิดของไฟล์)", async () => { + trapErrors(); + const cases: { label: string; mock: () => unknown }[] = [ + { label: "500", mock: () => vi.fn(async () => errorResponse(500)) }, + { label: "network", mock: () => vi.fn(async () => Promise.reject(new Error("offline"))) }, + { + label: "malformed", + mock: () => + vi.fn(async () => new Response(JSON.stringify({ nope: true }), { status: 200, headers: { "content-type": "application/json" } })), + }, + ]; + for (const { label, mock } of cases) { + window.localStorage.clear(); + ticketService.clearSession(); + vi.stubGlobal("fetch", mock()); + + const result = await authService.signInWithTicket(makeTicket()); + expect(result.ok, label).toBe(false); + if (!result.ok) expect(result.reason, label).toBe("token-request-failed"); + expect(errors, label).toEqual([]); + } + }); +}); + +describe("signInWithTicket — resilience", () => { + it("localStorage โดน block (webview/private mode) → ผ่านด้วย memory fallback ไม่ throw", async () => { + trapErrors(); + const blocked = () => { + throw new DOMException("blocked", "SecurityError"); + }; + vi.spyOn(Storage.prototype, "getItem").mockImplementation(blocked); + vi.spyOn(Storage.prototype, "setItem").mockImplementation(blocked); + vi.spyOn(Storage.prototype, "removeItem").mockImplementation(blocked); + vi.stubGlobal("fetch", vi.fn(async () => tokenResponse("token-memory"))); + + const result = await authService.signInWithTicket(makeTicket()); + expect(result.ok, JSON.stringify(result)).toBe(true); + // token ใช้ได้ภายใน task pane instance นี้ผ่าน memory fallback + expect(await authService.getAccessToken()).toBe("token-memory"); + expect(errors).toEqual([]); + }); +}); + +describe("renewTokenFromTicket", () => { + it("200 → true และ session ได้ token ใหม่", async () => { trapErrors(); - vi.stubGlobal("crypto", webcrypto); const fetchMock = vi.fn(async () => tokenResponse("token-renewed")); vi.stubGlobal("fetch", fetchMock); - // sign-in ครั้งแรกด้วย ticket หมดอายุผ่านทาง server 200 → currentTicket เป็นตัวหมดอายุ - const { ticketData, ticketHmac } = makeTicket(1); - const signIn = await authService.signInWithTicket({ ticketData, ticketHmac }); + const signIn = await authService.signInWithTicket(makeTicket()); expect(signIn.ok, JSON.stringify(signIn)).toBe(true); fetchMock.mockClear(); const renewed = await authService.renewTokenFromTicket(); expect(renewed).toBe(true); expect(fetchMock).toHaveBeenCalledTimes(1); + const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}"); + expect(stored.tokens[FID].token).toBe("token-renewed"); + expect(errors).toEqual([]); + }); + + it("ticket หมดอายุ (server ตอบ 403) / 401 / network → false ไม่ throw", async () => { + trapErrors(); + const cases: { label: string; mock: () => unknown }[] = [ + { label: "403", mock: () => vi.fn(async () => errorResponse(403)) }, + { label: "401", mock: () => vi.fn(async () => errorResponse(401)) }, + { label: "network", mock: () => vi.fn(async () => Promise.reject(new Error("offline"))) }, + ]; + for (const { label, mock } of cases) { + window.localStorage.clear(); + ticketService.clearSession(); + authService.signInForDemo(); + vi.stubGlobal("fetch", vi.fn(async () => tokenResponse("seed-token"))); + const signIn = await authService.signInWithTicket(makeTicket()); + expect(signIn.ok, label).toBe(true); + vi.stubGlobal("fetch", mock()); + + const renewed = await authService.renewTokenFromTicket(); + expect(renewed, label).toBe(false); + expect(errors, label).toEqual([]); + } + }); + + it("ยังไม่ได้ sign in (ไม่มี currentTicket) → false ไม่ยิง fetch", async () => { + trapErrors(); + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + // authService.signInForDemo() ใน afterEach/reset ตั้ง currentTicket = null อยู่แล้ว + + expect(await authService.renewTokenFromTicket()).toBe(false); + expect(fetchMock).not.toHaveBeenCalled(); expect(errors).toEqual([]); }); }); diff --git a/tests/wordaddin/versionFlow.test.ts b/tests/wordaddin/versionFlow.test.ts new file mode 100644 index 0000000..a7aa2ff --- /dev/null +++ b/tests/wordaddin/versionFlow.test.ts @@ -0,0 +1,18 @@ +// @vitest-environment jsdom +// resolveTrackingId (งาน B 2026-11-05) — tracking id ของ Start Versioning ใช้ fid จาก +// ticket แทน GUID สุ่มใหม่ (TRACKING_ID_SOURCE = "fid") · ไม่มี fid = fallback GUID เดิม +import { describe, expect, it } from "vitest"; + +import { resolveTrackingId } from "@/wordaddin/services/versionFlow"; + +const GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; + +describe("resolveTrackingId", () => { + it("มี fid (sign-in ผ่านแล้ว) → ใช้ fid ตรง ๆ ตรงกับ docId ฝั่ง server", () => { + expect(resolveTrackingId("T12_FD155_3s-long_ZZZ")).toBe("T12_FD155_3s-long_ZZZ"); + }); + + it("ไม่มี fid (null) → fallback เป็น GUID รูปแบบเดิม", () => { + expect(resolveTrackingId(null)).toMatch(GUID_RE); + }); +}); diff --git a/tests/wordaddin/versioningService.test.ts b/tests/wordaddin/versioningService.test.ts index a61ce94..4c146e8 100644 --- a/tests/wordaddin/versioningService.test.ts +++ b/tests/wordaddin/versioningService.test.ts @@ -5,7 +5,7 @@ // templateId — identity มาจาก Bearer หมด · และห้ามส่ง fields — server แกะจากไฟล์เอง // 2026-10-30), getVersions แปลง DTO, getDownloadLink ใส่ ?format= // - โหมด ?addindemo: ใช้ mock ในเครื่อง (ไม่ยิง network เลย) -import { createHmac, webcrypto } from "node:crypto"; +import { createHmac } from "node:crypto"; import { afterEach, describe, expect, it, vi } from "vitest"; import * as authService from "@/wordaddin/services/authService"; @@ -20,7 +20,6 @@ const TOKEN = "header.payload.sig"; // ค่าหยง ๆ ก็ได้ /** sign-in จริงผ่าน signInWithTicket (fetch ตอบ token 1 ครั้ง) — real mode + มี Bearer ใน session */ async function signInReal(): Promise { - vi.stubGlobal("crypto", webcrypto); // jsdom ไม่มี crypto.subtle const ticket = { exp: Math.floor(Date.now() / 1000) + 300, fid: "REAL_FID_001", diff --git a/utils/docver-ticket.ts b/utils/docver-ticket.ts index 696f96f..dcb1c43 100644 --- a/utils/docver-ticket.ts +++ b/utils/docver-ticket.ts @@ -1,7 +1,9 @@ // PLAN-ADDIN-AUTH.md §4 — ticket ของ add-in (custom properties `ticket_data` + // `HmacSha256` ที่ e-Court ผ่านกลุ่ม template stamp ลงไฟล์) — ส่วน pure ที่ // ใช้ร่วมกันทั้งฝั่ง server (lib/addin-ticket.ts — route /api/documents/token) -// และฝั่ง client ของ add-in (wordaddin/services/ticketService.ts) +// และฝั่ง client ของ add-in (wordaddin/services/authService.ts — ใช้แค่ +// parse + exp เป็น UX gate การตรวจ HMAC เป็นของ server อย่างเดียวตาม +// PLAN-SSO.md D13, 2026-11-04) // // กติกา (docs/requirement/addin-workflow.md + lib/lib-utils.ts generateHmacSha256 // ของตัว stamp ฝั่ง template): @@ -10,8 +12,7 @@ // - HmacSha256 เป็น hex lowercase 64 ตัว (การเทียบ normalize เป็น lowercase // ให้ก่อน — ฝั่ง stamp ตัวพิมพ์ใหญ่ก็ผ่านได้) // - ห้าม import อะไรที่เป็น platform-specific (node:crypto/crypto.subtle) — -// module นี้ต้องรันได้ทั้งใน route handler และ webview ของ Word โดยส่ง -// ตัวคำนวณ HMAC เข้ามาทาง parameter แทน +// module นี้ต้องรันได้ทั้งใน route handler และ webview ของ Word /** ticket ที่แกะจาก ticket_data — ชื่อ field ตามที่ e-Court กำหนด (requirement) */ export interface DocverTicket { @@ -216,25 +217,9 @@ export function isTicketExpired(exp: number, nowSec: number = Math.floor(Date.no /** * normalize ค่า digest ที่ได้รับ (property `HmacSha256` / body field) — * trim + lowercase แล้วตรวจว่าเป็น hex ครบ 64 ตัว คืน null เมื่อรูปไม่ใช่ + * (ใช้ฝั่ง server ตอนเทียบ HMAC — lib/addin-ticket.ts) */ export function normalizeHexDigest(value: string): string | null { const normalized = value.trim().toLowerCase(); return /^[0-9a-f]{64}$/.test(normalized) ? normalized : null; } - -/** - * เทียบ hex digest สองค่าแบบ constant-time (best-effort ของ pure JS — - * ฝั่ง server จริงใช้ node:crypto timingSafeEqual ผ่าน lib/addin-ticket.ts - * ตัวนี้ใช้กับ UX gate ฝั่ง client เป็นหลัก) — ค่าที่ส่งเข้าควร normalize แล้ว - */ -export function timingSafeEqualHex(a: string, b: string): boolean { - if (a.length !== b.length) return false; - let diff = 0; - for (let i = 0; i < a.length; i++) { - diff |= a.charCodeAt(i) ^ b.charCodeAt(i); - } - return diff === 0; -} - -/** ตัวคำนวณ HMAC-SHA256 คืนค่าเป็น hex lowercase — สองฝั่งฉีดของตัวเอง (Web Crypto / node:crypto) */ -export type HmacSha256Hex = (message: string, secret: string) => Promise; diff --git a/wordaddin/config.ts b/wordaddin/config.ts index 646079e..accbc65 100644 --- a/wordaddin/config.ts +++ b/wordaddin/config.ts @@ -4,17 +4,13 @@ * * ช่วง mock: versioningService เป็น mock ไม่ใช้ apiBaseUrl — * ตั้งค่านี้เมื่อปลด mock ต่อ backend จริง (ต้องเป็น HTTPS เสมอ — webview ของ Word บล็อก http) - * ส่วนการขอ token (ticket auth) ยิง same-origin /api/documents/token เสมอ ไม่ผูกกับ apiBaseUrl + * ส่วนการขอ token (ticket auth) ยิง same-origin /api/auth/token เสมอ ไม่ผูกกับ apiBaseUrl */ export const config = { /** base URL ของ backend API (ไม่มี slash ท้าย) — ว่าง = ยังไม่ต่อ backend จริง */ apiBaseUrl: process.env.NEXT_PUBLIC_ADDIN_API_BASE_URL ?? "", /** ชื่อผู้ใช้ของ mock auth — แสดงหน้า header + เป็น createdByUserId ของ version ที่ mock สร้าง */ mockUsername: process.env.NEXT_PUBLIC_ADDIN_MOCK_USERNAME ?? "tester01", - /** - * secret ตรวจ HMAC ของ ticket ฝั่ง client (UX gate — ตัดเคสไฟล์ถูกแก้ก่อนยิง network; - * ความมั่นคงจริงอยู่ที่ server ตรวจอีกชั้น เพราะค่าใน bundle สกัดได้) — ต้องตรงกับ - * ตัว stamp ของ e-Court และ env ADDIN_TICKET_SECRET/SECRET ฝั่ง server (PLAN-ADDIN-AUTH.md §7) - */ - ticketSecret: process.env.NEXT_PUBLIC_ADDIN_TICKET_SECRET ?? "super_secret", + // (ไม่มี secret ฝั่ง client อีกต่อไป — การตรวจ HMAC ของ ticket เป็นของ server อย่างเดียว + // ตาม PLAN-SSO.md D13, ถอด NEXT_PUBLIC_ADDIN_TICKET_SECRET ออก 2026-11-04) }; diff --git a/wordaddin/services/apiClient.ts b/wordaddin/services/apiClient.ts index 0b4476e..de94e21 100644 --- a/wordaddin/services/apiClient.ts +++ b/wordaddin/services/apiClient.ts @@ -4,7 +4,7 @@ import * as authService from "./authService"; /** * fetch wrapper ของ add-in — แนบ Bearer ทุก request (access_token ที่ authService - * ขอจาก /api/documents/token ด้วย ticket ของไฟล์ — PLAN-ADDIN-AUTH.md) + * ขอจาก /api/auth/token ด้วย ticket ของไฟล์ — PLAN-ADDIN-AUTH.md) * โดน 401 → ลองขอ token ใหม่จาก ticket แล้ว retry 1 ครั้ง ยังไม่ผ่าน → AuthRequiredError: * UI จับ error นี้แล้วพาไปหน้า no-access ให้ขอไฟล์ใหม่จาก e-Court * พอร์ตจาก D:\word-addin-poc\addin\src\services\apiClient.ts @@ -64,9 +64,9 @@ async function doFetch( /** * request พร้อม Bearer — โดน 401 → ขอ token ใหม่จาก ticket ของเอกสารปัจจุบัน - * (renewTokenFromTicket — ticket ยังไม่หมดอายุเท่านั้น) แล้ว retry 1 ครั้ง - * ยัง 401 อีก / renew ไม่ได้ = AuthRequiredError (UI พาไป no-access ให้ขอไฟล์ - * ใหม่จาก e-Court) · Response ที่ได้คืนไปให้ caller เช็ค ok เอง + * (renewTokenFromTicket — server เป็นคนตัดสิน: 200 = ออกให้ใหม่ · 403 = ticket + * หมดอายุ → false) แล้ว retry 1 ครั้ง ยัง 401 อีก / renew ไม่ได้ = AuthRequiredError + * (UI พาไป no-access ให้ขอไฟล์ใหม่จาก e-Court) · Response ที่ได้คืนไปให้ caller เช็ค ok เอง */ export async function apiFetch(path: string, options: ApiRequestOptions = {}): Promise { const accessToken = await authService.getAccessToken(); diff --git a/wordaddin/services/authService.ts b/wordaddin/services/authService.ts index 0c22b6a..8aa0876 100644 --- a/wordaddin/services/authService.ts +++ b/wordaddin/services/authService.ts @@ -1,21 +1,21 @@ /** * Auth ของ add-in — e-Court ticket ทางเข้าเดียว (PLAN-ADDIN-AUTH.md §5 ตาม - * docs/requirement/addin-workflow.md) + * docs/requirement/addin-workflow.md — ปรับ flow 2026-11-04) * * ไม่มี login ใน add-in: ไฟล์ที่ e-Court stamp custom properties `ticket_data` * + `HmacSha256` มา (ผ่านกลุ่ม template) = ผ่านเข้าได้ — signInWithTicket ตรวจ - * HMAC → session ใน localStorage ตาม uid/fid → ขอ access_token จาก - * /api/documents/token เก็บตาม fid ("คีย์คือ fid") · exp ของ ticket ไม่ใช่ - * เกณฑ์ตัดสิทธิ์ฝั่ง client — server ออก token ให้แม้ ticket หมดอายุเมื่อ - * เอกสารนี้เคยมี version โดย uid เดียวกัน (requirement ขั้น 7) + * ฟอร์แม็ต/exp เป็น UX gate ฝั่ง client (ไม่มี secret ใน bundle อีกต่อไปตาม + * PLAN-SSO.md D13 — การตรวจ HMAC เป็นของ server อย่างเดียว ยิง /api/auth/token + * ทุกครั้งที่เปิดเอกสาร) → แยกทางตาม contract: 200 = ได้ JWT ใหม่ / 400 ฟอร์แม็ตพัง / + * 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่ ticket หมดอายุ (มี JWT ของ fid ใน session + * ที่ยังไม่หมด = ใช้ต่อ ไม่มี = ขอไฟล์ใหม่จาก e-Court) * - * token เก็บใน localStorage (ข้าม session ได้ — กลับมาเปิดเอกสารเดิมไม่ต้องขอใหม่ - * ตาม requirement ขั้น 7) ส่วน ticket ดิบของเอกสารปัจจุบันเก็บใน memory ไว้ renew - * ตอน token หมดอายุกลาง session + * token เก็บใน localStorage ตาม fid ("คีย์คือ fid") — อายุ 7 วัน แทนระบบ refresh + * ส่วน ticket ดิบของเอกสารปัจจุบันเก็บใน memory ไว้ renew ตอนโดน 401 กลาง session */ import { config } from "../config"; -import { isTicketExpired, parseDocverTicket } from "@/utils/docver-ticket"; +import { isTicketExpired, parseDocverTicket, type DocverTicket } from "@/utils/docver-ticket"; import * as ticketService from "./ticketService"; /** สาเหตุที่เข้าไม่ได้ — UI แยกข้อความตาม reason (TaskpaneApp) */ @@ -46,13 +46,16 @@ export function isDemoMode(): boolean { } /** - * e-Court ticket — จุดเข้าเดียวของ auth ตอนเปิดเอกสาร (requirement ขั้น 1–7): - * 1. ตรวจมี ticket_data + HmacSha256 ครบ - * 2. parse + ตรวจ HMAC ฝั่ง client (UX gate) - * 3. session ใน localStorage ตาม uid/fid — token ยังใช้ได้ = ผ่านเลย · ต้องขอ - * token ใหม่ = ยิงขอเสมอแล้วให้ **server เป็นคนตัดสิน** (ticket หมดอายุแต่ - * เอกสารนี้เคยมี version โดย uid เดียวกัน = ออกให้ต่อ — requirement ขั้น 7 - * กลับมาทำเอกสารเดิมได้แม้ ticket หมดอายุ แม้ session หายไปจากเครื่อง) + * e-Court ticket — จุดเข้าเดียวของ auth ตอนเปิดเอกสาร (flow 2026-11-04): + * 1. ตรวจมี ticket_data + HmacSha256 ครบ (client) — ไม่ครบ = เตะเลย + * 2. parse ฟอร์แม็ต (client UX gate) — พัง = เตะเลย + * 3. pre-check: ticket หมดอายุ และ ไม่มี JWT ของ fid นี้ใน session (uid ตรงกัน) + * = เตะเลย ไม่ยิง API (มีใน session = มีโอกาสเป็นไฟล์เก่าที่เคยแก้ → ไปให้ + * server ตัดสินที่ขั้นถัดไป) + * 4. ที่เหลือยิง /api/auth/token **ทุกครั้ง** ให้ server ตรวจลายเซ็น (HMAC) + * แล้วแยกทางตาม contract: 200 ได้ JWT ใหม่ (user ต่าง = เคลียร์ session ทั้งก้อน + * สร้างใหม่) · 403 ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT ของ fid ที่ยังไม่หมด + * ใน session ก็ใช้ต่อ · 400/401 ฟอร์แม็ต/ลายเซ็นพัง = เตะ (ไฟล์ถูกแก้) */ export async function signInWithTicket(input: { ticketData?: string; @@ -74,14 +77,7 @@ export async function signInWithTicket(input: { return { ok: false, reason: "tampered", - message: "ตรวจสอบ ticket ไม่ผ่าน (รูปแบบไม่ถูกต้อง) — ไฟล์ถูกแก้ไขหรือเสียหาย", - }; - } - if (!(await ticketService.verifyTicketHmacWeb(ticketData, ticketHmac, config.ticketSecret))) { - return { - ok: false, - reason: "tampered", - message: "ตรวจสอบลายเซ็น (HMAC) ของ ticket ไม่ผ่าน — ไฟล์ถูกแก้ไข", + message: "รูปแบบ ticket ในไฟล์ไม่ถูกต้อง — ไฟล์ถูกแก้ไขหรือเสียหาย โปรดขอไฟล์ใหม่จาก e-Court", }; } const ticket = parsed.ticket; @@ -91,56 +87,68 @@ export async function signInWithTicket(input: { message: "ticket ของ e-Court หมดอายุ — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง", }; + // snapshot session ก่อนยิง — "มีใน session" = ยูสเซอร์ + ไฟล์ (uid+fid) ตรงกับ ticket const session = ticketService.loadSession(); - if (session === null || session.userId !== ticket.uid) { - // ไม่มี session / user ใหม่ — ขอ token ใหม่เสมอ (แม้ ticket หมดอายุ: server - // ออกให้ต่อเมื่อเอกสารนี้เคยทำงานโดย uid เดียวกัน ซึ่งช่วยเคส localStorage หาย - // ของคนที่กลับมาทำเอกสารเดิมด้วย) แล้วเริ่ม session ใหม่ทั้งก้อน - let entry: ticketService.SessionTokenEntry; - try { - entry = await ticketService.requestDocverToken(ticketData, ticketHmac); - } catch { - return isTicketExpired(ticket.exp) - ? expiredMessage - : { - ok: false, - reason: "token-request-failed", - message: "ขอ token จากระบบไม่สำเร็จ — ตรวจการเชื่อมต่อแล้วเปิดเอกสารอีกครั้ง", - }; - } - ticketService.clearSession(); - ticketService.saveSession({ userId: ticket.uid, userName: ticket.uname, tokens: { [ticket.fid]: entry } }); - } else { - // คนเดิม — userName อัปเดตตามไฟล์ล่าสุด (ชื่อเปลี่ยนได้) - session.userName = ticket.uname; - let entry = session.tokens[ticket.fid] ?? null; - if (entry !== null && ticketService.isTokenExpired(entry)) { - // token หมดอายุ — ยิง renew เสมอแล้วให้ server ตัดสิน (ticket ยังไม่หมด = - // ออกให้เสมอ · หมดแล้ว = ออกเฉพาะเอกสารนี้เคยทำงานโดย uid เดียวกัน) - try { - entry = await ticketService.requestDocverToken(ticketData, ticketHmac); - session.tokens[ticket.fid] = entry; - } catch { - // ใช้ token เก่าต่อ — ถ้าเน่าจริง apiClient จะ 401 → renew-retry → no-access - } - } - if (entry === null) { - // เอกสารใหม่ของคนเดิม — ขอ token เก็บเพิ่ม (server ตัดสินเรื่อง ticket หมดอายุ) - try { - session.tokens[ticket.fid] = await ticketService.requestDocverToken(ticketData, ticketHmac); - } catch { - return isTicketExpired(ticket.exp) - ? expiredMessage - : { - ok: false, - reason: "token-request-failed", - message: "ขอ token จากระบบไม่สำเร็จ — ตรวจการเชื่อมต่อแล้วเปิดเอกสารอีกครั้ง", - }; - } - } - ticketService.saveSession(session); + const inSession = + session !== null && session.userId === ticket.uid && session.tokens[ticket.fid] !== undefined; + + // pre-check: หมดอายุ + ไม่มีใน session = เตะโดยไม่ยิง API (ลดการเรียกใช้ network) + if (isTicketExpired(ticket.exp) && !inSession) { + return expiredMessage; } + // ยิงทุกครั้งที่เปิดเอกสาร — signature ถูกตรวจที่ server เท่านั้น (ไม่มี secret ฝั่ง client) + const result = await ticketService.requestDocverToken(ticketData, ticketHmac); + if (!result.ok) { + if (result.kind === "http" && result.status === 400) { + return { + ok: false, + reason: "tampered", + message: "รูปแบบ ticket ในไฟล์ไม่ถูกต้อง — ไฟล์ถูกแก้ไขหรือเสียหาย โปรดขอไฟล์ใหม่จาก e-Court", + }; + } + if (result.kind === "http" && result.status === 401) { + return { + ok: false, + reason: "tampered", + message: "ตรวจสอบลายเซ็น (HMAC) ของ ticket กับระบบไม่ผ่าน — ไฟล์ถูกแก้ไขหรือเสียหาย โปรดขอไฟล์ใหม่จาก e-Court", + }; + } + if (result.kind === "http" && result.status === 403) { + // ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT ของ fid (uid ตรง) ที่ยังไม่หมด = ใช้ต่อ + if (session !== null && session.userId === ticket.uid) { + const entry = session.tokens[ticket.fid]; + if (entry !== undefined && !ticketService.isTokenExpired(entry)) { + session.userName = ticket.uname; // refresh ชื่อตามไฟล์ล่าสุด + ticketService.saveSession(session); + return finishSignIn(ticket, ticketData, ticketHmac); + } + } + return expiredMessage; + } + // network/malformed/5xx และอื่น ๆ — ไม่ใช่ความผิดของไฟล์ ลองใหม่ได้เมื่อเน็ตกลับมา + return { + ok: false, + reason: "token-request-failed", + message: "ขอ token จากระบบไม่สำเร็จ — ตรวจการเชื่อมต่อแล้วเปิดเอกสารอีกครั้ง", + }; + } + + // 200 — user ใหม่ = เคลียร์ session เก่าทั้งก้อน (requirement ขั้น 6) สร้างใหม่ · + // คนเดิม = update userName (ชื่อเปลี่ยนได้) + เก็บ token ใหม่ตาม fid + if (session === null || session.userId !== ticket.uid) { + ticketService.clearSession(); + ticketService.saveSession({ userId: ticket.uid, userName: ticket.uname, tokens: { [ticket.fid]: result.entry } }); + } else { + session.userName = ticket.uname; + session.tokens[ticket.fid] = result.entry; + ticketService.saveSession(session); + } + return finishSignIn(ticket, ticketData, ticketHmac); +} + +/** ท้ายร่วมของทุกทางที่ผ่าน — ตั้ง module state ของ taskpane instance นี้ */ +function finishSignIn(ticket: DocverTicket, ticketData: string, ticketHmac: string): SignInResult { currentFid = ticket.fid; currentTicket = { data: ticketData, hmac: ticketHmac }; username = ticket.uname; @@ -151,26 +159,22 @@ export async function signInWithTicket(input: { /** * ขอ token ใหม่จาก ticket ของเอกสารปัจจุบัน — เรียกจาก apiClient ตอนโดน 401 * ก่อน throw AuthRequiredError 1 รอบ · ไม่ตัดสิน exp ของ ticket ฝั่งนี้ (server - * ออกให้ต่อได้แม้ ticket หมดอายุเมื่อเอกสารนี้เคยทำงานโดย uid เดียวกัน — - * requirement ขั้น 7) 401 กลับมา = false ตามเดิม + * เป็นคนตอบ: 200 = ออกให้ใหม่ · 403 = ticket หมดอายุ → false) * คืน true = ได้ token ใหม่เรียบร้อย (getAccessToken จะเห็นค่าใหม่) */ export async function renewTokenFromTicket(): Promise { if (currentTicket === null || currentFid === null) return false; const parsed = parseDocverTicket(currentTicket.data); if (!parsed.ok) return false; - try { - const entry = await ticketService.requestDocverToken(currentTicket.data, currentTicket.hmac); - const session = - ticketService.loadSession() ?? - { userId: parsed.ticket.uid, userName: parsed.ticket.uname, tokens: {} }; - session.userName = parsed.ticket.uname; - session.tokens[parsed.ticket.fid] = entry; - ticketService.saveSession(session); - return true; - } catch { - return false; - } + const result = await ticketService.requestDocverToken(currentTicket.data, currentTicket.hmac); + if (!result.ok) return false; + const session = + ticketService.loadSession() ?? + { userId: parsed.ticket.uid, userName: parsed.ticket.uname, tokens: {} }; + session.userName = parsed.ticket.uname; + session.tokens[parsed.ticket.fid] = result.entry; + ticketService.saveSession(session); + return true; } /** Bearer ของ apiClient — token ของ fid ปัจจุบัน (stale ได้ — apiClient เป็นคนตัดสิน 401) */ @@ -185,3 +189,9 @@ export async function getAccessToken(): Promise { export function getUsername(): string | undefined { return username ?? undefined; } + +/** fid ของเอกสารปัจจุบัน (จาก ticket ที่ sign-in ผ่าน) — null = ยังไม่ได้ sign-in หรือโหมด demo + * ใช้เป็น tracking id ตอน Start Versioning (versionFlow.resolveTrackingId, 2026-11-05) */ +export function getCurrentFid(): string | null { + return currentFid; +} diff --git a/wordaddin/services/metadataService.ts b/wordaddin/services/metadataService.ts index cd25bc8..6051723 100644 --- a/wordaddin/services/metadataService.ts +++ b/wordaddin/services/metadataService.ts @@ -14,6 +14,8 @@ * (known bug บน Mac บาง build — เขียนไม่ได้ก็ปล่อยผ่าน) */ +import { saveDocumentVerified } from "./documentService"; + /** GUID คงที่ของ add-in — ต้องตรงเป๊ะกับ `AddInMetadata.AddInSignatureGuid` ฝั่ง backend (spec ส่วน 8) */ export const ADDIN_SIGNATURE_GUID = "9B7A3C51-4E6F-4D2A-8B5C-1E0F2A3B4C5D"; @@ -128,6 +130,60 @@ export async function stampTrackedProperties(input: StampPropertiesInput): Promi }); } +// --------------------------------------------------------------------------- +// Auto-open taskpane (Office.AutoShowTaskpaneWithDocument) — 2026-11-05 +// --------------------------------------------------------------------------- + +/** ชื่อ setting ที่ทำให้ taskpane เด้งอัตโนมัติเมื่อเปิดเอกสารนี้ครั้งถัดไป — ต้องตรงกับ + * `` ใน manifest.template.xml และ `` ที่ lib/lib-tmpl.ts stamp + * ลง word/webextensions/ ฝั่ง server (learn.microsoft.com/.../automatically-open-a-task-pane-with-a-document) */ +export const AUTO_OPEN_SETTING = "Office.AutoShowTaskpaneWithDocument"; + +/** memo กัน double-call — detect() ถูกเรียกซ้ำ (ปุ่ม "ลองใหม่") ก็แชร์ promise เดิม; + * ล้มแล้วล้างทิ้งให้ครั้งถัดไปได้ลองใหม่ */ +let autoOpenPromise: Promise | null = null; + +/** + * tag เอกสารปัจจุบันให้ taskpane เด้งอัตโนมัติครั้งถัดไปที่เปิดไฟล์นี้ — **best-effort + * ห้าม throw ทุกกรณี** (auto-open เป็น UX enhancement: ล้ม = ผู้ใช้กดปุ่มเองตามเดิม) + * คืน true = setting มีอยู่แล้ว หรือเขียนสำเร็จ · false = ทำไม่สำเร็จ (เงียบ ไม่ใช่ error ของ UI) + */ +export function enableAutoOpenTaskpane(): Promise { + if (autoOpenPromise === null) { + autoOpenPromise = (async () => { + try { + const settings = Office.context.document.settings; + if (settings.get(AUTO_OPEN_SETTING) === true) return true; // idempotent — ไม่เขียนซ้ำ + settings.set(AUTO_OPEN_SETTING, true); + const saved = await new Promise((resolve) => { + settings.saveAsync((result) => resolve(result.status === Office.AsyncResultStatus.Succeeded)); + }); + if (!saved) { + console.warn("auto-open: settings.saveAsync ไม่สำเร็จ — taskpane จะไม่เด้งครั้งถัดไป"); + return false; + } + try { + // saveAsync เขียนเฉพาะ document ในหน่วยความจำ — persist ลงดิสก์จริงต้องตามด้วย native + // save (ไฟล์ read-only/Protected View = ยอมแพ้เงียบ ๆ: auto-save 30 วิเป็น net รอง) + await saveDocumentVerified(); + } catch (error) { + console.warn("auto-open: บันทึกไฟล์ไม่สำเร็จ (read-only?) — setting อาจยังไม่ถึงดิสก์", error); + } + return true; + } catch (error) { + // นอก host Word (browser/SSR) หรือ host ไม่รองรับ Settings API — เงียบไป + console.warn("auto-open: เขียน setting ไม่สำเร็จ (นอก Word หรือ host ไม่รองรับ)", error); + return false; + } + })(); + // ล้ม = ล้าง memo ให้ detect ครั้งถัดได้ลองใหม่ (ผ่านปุ่ม "ลองใหม่") + void autoOpenPromise.then((ok) => { + if (!ok) autoOpenPromise = null; + }); + } + return autoOpenPromise; +} + /** GUID ใหม่สำหรับ tracking id / clientVersionId */ export function generateGuid(): string { if (typeof crypto.randomUUID === "function") { diff --git a/wordaddin/services/ticketService.ts b/wordaddin/services/ticketService.ts index 744c764..0cc3d89 100644 --- a/wordaddin/services/ticketService.ts +++ b/wordaddin/services/ticketService.ts @@ -1,17 +1,19 @@ /** * Ticket ของ add-in — ของมือถือ (primitives) ให้ authService ประกอบเป็น flow * (PLAN-ADDIN-AUTH.md §5): - * - verifyTicketHmacWeb: ตรวจ HMAC ฝั่ง client ด้วย Web Crypto (UX gate — ตัดเคส - * ไฟล์ถูกแก้ก่อนยิง network ส่วนความมั่นคงจริงอยู่ที่ server ตรวจอีกชั้น) * - session store: localStorage key เดียว `wordaddin-auth` — userId/userName + * map token ต่อ fid · webview block localStorage = fallback memory ต่อ session * (pattern เดียวกับ versioningService) - * - requestDocverToken: POST same-origin /api/documents/token แบบ body ticket — - * ไม่ผูกกับ config.apiBaseUrl (endpoint อยู่แอปนี้ ใช้ได้ทั้งช่วง mock และจริง) + * - requestDocverToken: POST same-origin /api/auth/token แบบ body ticket + * (path สั้นของ /api/documents/token — endpoint ก้อนเดียวกัน เปลี่ยนมาใช้ 2026-11-05) — + * ไม่ผูกกับ config.apiBaseUrl (endpoint อยู่แอปนี้ ใช้ได้ทั้งช่วง mock และจริง) · + * คืน discriminated result แยก network/http status/malformed ให้ caller + * ตัดสินตาม contract 400/401/403 (2026-11-04) + * - (ไม่มีการตรวจ HMAC ฝั่ง client อีกต่อไป — secret เป็นของ server อย่างเดียว + * ตาม PLAN-SSO.md D13, ถอด verifyTicketHmacWeb ออก 2026-11-04) */ import { withBasePath } from "@/utils/base-path"; -import { normalizeHexDigest } from "@/utils/docver-ticket"; /** key เดียวของ session — v ไว้ migrate รูปในอนาคต */ const SESSION_STORAGE_KEY = "wordaddin-auth"; @@ -92,39 +94,7 @@ export function isTokenExpired(entry: SessionTokenEntry): boolean { } // --------------------------------------------------------------------------- -// HMAC ฝั่ง client — Web Crypto (crypto.subtle ให้ promise เสมอ) -// --------------------------------------------------------------------------- - -/** คำนวณ HMAC-SHA256 คืน hex lowercase — รูปแบบเดียวกับ generateHmacSha256 ฝั่ง server */ -export async function hmacSha256HexWeb(message: string, secret: string): Promise { - const encoder = new TextEncoder(); - const key = await crypto.subtle.importKey( - "raw", - encoder.encode(secret), - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], - ); - const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message)); - return Array.from(new Uint8Array(signature), (byte) => byte.toString(16).padStart(2, "0")).join(""); -} - -/** - * ตรวจ HMAC ของ ticket ฝั่ง client — message คือ string ดิบของ ticket_data ตามที่อ่านจาก - * property (ห้าม trim — ตัว stamp เซ็นบนค่าดิบ) · เทียบหลัง normalize lowercase - */ -export async function verifyTicketHmacWeb(ticketData: string, expectedHex: string, secret: string): Promise { - const expected = normalizeHexDigest(expectedHex); - if (expected === null) return false; - try { - return (await hmacSha256HexWeb(ticketData, secret)) === expected; - } catch { - return false; - } -} - -// --------------------------------------------------------------------------- -// ขอ token จาก /api/documents/token (ทางเข้า ticket — PLAN-ADDIN-AUTH.md §3) +// ขอ token จาก /api/auth/token (ทางเข้า ticket — PLAN-ADDIN-AUTH.md §3) // --------------------------------------------------------------------------- interface TokenResponse { @@ -147,30 +117,50 @@ function jwtExp(token: string): number | undefined { } } +/** ผลของ requestDocverToken — แยกสาเหตุให้ caller ตัดสินตาม contract ไม่โยน exception */ +export type RequestDocverTokenResult = + | { ok: true; entry: SessionTokenEntry } + /** fetch reject — ไม่ถึง server (offline/dev server ตาย) */ + | { ok: false; kind: "network" } + /** ได้ response แต่ไม่ใช่ 200 — 400 ฟอร์แม็ตพัง / 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่ ticket หมดอายุ */ + | { ok: false; kind: "http"; status: number } + /** ตอบ 200 แต่ body ใช้ไม่ได้ (ไม่ใช่ JSON / ไม่มี access_token) */ + | { ok: false; kind: "malformed" }; + /** - * ขอ access_token ด้วย ticket — โยน Error เมื่อ status ไม่สำเร็จ - * (401 = ticket ไม่ผ่านการตรวจฝั่ง server — ให้ caller ไป no-access ตาม flow) + * ขอ access_token ด้วย ticket — คืน discriminated result ไม่โยน Error: + * caller (authService) แยกทางตาม contract ของทางเข้า ticket ของ + * /api/auth/token (2026-11-04: 200 ผ่าน / 400 ฟอร์แม็ต / 401 ลายเซ็น / + * 403 ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT เดิมของ fid ใน session ใช้ต่อได้) */ -export async function requestDocverToken(ticketData: string, ticketHmac: string): Promise { - const response = await fetch(withBasePath("/api/documents/token"), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ ticket_data: ticketData, HmacSha256: ticketHmac }), - }); +export async function requestDocverToken( + ticketData: string, + ticketHmac: string, +): Promise { + let response: Response; + try { + response = await fetch(withBasePath("/api/auth/token"), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ ticket_data: ticketData, HmacSha256: ticketHmac }), + }); + } catch { + return { ok: false, kind: "network" }; + } if (!response.ok) { - throw new Error(`ขอ token ไม่สำเร็จ (HTTP ${response.status})`); + return { ok: false, kind: "http", status: response.status }; } let data: TokenResponse; try { data = (await response.json()) as TokenResponse; } catch { - throw new Error("ขอ token ไม่สำเร็จ (response ไม่ใช่ JSON)"); + return { ok: false, kind: "malformed" }; } if (typeof data.access_token !== "string" || data.access_token === "") { - throw new Error("ขอ token ไม่สำเร็จ (ไม่มี access_token)"); + return { ok: false, kind: "malformed" }; } const exp = jwtExp(data.access_token) ?? (typeof data.expires_in === "number" ? Math.floor(Date.now() / 1000) + data.expires_in : 0); - return { token: data.access_token, exp }; + return { ok: true, entry: { token: data.access_token, exp } }; } diff --git a/wordaddin/services/versionFlow.ts b/wordaddin/services/versionFlow.ts index 11f369c..f0b879c 100644 --- a/wordaddin/services/versionFlow.ts +++ b/wordaddin/services/versionFlow.ts @@ -30,6 +30,17 @@ import { } from "./metadataService"; import { uploadVersion } from "./versioningService"; +/** 2026-11-05 — tracking id ใช้ **fid จาก ticket** (ตรงกับ docId ของทุก operation ฝั่ง server — + * uploadVersion real mode ไม่ส่ง documentId ไป server อยู่แล้ว identity มาจาก Bearer) แทน GUID + * สุ่มใหม่ · **ย้อนกลับเป็นแบบเดิม**: เปลี่ยนค่าเป็น "guid" (generateGuid ยังอยู่ — ไฟล์เก่า + * ที่ stamp GUID ไว้แล้วยัง track ได้เพราะ detect อ่านค่าตาม property ไม่สนรูปแบบค่า) */ +const TRACKING_ID_SOURCE: "fid" | "guid" = "fid"; + +/** เลือกค่า tracking id ตามโหมด — pure แยกไว้ให้ unit test ได้ (ไม่มี fid = fallback GUID เดิม) */ +export function resolveTrackingId(currentFid: string | null): string { + return TRACKING_ID_SOURCE === "fid" && currentFid !== null ? currentFid : generateGuid(); +} + export interface StartVersioningResult { hasPdf: boolean; documentId: string; @@ -121,7 +132,7 @@ export async function startVersioning( // FR-17 — หยุด auto-save ตลอด flow กัน save แทรกกลาง stamp/capture แล้วค่อนคืนตอนจบ await autoSaveService.suspend(); try { - const documentId = generateGuid(); + const documentId = resolveTrackingId(authService.getCurrentFid()); const clientVersionId = generateGuid(); const timestamp = new Date().toISOString(); @@ -147,6 +158,13 @@ export async function startVersioning( // 4) upload version แรก (แนบ templateId เมื่อเป็นไฟล์ร่าง — FR-16) onProgress?.("กำลังอัปโหลด version 1…"); const result = await uploadCurrentFile(documentId, clientVersionId, message, onProgress, templateId); + + // 5) final save (2026-11-05 bug fix) — การันตีว่าทุกอย่างที่ stamp ใน flow นี้ persist + // ถึงดิสก์จริงก่อนจบ (เดิม save ครั้งเดียวช่วงขั้น 3 ก่อน upload — เคยเจอไฟล์ถูก stamp + // แล้วได้ version แต่ดิสก์ไม่ได้รับค่า ปิดไฟล์กลับมาเปิดใหม่กลายเป็น untracked) + onProgress?.("กำลังบันทึกไฟล์…"); + await saveDocumentVerified(); + // FR-3 — สร้าง version สำเร็จ → เคลียร์ flag "มีแก้ไขที่ยังไม่เป็น version" autoSaveService.clearUncommittedSinceVersion(); return { documentId, ...result }; @@ -206,11 +224,19 @@ export async function saveAndVersion( } } + // 6) final save (2026-11-05 bug fix) — ขั้น 5 เขียน XML part หลัง upload โดยไม่มี save ตาม + // = เอกสาร dirty ตอน flow จบ ผู้ใช้ปิด Word โดยไม่ save เพิ่ม = ค่าที่ stamp ทั้ง flow + // (รวม tracking properties ของขั้น 1) ไม่ถึงดิสก์ → เปิดใหม่กลายเป็น untracked — + // ต้อง save ปิดท้ายหลังทุก mutation ก่อนคืนผล (และก่อน close เพราะหลัง close เรียก + // Word.run ต่อไม่ได้) + onProgress?.("กำลังบันทึกไฟล์…"); + await saveDocumentVerified(); + // FR-3 — สร้าง version สำเร็จ → เคลียร์ flag "มีแก้ไขที่ยังไม่เป็น version" autoSaveService.clearUncommittedSinceVersion(); - // 6) FR-18 — ปิดเอกสารเฉพาะเมื่อ upload สำเร็จแล้วเท่านั้น (ต้องอยู่หลังขั้น 5 เพราะหลัง close - // เอกสารหมดสิทธิ์เรียก Word.run ต่อ) + // 7) FR-18 — ปิดเอกสารเฉพาะเมื่อ upload สำเร็จแล้วเท่านั้น (ต้องอยู่หลังขั้น final save + // เพราะหลัง close เอกสารหมดสิทธิ์เรียก Word.run ต่อ) if (closeAfter) { try { await closeDocument(DocumentCloseMode.Save);