diff --git a/.env.example b/.env.example index 7af635b..0a4a54b 100644 --- a/.env.example +++ b/.env.example @@ -6,22 +6,6 @@ DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/ecourt-note # ไม่ตั้ง/ว่าง = ปฏิเสธทุก request (fail closed) — ตั้งค่ายาวและสุ่ม เช่น 32+ ตัวอักษร # EXTERNAL_API_KEY= -# สำหรับตัวอย่างระบบนอกเรียกใช้ API ของ template -DOCEDIT_BASE_URL=http://192.168.2.51:3000/docedit -# สำหรับตัวอย่างระบบนอกเรียกใช้ API ของ template -EXTERNAL_API_KEY='secret-api-key-1234' -# ที่เก็บไฟล์เทมเพลต(docx) -TMPL_PATH='./public/tmpl' - -# Sign/Hash ที่ใช้ฝังในเอกสาร(docx) -SECRET="super_secret" -PRIVATE_KEY_HEX="8f2077b57b39399b6a7828f35da03a63ade8e3a36a6bf437b091a8bdb3d18ee1" -PUBLIC_KEY_HEX="66b09b4a1b66271d5728c592c2409657e1c070a7bf371bf9f6ed457855270546" -# Optional เวลาหมดอายุที่ฝังไว้ในข้อมูล (ticket_data) -TICKET_SEC=300 - - - # MinIO ของ API เอกสาร versioning (docs/PLAN-DOCVER.md §6) — ค่านี้ตรง docker-compose.yml # (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY ไม่ตั้ง = ทุก endpoint ของกลุ่มนี้ตอบ 500) # หมายเหตุ: พอร์ตโฮสต์เป็น 19000 เพราะ 9000/9001 ชนกับโปรเจกต์อื่นในเครื่อง dev @@ -34,12 +18,3 @@ MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin # MINIO_BUCKET=docver # DOCVER_LINK_TTL_SECONDS=900 - -# docver token (2026-10-22): API key ของ POST /api/documents/token (header x-api-key) -# ไม่ตั้ง/ว่าง = ปฏิเสธทุก request (fail closed) — ตั้งค่ายาวและสุ่ม เช่น 32+ ตัวอักษร -# (ห้าม log ค่า key ดิบ — เทียบแบบ timing-safe ทุกครั้ง) -DOCVER_API_KEY= -# secret เซ็น/ตรวจ JWT HS256 ของทุก endpoint /api/documents/* (Authorization: Bearer) -# ไม่ตั้ง/ว่าง = ออก token ไม่ได้ (500) และทุก endpoint ตอบ 401 (fail closed) — ห้าม log ค่า secret -DOCVER_TOKEN_SECRET= -# DOCVER_TOKEN_TTL_SECONDS=3600 diff --git a/CLAUDE.md b/CLAUDE.md index f5ccb40..c1656e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ 1. **Non-Destructive:** ไฟล์ต้นฉบับ (PDF/รูปภาพ) ไม่ถูกแก้ไขเด็ดขาด — annotation เก็บใน DB เป็นพิกัดเท่านั้น (export PDF ของ F8/N3 สร้างไฟล์ใหม่จากสำเนาในหน่วยความจำ — อ่าน `public/` อย่างเดียว ห้ามเขียนทับ; อัปโหลด N4 เขียนไฟล์**ใหม่**ที่ `public/note/uploads/` เท่านั้น ห้ามแตะไฟล์เดิม) 2. **พิกัดทุกอย่างใน DB เป็น % ของขนาดหน้า** — `rects`, `paths`, `position`, `strokeWidth` (% ของ width), `yOffset` (% ของ height) — แปลง px⇄% ผ่าน `utils/coordinates.ts` เท่านั้น และก่อนบันทึก HIGHLIGHT ต้องรวบ rects ด้วย `mergeRectsByLine` เหลือ 1 rect ต่อบรรทัด 3. **ลำดับ layer ตาม PLAN.md §1:** canvas (`pointer-events: none` เสมอ) → highlight-layer (`none` เสมอ, `mix-blend-mode: multiply`) → textLayer → InkLayer (SVG) → comment pins — สลับ pointer-events ตาม tool ปัจจุบัน -4. **เขียนผ่าน Server Actions เท่านั้น** (`app/actions/annotations.ts`) และ**หลังเขียน DB สำเร็จทุกครั้ง ต้อง publish event ผ่าน `lib/annotate-events.ts` + `revalidatePath` หน้า viewer และ summary ของ fileId นั้น** (helper `revalidateFilePages` — สั่ง browser ทิ้ง Router Cache ฝั่ง client, `force-dynamic` ปิดแค่ฝั่ง server ไม่พอ: ไม่งั้นกด back แล้วคลิกเข้าไฟล์ใหม่ยังได้ initial data เก่าจนต้อง hard refresh) / **อ่าน + sync ผ่าน SSE hook** (`hooks/useAnnotationRealtime.ts` เปิด `EventSource` ไป `/api/note/annotations/stream?fileId=`) — event เป็น DTO (camelCase + ISO string) จาก server ตรง ๆ — **ยกเว้น `registerExternalFile` (N8)** ที่เขียน row `files` ระหว่าง render ของหน้า `/note` และ `/note/annotate` (entry point ทั้งสองของสัญญาลิงก์ `?fileUrl=`) แล้ว `redirect` ออกทันที (หน้า `force-dynamic` + รายการไฟล์ไม่ได้ sync ผ่าน SSE — ไม่ต้อง publish event/`revalidatePath`) · เช่นกัน **กลุ่ม docver `app/api/documents/**`** (จัด version เอกสาร — `docs/PLAN-DOCVER.md`) เขียนตาราง `documents`/`document_versions` ผ่าน route handler ไม่ใช่ Server Action (ผู้เรียกเป็นระบบภายนอก stand-alone ไม่มี session ของแอป note — identity userId/userName, templateId และ docId (จาก claim fid) แกะจาก Bearer token ที่ขอจาก `POST /api/documents/token` ผ่าน `lib/docver-token.ts` ไม่รับจาก request payload อีกต่อไป) — ไม่มีหน้า note ใดอ่านตารางนั้น จึงไม่ต้อง publish event/`revalidatePath` +4. **เขียนผ่าน Server Actions เท่านั้น** (`app/actions/annotations.ts`) และ**หลังเขียน DB สำเร็จทุกครั้ง ต้อง publish event ผ่าน `lib/annotate-events.ts` + `revalidatePath` หน้า viewer และ summary ของ fileId นั้น** (helper `revalidateFilePages` — สั่ง browser ทิ้ง Router Cache ฝั่ง client, `force-dynamic` ปิดแค่ฝั่ง server ไม่พอ: ไม่งั้นกด back แล้วคลิกเข้าไฟล์ใหม่ยังได้ initial data เก่าจนต้อง hard refresh) / **อ่าน + sync ผ่าน SSE hook** (`hooks/useAnnotationRealtime.ts` เปิด `EventSource` ไป `/api/note/annotations/stream?fileId=`) — event เป็น DTO (camelCase + ISO string) จาก server ตรง ๆ — **ยกเว้น `registerExternalFile` (N8)** ที่เขียน row `files` ระหว่าง render ของหน้า `/note` และ `/note/annotate` (entry point ทั้งสองของสัญญาลิงก์ `?fileUrl=`) แล้ว `redirect` ออกทันที (หน้า `force-dynamic` + รายการไฟล์ไม่ได้ sync ผ่าน SSE — ไม่ต้อง publish event/`revalidatePath`) · เช่นกัน **กลุ่ม docver `app/api/documents/**`** (จัด version เอกสาร — `docs/PLAN-DOCVER.md`) เขียนตาราง `documents`/`document_versions` ผ่าน route handler ไม่ใช่ Server Action (ผู้เรียกเป็นระบบภายนอก stand-alone บอก userId/userName เองใน request ไม่มี session ของแอป note) — ไม่มีหน้า note ใดอ่านตารางนั้น จึงไม่ต้อง publish event/`revalidatePath` 5. **Timestamp ข้าม server→client→SSE เป็น ISO string เสมอ** (DTO types) — ห้ามส่ง Date object ตรง ๆ 6. **pdfjs-dist ต้อง `dynamic import()` ภายใน useEffect เท่านั้น** — ห้าม import top-level ใน module scope ที่ server evaluate และต้องตั้ง `GlobalWorkerOptions.workerSrc = withBasePath("/pdf.worker.min.mjs")` (postinstall `scripts/patch-pdf-worker.mjs` คัดลอกไฟล์ไป `public/note/` **แล้ว patch จุดเดียว: ถอด `if (scaledDim)` รอบ `textChunk.prevTransform = getCurrentTextTransform()` ให้อัปเดตทุก glyph** — pdf.js เดิมข้าม glyph กว้าง 0 อย่างสระ/วรรณยุกต์ไทย ทำให้ charSpacing ของ glyph เหล่านั้นสะสมเป็น gap จน `addFakeSpaces` แทรก " " กลางคำ "ฉะนั้ น" — bug ของ pdf.js ที่ upstream ยังไม่แก้; การ patch ทำบนโค้ด minified ด้วย pattern จำเพาะ ถ้าอัปเกรด pdfjs-dist แล้ว pattern ไม่ตรง postinstall จะ **fail ดัง ๆ โดยตั้งใจ** กัน patch หลุดเงียบ — test กัน regression: `tests/scripts/patch-pdf-worker.test.ts` + fixture `tests/fixtures/thai_letterspacing.pdf` — Docker build ต้องใช้ script นี้ด้วย ไม่ใช่ copyFileSync เปล่า ดู `docker/web.Dockerfile`) — ห้ามใช้ `new URL(..., import.meta.url)` 7. **InkLayer ใช้ SVG แบบไม่มี viewBox** — render ด้วย px จริง แปลง % → px จากขนาดที่ ResizeObserver วัด (viewBox จะทำให้ความหนาเส้นบิดเบี้ยว) @@ -35,13 +35,13 @@ ``` docker-compose.yml ← dev dependencies: `db` (judes-note-db — ชื่อ container/วอลุ่มเดียวกับคำสั่ง docker run เดิมใน README migrate ได้เลย, POSTGRES_DB ecourt-note) + `minio` (judes-note-minio — object storage ของกลุ่ม docver; โฮสต์แมพ 19000 API/19001 console เพราะ 9000/9001 ชน container โปรเจกต์อื่นในเครื่อง dev, image จาก quay.io ทางการ; bucket `docver` ไม่ต้องสร้างเอง app สร้างตอน first use ผ่าน ensureBucket) -docs/PLAN-DOCVER.md ← แผนกลุ่ม API docver: จัด version เอกสาร .docx+.pdf ต่อ docId (ห้ามซ้ำ) เก็บ object ใน MinIO + ผู้อัปโหลดต่อ version — contract 4 endpoint (เพิ่ม POST /token ขอ access_token — Bearer บังคับทุก endpoint), ดีไซน์ตาราง, env MINIO_* + DOCVER_API_KEY/DOCVER_TOKEN_SECRET/DOCVER_TOKEN_TTL_SECONDS ทั้งหมด +docs/PLAN-DOCVER.md ← แผนกลุ่ม API docver: จัด version เอกสาร .docx+.pdf ต่อ docId (ห้ามซ้ำ) เก็บ object ใน MinIO + ผู้อัปโหลดต่อ version — contract 3 endpoint, ดีไซน์ตาราง, env MINIO_* ทั้งหมด scripts/patch-pdf-worker.mjs ← postinstall: คัดลอก pdf.worker.min.mjs จาก node_modules ไป public/note/ + patch fake-space ไทย (invariant #6 — เจอ pattern ≠ 1 จุดหลังอัปเกรด pdfjs-dist = exit 1 ดัง ๆ) scripts/copy-swagger-ui.mjs ← postinstall ตัวที่สอง: คัดลอก swagger-ui-dist (bundle+css) ไป public/note/swagger-ui/ สำหรับหน้า Swagger UI ที่ GET /api/swagger (intranet ไม่ใช้ CDN — หาไฟล์ไม่เจอหลังอัปเกรด = exit 1 ดัง ๆ เหมือน patch-pdf-worker; Docker build รัน script นี้ด้วย ดู docker/web.Dockerfile) scripts/fetch-office-js.mjs ← postinstall ตัวที่สาม: ดาวน์โหลด Office.js runtime **ฉบับ CDN ล่าสุด** (appsforoffice.microsoft.com — ชุดไฟล์เฉพาะ Word: loader + host files ราย platform + legacy 15.x + o15apptofilemappingtable/microsoftajax + office_strings.js ต่อ locale en-us/th-th บังคับ) ไป public/wordaddin/office-js/ ให้ taskpane โหลด same-origin — npm `@microsoft/office-js` เลิกอัปเดตที่ 1.1.110 (~2018) ไม่มี `Document.close`/enum `CloseBehavior` ที่ runtime ใหม่+`@types/office-js` มี; ไฟล์หลักหาย/ตรวจไม่เจอ `"Document.close"`+`CloseBehavior` ใน word-win32-16.01.js หลังโหลด = exit 1 ดัง ๆ (ไฟล์ปลายทาง gitignore — ดาวน์โหลดใหม่ทุกครั้ง; ติดตั้งต้องออกเน็ตถึง CDN ของ Microsoft ส่วน runtime ตัวระบบ intranet ล้วน) db/schema.ts ← ตาราง + JSONB types + DTO + mapper toDTO db/index.ts ← postgres.js + drizzle (DATABASE_URL → container judes-note-db, prepare:false) -middleware.ts ← บังคับ login ทุกหน้า — ไม่มี cookie oacCase ที่ parse ได้ = redirect /note/login (path ที่ตัด basePath แล้ว: หน้าขึ้นต้น /note/… ส่วน API ขึ้นต้น /api/note/… — ยกเว้น note/login, api/note/*, api/template/* (กลุ่ม docx template เปิดกว้างให้ทางศาล/ผู้ทดสอบภายนอกยิงได้เลย — ไม่แตะ DB), api/documents/* (กลุ่ม docver จัด version เอกสาร — stand-alone ไม่มี session ใช้ auth ของกลุ่มเอง: ขอ access_token ผ่าน x-api-key แล้วแนบ Bearer ทุก endpoint, docs/PLAN-DOCVER.md), api/swagger/* (หน้า Swagger UI), wordaddin/* (taskpane ของ Word add-in — auth mock ของตัวเอง), note/uploads/*, note/samples, note/swagger-ui/*, note/pdf.worker.min.mjs, _next, fonts, favicon และ **root "/" เอง** (matcher ใช้ `.+` — root เป็น redirect สู่ /wordaddin ซึ่ง public ตามคำขอผู้ใช้ 2026-10-14, ลิงก์ e-Court N8 ที่ root ยังถูกคุมผ่าน /note ที่รับส่งต่อ)) +middleware.ts ← บังคับ login ทุกหน้า — ไม่มี cookie oacCase ที่ parse ได้ = redirect /note/login (path ที่ตัด basePath แล้ว: หน้าขึ้นต้น /note/… ส่วน API ขึ้นต้น /api/note/… — ยกเว้น note/login, api/note/*, api/template/* (กลุ่ม docx template เปิดกว้างให้ทางศาล/ผู้ทดสอบภายนอกยิงได้เลย — ไม่แตะ DB), api/documents/* (กลุ่ม docver จัด version เอกสาร — stand-alone ผู้เรียกบอก userId/userName เอง ไม่มี session, docs/PLAN-DOCVER.md), api/swagger/* (หน้า Swagger UI), wordaddin/* (taskpane ของ Word add-in — auth mock ของตัวเอง), note/uploads/*, note/samples, note/swagger-ui/*, note/pdf.worker.min.mjs, _next, fonts, favicon และ **root "/" เอง** (matcher ใช้ `.+` — root เป็น redirect สู่ /wordaddin ซึ่ง public ตามคำขอผู้ใช้ 2026-10-14, ลิงก์ e-Court N8 ที่ root ยังถูกคุมผ่าน /note ที่รับส่งต่อ)) lib/annotate-events.ts ← pub/sub in-process (actions publish → SSE route subscribe) lib/server-pdf.ts ← path guard อ่าน PDF จาก public/note/ (export ใช้) lib/session.ts ← getSessionUser — identity จาก cookie oacCase (mock login e-Court) @@ -50,8 +50,7 @@ lib/remote-file.ts ← N8 ดึงไฟล์รีโมตจาก allow lib/external-api-auth.ts ← N10 auth ของ /api/note/external/* — requireExternalApiKey เทียบ header `x-api-key` กับ env `EXTERNAL_API_KEY` ด้วย SHA-256 + crypto.timingSafeEqual (env ไม่ตั้ง/ว่าง = 401 fail closed ทุก request; ข้อความ 401 อันเดียวกันทั้งไม่ใส่/key ผิด กัน enumerate; ห้าม log ค่า key/header ดิบ) + externalApiError สร้าง Response ผิดพลาด JSON {"error": ไทย} ใช้ร่วมทุก route ของ N10 — **module นี้ห้าม import อะไรที่ลาก @/db มาด้วย** (db/index.ts throw ตอน import เมื่อไม่มี DATABASE_URL — unit test tests/lib/external-api-auth.test.ts ต้องรันได้โดยไม่แตะ DB) lib/external-api.ts ← N10 ส่วนกลางของ route — parseQueryBody (validate JSON body ของ /api/note/external/documents: รับเฉพาะ fileId/typeId/filter, ต้องมี fileId หรือ typeId อย่างน้อยหนึ่งอย่าง — ส่งทั้งคู่ได้ = AND เหลือเฉพาะเอกสารเดียวที่ตรงทั้งสองเงื่อนไข, คำขอผู้ใช้ 2026-09-25 (รอบเดียวกันเคยเป็น OR แล้วเปลี่ยนหลังทดสอบจริง), filter รับเฉพาะ type ∈ bookmarks|annotations ไม่ส่ง = ทั้งสอง + userId (string|array กรองตามเจ้าของรายการ annotations+bookmarks) + annotationTypes (ค่า enum annotation_type เฉพาะ annotations ค่าอื่น = 400 — เดิมชื่อ types เปลี่ยน 2026-09-25) — คืน discriminated mode "file" (fileId + typeId อาจมาด้วย) / "type" (typeId เดี่ยว)) + loadDocumentBundles(fileRows, parts) (bulk: inArray ทุกไฟล์ในชุด query เดียว = 3 query ต่อ request ไม่ว่ากลุ่มกี่ไฟล์ แทนยิง 3 query ต่อไฟล์พร้อมกันทั้งกลุ่ม, code review 2026-09-25 — กรอง userIds/annotationTypes ใน SQL; คืน bundle ยุบชั้น: field เอกสาร + annotations + bookmarks ระดับเดียวกัน ไม่มี object document ซ้อน; ต่อรายการมี `url` deep link F12 กลับหน้า `/annotate/[id]` — annotation `?ann=` / bookmark `?page=<หน้า 1-based>` — URL เต็ม: origin จาก request ผู้เรียก (route อ่าน x-forwarded-proto/host แล้ว fallback host ตรง ส่งเข้าทาง parameter origin) + basePath ผ่าน withBasePath, คำขอผู้ใช้ 2026-09-25) + type ExternalDocumentDTO (externalFileId เป็น null สำหรับไฟล์ url:-fallback/อัปโหลด N4 ในกลุ่ม) 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 + fields JSON ≤100 keys + 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): 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/document-versioning.ts ← docver pure validation (ห้าม import อะไรที่ลาก @/db หรือ minio มาด้วย — unit test tests/lib/document-versioning.test.ts ต้องรันได้เหมือน lib/external-api-auth.ts): parseDocId (charset [A-Za-z0-9._:-] 1–200), parseVersionUploadForm (allowlist field + userId/userName บังคับ + fields JSON ≤100 keys + message ≤2048), isDocxBytes/isPdfBytes (magic bytes), parseDownloadFormat/parseVersionIdParam, contentDispositionValue (ASCII fallback + filename*=UTF-8'') + docverError สร้าง Response JSON {"error": ไทย} ใช้ร่วมทุก route ของกลุ่ม 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 (mock auth) @@ -83,7 +82,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-26 ถอด segment `[docId]` ออกจาก URL ทุก endpoint — docId = claim `fid` (string) จาก token ตรง ๆ (บังคับตั้งแต่ขอ token)**): POST versions = multipart `file`(.docx บังคับ ≤100MB เช็ค size ก่อน buffer → 413)+`pdf`(optional ไฟล์เปล่า = ไม่มี)+fields/message/clientVersionId/fileName → สร้างเอกสารอัตโนมัติถ้า 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/[docId]/versions/route.ts + [versionId]/download-link/route.ts ← **กลุ่ม API docver จัด version เอกสาร** (docs/PLAN-DOCVER.md — URL /docedit/api/documents/…, ไม่มี auth เปิดกว้างเหมือน api/template): POST versions = multipart `file`(.docx บังคับ ≤100MB เช็ค size ก่อน buffer → 413)+`pdf`(optional ไฟล์เปล่า = ไม่มี)+userId/userName+fields/message/clientVersionId/templateId/fileName → สร้างเอกสารอัตโนมัติถ้า 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/app/api/documents/versions/[versionId]/download-link/route.ts b/app/api/documents/[docId]/versions/[versionId]/download-link/route.ts similarity index 65% rename from app/api/documents/versions/[versionId]/download-link/route.ts rename to app/api/documents/[docId]/versions/[versionId]/download-link/route.ts index b97ce00..0b91a6c 100644 --- a/app/api/documents/versions/[versionId]/download-link/route.ts +++ b/app/api/documents/[docId]/versions/[versionId]/download-link/route.ts @@ -1,13 +1,12 @@ -// PLAN-DOCVER.md §1 — GET /api/documents/versions/{versionId}/download-link?format=docx|pdf +// PLAN-DOCVER.md §1 — GET /api/documents/{docId}/versions/{versionId}/download-link?format=docx|pdf // คืน presigned URL อายุสั้น (env DOCVER_LINK_TTL_SECONDS) ให้ UI เปิดในเบราว์เซอร์จริง -// ของผู้ใช้ — เช็ค docId (จาก fid ใน token) กับ versionId คู่กันกันโยน id ข้ามเอกสาร -// auth: Authorization: Bearer (access_token จาก POST /api/documents/token — ทุก endpoint ของกลุ่ม) +// ของผู้ใช้ — เช็คทั้ง docId และ versionId พร้อมกันกันโยน id ข้ามเอกสาร import { docverError, + parseDocId, parseDownloadFormat, parseVersionIdParam, } from "@/lib/document-versioning"; -import { requireDocverBearer } from "@/lib/docver-token"; import { findVersionForDoc, presignVersionUrl } from "@/lib/docver-store"; export const runtime = "nodejs"; @@ -15,13 +14,10 @@ export const dynamic = "force-dynamic"; export async function GET( request: Request, - { params }: { params: { versionId: string } }, + { params }: { params: { docId: string; versionId: string } }, ) { - const auth = requireDocverBearer(request); - if (!auth.ok) return auth.response; - - // docId มาจาก fid ใน token — claim ถูกบังคับเป็น string ไม่ว่างตั้งแต่ขอ token + verify - const docId = auth.claims.fid; + const docId = parseDocId(params.docId); + if (!docId.ok) return docverError(docId.error, 400); const versionId = parseVersionIdParam(params.versionId); if (!versionId.ok) return docverError(versionId.error, 400); let format: string | null = null; @@ -34,7 +30,7 @@ export async function GET( if (!parsedFormat.ok) return docverError(parsedFormat.error, 400); try { - const row = await findVersionForDoc(docId, versionId.versionId); + const row = await findVersionForDoc(docId.docId, versionId.versionId); if (!row) { return docverError("ไม่พบ version นี้ของเอกสาร", 404); } diff --git a/app/api/documents/versions/route.ts b/app/api/documents/[docId]/versions/route.ts similarity index 81% rename from app/api/documents/versions/route.ts rename to app/api/documents/[docId]/versions/route.ts index 79dcc2b..d022100 100644 --- a/app/api/documents/versions/route.ts +++ b/app/api/documents/[docId]/versions/route.ts @@ -1,9 +1,6 @@ -// PLAN-DOCVER.md §1 — POST อัปโหลด version ใหม่ + GET history ของ /api/documents/versions -// กลุ่ม API stand-alone ไม่มี session — auth ด้วย access_token ของกลุ่มนี้: ขอที่ -// POST /api/documents/token (ยืนยัน x-api-key กับ env DOCVER_API_KEY) แล้วแนบ -// Authorization: Bearer ทุก request — identity (userid/username), templateId และ -// docId (= fid — fileId ของ e-Court, string) แกะจาก claims ของ token ทั้งหมด -// ไม่รับจาก URL/form อีก (lib/docver-token.ts) +// PLAN-DOCVER.md §1 — POST อัปโหลด version ใหม่ + GET history ของ /api/documents/{docId}/versions +// กลุ่ม API stand-alone ไม่มี auth (เปิดกว้างเหมือน api/template — intranet) — ผู้เรียกบอก +// identity เองผ่าน field userId/userName เพราะไม่มี session ร่วมกับใคร import { createHash, randomUUID } from "node:crypto"; import { toDocumentVersionDTO } from "@/db/schema"; @@ -13,10 +10,10 @@ import { DOCX_CONTENT_TYPE, isDocxBytes, isPdfBytes, + parseDocId, parseVersionUploadForm, sanitizeFilePartName, } from "@/lib/document-versioning"; -import { requireDocverBearer } from "@/lib/docver-token"; import { findDocumentByDocId, insertVersionRecord, @@ -30,16 +27,13 @@ export const runtime = "nodejs"; export const dynamic = "force-dynamic"; /** POST — อัปโหลด version ใหม่ (เอกสารสร้างแบบ implicit ถ้าเป็น version แรก) - * ลำดับ: Bearer token → validate text field ก่อนแตะไฟล์ → cap ขนาดก่อน buffer → magic bytes → + * ลำดับ: validate text field ก่อนแตะไฟล์ → cap ขนาดก่อน buffer → magic bytes → * putObject MinIO → insert DB (tx) — DB ล้ม = เก็บกวาด object (pattern N4) * response = UploadVersionResult เป๊ะ โดย versionNumber เป็นค่า authoritative * จาก server (client เดา latest+1 แล้วแก้เองเมื่ออัปโหลดชนกัน) */ -export async function POST(request: Request) { - const auth = requireDocverBearer(request); - if (!auth.ok) return auth.response; - - // docId มาจาก fid ใน token — claim ถูกบังคับเป็น string ไม่ว่างตั้งแต่ขอ token + verify - const docId = auth.claims.fid; +export async function POST(request: Request, { params }: { params: { docId: string } }) { + const docId = parseDocId(params.docId); + if (!docId.ok) return docverError(docId.error, 400); let form: FormData; try { @@ -115,19 +109,19 @@ export async function POST(request: Request) { try { row = await insertVersionRecord({ versionId, - docId, + docId: docId.docId, clientVersionId: parsed.form.clientVersionId, contentHash, message: parsed.form.message, fields: parsed.form.fields, - templateId: auth.claims.template, + templateId: parsed.form.templateId, fileName: docxDisplayName, docxObjectKey: keys.docx, pdfObjectKey: pdfBytes ? keys.pdf : null, docxSize: docxBytes.length, pdfSize: pdfBytes?.length ?? null, - userId: auth.claims.userid, - userName: auth.claims.username, + userId: parsed.form.userId, + userName: parsed.form.userName, }); } catch (err) { // DB ล้มหลังเขียน object — เก็บกวาดก่อนตอบ กัน object กำพร้าไม่มี row ชี้ @@ -148,14 +142,12 @@ export async function POST(request: Request) { /** GET — history ของเอกสาร (ใหม่ → เก่า) · docId ที่ยังไม่เคยมี version = 404 * (สัญญาณ REST ตรง ๆ แบบ N10 — UI ฝั่ง add-in แสดง error + ปุ่มลองใหม่ ไม่พัง) */ -export async function GET(request: Request) { - const auth = requireDocverBearer(request); - if (!auth.ok) return auth.response; - - const docId = auth.claims.fid; +export async function GET(_request: Request, { params }: { params: { docId: string } }) { + const docId = parseDocId(params.docId); + if (!docId.ok) return docverError(docId.error, 400); try { - const doc = await findDocumentByDocId(docId); + const doc = await findDocumentByDocId(docId.docId); if (!doc) { return docverError("ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยอัปโหลด version)", 404); } diff --git a/app/api/documents/token/route.ts b/app/api/documents/token/route.ts deleted file mode 100644 index 07cd544..0000000 --- a/app/api/documents/token/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -// PLAN-DOCVER.md §auth — POST /api/documents/token ออก access_token ของกลุ่ม docver -// ยืนยันด้วย header "x-api-key" เทียบ env DOCVER_API_KEY (fail closed เมื่อ env ไม่ -// ตั้ง/ว่าง) — token เป็น JWT HS256 เซ็นด้วย env DOCVER_TOKEN_SECRET อายุตาม env -// DOCVER_TOKEN_TTL_SECONDS (default 3600) แล้วนำไปแนบ Authorization: Bearer กับ -// endpoint อื่นของกลุ่ม (lib/docver-token.ts) · segment คงที่ "token" ไม่ชน [docId] -// (Next ให้ static ชนะ) และ middleware ยกเว้น api/documents/* อยู่แล้ว -import { docverError } from "@/lib/document-versioning"; -import { - issueDocverToken, - parseTokenRequestBody, - requireDocverApiKey, -} from "@/lib/docver-token"; - -export const runtime = "nodejs"; -export const dynamic = "force-dynamic"; - -export async function POST(request: Request) { - 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); - - // ความผิด config ฝั่ง server ไม่ใช่ความผิดของผู้เรียก — ไม่ตั้ง = 500 (ห้าม log ค่า secret) - const secret = process.env.DOCVER_TOKEN_SECRET?.trim(); - // const secret = - // "487023dd1c1c4d6a07475e96b1cc300fec1e68ac9c2dd7d5ac9b1bf75da1053e"; - - if (secret === undefined || secret === "") { - return docverError("DOCVER_TOKEN_SECRET ยังไม่ถูกตั้งค่า", 500); - } - const ttlRaw = Number(process.env.DOCVER_TOKEN_TTL_SECONDS); - const ttlSeconds = - Number.isFinite(ttlRaw) && ttlRaw > 0 ? Math.floor(ttlRaw) : 3600; - - const accessToken = issueDocverToken(parsed.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/template/download-docx/route.ts b/app/api/template/download-docx/route.ts deleted file mode 100644 index 13f8725..0000000 --- a/app/api/template/download-docx/route.ts +++ /dev/null @@ -1,108 +0,0 @@ -// ตัวอย่างรองรับค่าจาก Frontend ที่เป็น Vanilla JavaScript และ Classic Framework -// โดย ผ่าน Form หรือ การกดลิงค์(GET) เปลี่ยนหน้าเพื่อดาว์นโหลดไฟล์ -// จะมีข้อมูล info.ticket_data ที่สมบูรณ์แล้ว({fid,tid,uid,uname})แล้วเเรียก /api/template/generate เพื่อสร้างเอกสาร docx -import { NextRequest, NextResponse } from 'next/server'; -import path from 'path'; -import fs from 'fs/promises'; - -const DOCEDIT_BASE_URL = process.env.DOCEDIT_BASE_URL??""; -const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY??""; -const TMPL_PATH = process.env.TMPL_PATH??""; -if (!DOCEDIT_BASE_URL||!EXTERNAL_API_KEY||!TMPL_PATH) { - console.error('Missing variable DOCEDIT_BASE_URL, EXTERNAL_API_KEY or TMPL_PATH') -} - - -async function processRequest(fid: string,tid:string,uid:string,uname:string):Promise> -{ - // console.log("processRequest get:",fid,tid,uid,uname) - // console.log(process.cwd()) - const filePath = path.join(TMPL_PATH,`${fid}.json`); - try { - if (!fid||!tid||!uid||!uname) { - return NextResponse.json({ error: 'Missing fid, tid, uid or uname' }, { status: 400 }); - } - - // อ่านไฟล์ JSON จำลองการอ่านจากฐานข้อมูล - const rawJson = await fs.readFile(filePath, 'utf-8'); - const jsonData = JSON.parse(rawJson); - if (!jsonData || typeof jsonData !== 'object' ) { - return NextResponse.json({error:'Invalid data format'}, { status: 400 }); - } - - //!! ตรวจสอบสิทธิ์ผู้ใช้ uid ว่ามีสิทธิ์เข้าถึงไฟล์ fid หรือไม่เพื่อกันการการยิง API ตรงๆ ไม่ผ่าน Frontend!! - - // ข้อมูลจำเป็นต้องมี prop นี้เสมอถ้าไม่มีให้สร้าง jsonData.info.ticket_data - if(!Object.hasOwn(jsonData, 'info')) - jsonData['info']={} - jsonData.info['ticket_data'] = JSON.stringify({fid,tid,uid,uname}) - // sign เองตรงนี้ให้อยู่ในรูปแบบ data.info={ticket_data,HmacSha256,Ed25519} หรือว่างไว้ให้ docedit สร้างให้ - - //ยิงไปหาระบบ Docedit เพื่อเอา Stream - // console.log(`fetch ${DOCEDIT_BASE_URL}/api/template/generate`) - const docxResponse = await fetch(`${DOCEDIT_BASE_URL}/api/template/generate`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': EXTERNAL_API_KEY, - }, - body: JSON.stringify(jsonData), - }); - - // เช็ค Content-Type และ Status ก่อน - const contentType = docxResponse.headers.get('content-type') || ''; - - if (!docxResponse.ok || contentType.includes('text/html')) { - const errorText = await docxResponse.text(); - console.error('download-docx:Invalid docx Response:', errorText); - - return NextResponse.json( - {error:`Error from docedit generate: Expected docx stream but received ${contentType} (Status ${docxResponse.status})`}, - { status: 502 } - ); - } - - // 2. ดึง ReadableStream โดยตรงจาก Response ของระบบ - const stream = docxResponse.body; - - // 3. ส่ง Binary Stream กลับไปยัง Browser พร้อม Header กำหนดประเภทไฟล์ - const download_file = `${fid}_${uid}_xxx` - return new NextResponse(stream as unknown as BodyInit, { - status: 200, - headers: { - 'Content-Type': - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'Content-Disposition': `attachment; filename="${encodeURIComponent(download_file)}.docx"`, - 'Cache-Control': 'no-store, max-age=0', - }, - }); - } catch (error:any) { - if ( error.code === 'ENOENT') { - return NextResponse.json( - { error: `Data not found: ${filePath}`}, - { status: 404 } - ); - } - console.error(error) - return NextResponse.json({error:'Internal Server Error'}, { status: 500 }); - } -} - -export async function GET(request: NextRequest) -{ - const searchParams = request.nextUrl.searchParams; - const fid = searchParams.get('fid') || ''; - const tid = searchParams.get('tid') || ''; - const uid = searchParams.get('uid') || ''; - const uname = searchParams.get('uname') || ''; - return await processRequest(fid,tid,uid,uname) -} - -export async function POST(request: NextRequest) { - const formData = await request.formData(); - const fid = String(formData.get('fid')) - const tid = String(formData.get('tid')) - const uid = String(formData.get('uid')) - const uname = String(formData.get('uname')) - return await processRequest(fid,tid,uid,uname) -} \ No newline at end of file diff --git a/app/api/template/extract/route.ts b/app/api/template/extract/route.ts index 12108b7..dc5b49c 100644 --- a/app/api/template/extract/route.ts +++ b/app/api/template/extract/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; -import { extractContentControls, extractBookmark, templateContentControls, setCustomDocProps, getDocProps,templateDynamicContents } from "@/lib/lib-tmpl"; -import { verifyEd25519, verifyHmacSha256 } from "@/lib/lib-utils"; -// export const runtime = "nodejs"; +import { extractContentControls } from "../lib-tmpl"; + +export const runtime = "nodejs"; const DOCX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; @@ -14,14 +14,24 @@ export async function POST(request: Request) { try { const formData = await request.formData(); const file = formData.get("file"); - const TEST_SECRET = String(formData.get("secret") ?? ""); - const TEST_PUBLIC_KEY_HEX = String(formData.get("public_key") ?? ""); - if (!(file instanceof File) || - (file.type !== DOCX_CONTENT_TYPE && !file.name.toLowerCase().endsWith(".docx") ) + if (!(file instanceof File)) { + return NextResponse.json( + { + success: false, + error: 'Missing DOCX file in field "file".', + }, + { status: 400 }, + ); + } + + if ( + file.type !== DOCX_CONTENT_TYPE && + !file.name.toLowerCase().endsWith(".docx") ) { return NextResponse.json( { + success: false, error: 'Field "file" must be a .docx file.', }, { status: 400 }, @@ -31,6 +41,7 @@ export async function POST(request: Request) { if (file.size > MAX_UPLOAD_BYTES) { return NextResponse.json( { + success: false, error: 'Field "file" exceeds the 100MB size limit.', }, { status: 400 }, @@ -41,36 +52,18 @@ export async function POST(request: Request) { const buffer = Buffer.from(await file.arrayBuffer()); // ใช้ lib-tmpl.ts เดิม - // const data = extractContentControls(buffer); + const data = extractContentControls(buffer); - const cc = extractContentControls(buffer); - const bookmark = extractBookmark(buffer) - const properties = getDocProps(buffer) - let verify: Record ={} - const ticket_data = properties?.custom?.ticket_data - const HmacSha256 = String(properties?.custom?.HmacSha256) - const Ed25519 = String(properties?.custom?.Ed25519) - - if(ticket_data && typeof ticket_data === 'string'){ - const ticketDataObj= JSON.parse(ticket_data) - const unix_timestamp = Number(ticketDataObj.exp)*1000 - if(unix_timestamp){ - verify['exp'] = new Date(unix_timestamp).toLocaleString("th-TH", { timeZone: "Asia/Bangkok" }) - } - if(TEST_SECRET && HmacSha256){ - verify['HmacSha256'] = verifyHmacSha256(ticket_data,TEST_SECRET,HmacSha256) - } - if(TEST_PUBLIC_KEY_HEX&& Ed25519){ - verify["Ed25519"]=verifyEd25519(ticket_data,Ed25519,TEST_PUBLIC_KEY_HEX) - } - } - - let returnData : Record = {cc,bookmark,properties,verify} - return NextResponse.json(returnData); + return NextResponse.json({ + success: true, + data, + }); } catch (error) { console.error(error); + return NextResponse.json( { + success: false, error: error instanceof Error ? error.message diff --git a/app/api/template/generate/route.ts b/app/api/template/generate/route.ts deleted file mode 100644 index 1a057ee..0000000 --- a/app/api/template/generate/route.ts +++ /dev/null @@ -1,101 +0,0 @@ -// สร้าง docx จาก json -// - json.info.ticket_data จะมี tid ให้โหลดไฟล์ template นี้มาใช้ -// - สร้าง signature/hash จาก json.info.ticket_data - -import { NextRequest, NextResponse } from 'next/server'; -import {readFile} from 'fs/promises'; -import path from 'path'; -import {DocumentTemplate,applyTemplate} from "@/lib/lib-tmpl"; -const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY || 'secret-api-key-1234'; -const TMPL_PATH = process.env.TMPL_PATH || '' -const PRIVATE_KEY_HEX = process.env.PRIVATE_KEY_HEX ||'' -const SECRET = process.env.SECRET || '' -const TICKET_SEC = Number(process.env.TICKET_SEC||'300') - -if (EXTERNAL_API_KEY || !TMPL_PATH || !PRIVATE_KEY_HEX || !SECRET) - console.log("api/template/generate: require EXTERNAL_API_KEY, TMPL_PATH and PRIVATE_KEY_HEX") - -export async function POST(request: NextRequest) { - // 1. ตรวจสอบ API Key จาก HTTP Header (x-api-key) - const apiKey = request.headers.get('x-api-key'); - - if (!apiKey || apiKey !== EXTERNAL_API_KEY) { - return NextResponse.json( - { error: 'Unauthorized: Invalid API Key' }, - { status: 401 } - ); - } - - try { - // รับข้อมูล JSON Payload - const templateData:DocumentTemplate = (await request.json()); - console.log(templateData) - // ต้องมี templateData.info.ticket_data เป็น json string เสมอ - if (!templateData || !templateData.info || - !templateData.info.ticket_data || - typeof templateData.info.ticket_data !== "string" - ) { - return NextResponse.json( - { - error: "template/generate json.info.ticket_data not avaliable", - }, - { status: 400 }, - ); - } - const ticketObj = JSON.parse(templateData.info.ticket_data) - const tid = String(ticketObj.tid) - const fid = String(ticketObj.fid) - const uid = String(ticketObj.uid) - if(!tid||!fid||!uid) - { - return NextResponse.json( - { - error: "template/generate require json.info.ticket_data.(fid|tid|uid)", - }, - { status: 400 }, - ); - } - - - //เปิดไฟล์ template - const tmplPath = path.join(TMPL_PATH, `${tid}.docx`); - let templateBuffer: Buffer = await readFile(tmplPath) - - const output = await applyTemplate(templateBuffer, templateData, SECRET, PRIVATE_KEY_HEX, TICKET_SEC); - - // สร้าง ReadableStream ของไฟล์ .docx - // ในโค้ดจริง ส่วนนี้จะดึง Binary Buffer จาก Library เช่น docx, docxtemplater หรืออ่านจาก File System - // const encoder = new TextEncoder(); - - const docxStream = new ReadableStream({ - start(controller) { - controller.enqueue(output); - controller.close(); - }, - }); - - // 4. ส่ง Return เป็น Response พร้อม Stream และ Headers กำหนด MIME type ของ MS Word - const dlFileName = encodeURIComponent(`${tid}_${fid}_${uid}XX`) - return new NextResponse(docxStream, { - status: 200, - headers: { - 'Content-Type': - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'Content-Disposition': `attachment; filename="${dlFileName}.docx"`, - 'Cache-Control': 'no-store, max-age=0', - }, - }); - } catch (error:any) { - if ( error.code === 'ENOENT') { - return NextResponse.json( - { error: 'NotFound: Invalid template ID or incorrect storage path'}, - { status: 404 } - ); - } - console.error('Error generating docx:', error); - return NextResponse.json( - { error: 'Failed to generate document payload'}, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/template/lib-tmpl.ts b/app/api/template/lib-tmpl.ts new file mode 100644 index 0000000..2c15f52 --- /dev/null +++ b/app/api/template/lib-tmpl.ts @@ -0,0 +1,518 @@ +import PizzaZip from "pizzip"; +import { + DOMParser, + XMLSerializer, + type Document as XmlDocument, + type Element as XmlElement, + type Node as XmlNode, + type Attr as XmlAttr, +} from "@xmldom/xmldom"; +import xpath from "xpath"; + +export interface ExtractedData { + [tagName: string]: string; +} + +const W_NS = + "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; + +const asXpathNode = (node: XmlNode): globalThis.Node => + node as unknown as globalThis.Node; + +function createSelector() { + return xpath.useNamespaces({ w: W_NS }); +} + +/** + * อ่าน Content Control (w:sdt) จาก DOCX Buffer + * ปัจจุบันอ่านจาก word/document.xml ตาม logic ของโค้ดต้นฉบับ + */ +export function extractContentControls( + docxBuffer: Buffer, +): ExtractedData { + const zip = new PizzaZip(docxBuffer); + const docXmlFile = zip.file("word/document.xml"); + + if (!docXmlFile) { + throw new Error("Invalid docx file: word/document.xml not found."); + } + + const parser = new DOMParser(); + const doc = parser.parseFromString(docXmlFile.asText(), "text/xml"); + const select = createSelector(); + const result: ExtractedData = {}; + + const sdtNodes = select( + "//w:sdt", + asXpathNode(doc), + ) as unknown as XmlElement[]; + + sdtNodes.forEach((sdt) => { + const tagAttr = select( + "./w:sdtPr/w:tag/@w:val", + asXpathNode(sdt), + true, + ) as unknown as XmlAttr | undefined; + + const tagName = tagAttr?.value; + + if (!tagName) return; + + const sdtContent = select( + "./w:sdtContent", + asXpathNode(sdt), + true, + ) as unknown as XmlElement | undefined; + + if (!sdtContent) { + result[tagName] = ""; + return; + } + + const nodes = select( + ".//w:p | .//w:t | .//w:tab | .//w:br | .//w:noBreakHyphen | .//w:softHyphen | .//w:sym", + asXpathNode(sdtContent), + ) as unknown as XmlElement[]; + + let extractedText = ""; + + nodes.forEach((node) => { + switch (node.nodeName) { + case "w:p": + if ( + extractedText.length > 0 && + !extractedText.endsWith("\n") + ) { + extractedText += "\n"; + } + break; + + case "w:t": + extractedText += node.textContent || ""; + break; + + case "w:tab": + extractedText += "\t"; + break; + + case "w:br": + extractedText += "\n"; + break; + + case "w:noBreakHyphen": + extractedText += "\u2011"; + break; + + case "w:softHyphen": + extractedText += "\u00ad"; + break; + + case "w:sym": { + const charAttr = node.getAttribute("w:char"); + if (charAttr) { + const charCode = Number.parseInt(charAttr, 16); + extractedText += String.fromCharCode(charCode); + } + break; + } + } + }); + + result[tagName] = extractedText; + }); + + return result; +} + +type SdtControlType = "plain" | "rich" | "other"; +type SdtStructure = "block" | "inline"; + +function detectSdtControlType( + sdt: XmlElement, + select: xpath.XPathSelect, +): { type: SdtControlType; multiLine: boolean } { + const sdtPr = select( + "./w:sdtPr", + asXpathNode(sdt), + true, + ) as unknown as XmlElement | undefined; + + if (!sdtPr) { + return { type: "other", multiLine: false }; + } + + const textEl = select( + "./w:text", + asXpathNode(sdtPr), + true, + ) as unknown as XmlElement | undefined; + + if (textEl) { + const multiLineAttr = select( + "./@w:multiLine", + asXpathNode(textEl), + true, + ) as unknown as XmlAttr | undefined; + + return { + type: "plain", + multiLine: + multiLineAttr?.value === "1" || + multiLineAttr?.value === "true", + }; + } + + const richTextEl = select( + "./w:richText", + asXpathNode(sdtPr), + true, + ) as unknown as XmlElement | undefined; + + if (richTextEl) { + return { type: "rich", multiLine: true }; + } + + const otherTypeTags = [ + "w:date", + "w:dropDownList", + "w:comboBox", + "w:checkbox", + "w:citation", + "w:group", + "w:docPartObj", + "w:docPartList", + ]; + + for (const tag of otherTypeTags) { + const found = select( + `./${tag}`, + asXpathNode(sdtPr), + true, + ) as unknown as XmlElement | undefined; + + if (found) { + return { type: "other", multiLine: false }; + } + } + + return { type: "rich", multiLine: true }; +} + +function detectSdtStructure( + sdtContent: XmlElement, + select: xpath.XPathSelect, +): SdtStructure { + const directP = select( + "./w:p", + asXpathNode(sdtContent), + ) as unknown as XmlElement[]; + + return directP.length > 0 ? "block" : "inline"; +} + +function buildRunChildren( + doc: XmlDocument, + text: string, +): XmlElement[] { + const nodes: XmlElement[] = []; + const tokens = text.split(/(\t|\n)/); + + tokens.forEach((token) => { + if (token === "") return; + + if (token === "\t") { + nodes.push(doc.createElementNS(W_NS, "w:tab")); + } else if (token === "\n") { + nodes.push(doc.createElementNS(W_NS, "w:br")); + } else { + const t = doc.createElementNS(W_NS, "w:t"); + t.setAttribute("xml:space", "preserve"); + t.textContent = token; + nodes.push(t); + } + }); + + if (nodes.length === 0) { + const t = doc.createElementNS(W_NS, "w:t"); + t.setAttribute("xml:space", "preserve"); + t.textContent = ""; + nodes.push(t); + } + + return nodes; +} + +function buildRun( + doc: XmlDocument, + text: string, + rPr?: XmlElement, +): XmlElement { + const run = doc.createElementNS(W_NS, "w:r"); + + if (rPr) { + run.appendChild(rPr.cloneNode(true)); + } + + buildRunChildren(doc, text).forEach((node) => + run.appendChild(node), + ); + + return run; +} + +function insertInlineContent( + doc: XmlDocument, + sdtContent: XmlElement, + select: xpath.XPathSelect, + text: string, + allowLineBreak: boolean, +): void { + const safeText = allowLineBreak + ? text + : text.replace(/\n/g, " "); + + const existingRuns = select( + "./w:r", + asXpathNode(sdtContent), + ) as unknown as XmlElement[]; + + const firstRPr = + existingRuns.length > 0 + ? (select( + "./w:rPr", + asXpathNode(existingRuns[0]), + true, + ) as unknown as XmlElement | undefined) + : undefined; + + existingRuns.forEach((r) => { + if (r.parentNode) r.parentNode.removeChild(r); + }); + + sdtContent.appendChild( + buildRun(doc, safeText, firstRPr), + ); +} + +function insertBlockContent( + doc: XmlDocument, + sdtContent: XmlElement, + select: xpath.XPathSelect, + text: string, + allowMultiParagraph: boolean, +): void { + const allParagraphs = select( + "./w:p", + asXpathNode(sdtContent), + ) as unknown as XmlElement[]; + + if (allParagraphs.length === 0) { + // ป้องกัน template ที่มีโครงสร้างผิดปกติ + const p = doc.createElementNS(W_NS, "w:p"); + sdtContent.appendChild(p); + allParagraphs.push(p); + } + + const firstP = allParagraphs[0]; + + const firstPPr = select( + "./w:pPr", + asXpathNode(firstP), + true, + ) as unknown as XmlElement | undefined; + + const firstRun = select( + "./w:r", + asXpathNode(firstP), + true, + ) as unknown as XmlElement | undefined; + + const firstRPr = firstRun + ? (select( + "./w:rPr", + asXpathNode(firstRun), + true, + ) as unknown as XmlElement | undefined) + : undefined; + + allParagraphs.forEach((p) => { + if (p !== firstP && p.parentNode) { + p.parentNode.removeChild(p); + } + }); + + const existingRuns = select( + "./w:r", + asXpathNode(firstP), + ) as unknown as XmlElement[]; + + existingRuns.forEach((r) => { + if (r.parentNode) r.parentNode.removeChild(r); + }); + + if (!allowMultiParagraph) { + firstP.appendChild(buildRun(doc, text, firstRPr)); + return; + } + + text.split("\n").forEach((line, index) => { + let targetP: XmlElement; + + if (index === 0) { + targetP = firstP; + } else { + targetP = doc.createElementNS(W_NS, "w:p"); + + if (firstPPr) { + targetP.appendChild(firstPPr.cloneNode(true)); + } + + sdtContent.appendChild(targetP); + } + + const run = doc.createElementNS(W_NS, "w:r"); + + if (firstRPr) { + run.appendChild(firstRPr.cloneNode(true)); + } + + buildRunChildren(doc, line).forEach((node) => + run.appendChild(node), + ); + + targetP.appendChild(run); + }); +} + +function insertFallbackContent( + sdtContent: XmlElement, + select: xpath.XPathSelect, + text: string, +): void { + const textNodes = select( + ".//w:t", + asXpathNode(sdtContent), + ) as unknown as XmlElement[]; + + if (textNodes.length === 0) return; + + textNodes[0].textContent = text; + + for (let i = 1; i < textNodes.length; i++) { + textNodes[i].textContent = ""; + } +} + +export type TemplateData = Record; + +function normalizeValue(value: unknown): string { + if (value === null || value === undefined) { + return ""; + } + + if (typeof value === "string") { + return value; + } + + if ( + typeof value === "number" || + typeof value === "boolean" || + typeof value === "bigint" + ) { + return String(value); + } + + // รองรับ JSON ที่ส่ง array/object มา แต่ Content Control ต้องการ text + return JSON.stringify(value); +} + +/** + * นำ JSON data ไปแทนค่า Content Control ใน DOCX template + * คืนค่าเป็น Buffer ของ DOCX ที่สร้างเสร็จแล้ว + */ +export function templateContentControls( + templateBuffer: Buffer, + data: TemplateData, +): Buffer { + const zip = new PizzaZip(templateBuffer); + const docXmlFile = zip.file("word/document.xml"); + + if (!docXmlFile) { + throw new Error("Invalid docx file: word/document.xml not found."); + } + + const parser = new DOMParser(); + const doc = parser.parseFromString(docXmlFile.asText(), "text/xml"); + const select = createSelector(); + + const sdtNodes = select( + "//w:sdt", + asXpathNode(doc), + ) as unknown as XmlElement[]; + + sdtNodes.forEach((sdt) => { + const tagAttr = select( + "./w:sdtPr/w:tag/@w:val", + asXpathNode(sdt), + true, + ) as unknown as XmlAttr | undefined; + + const tagName = tagAttr?.value; + + if (!tagName || !Object.prototype.hasOwnProperty.call(data, tagName)) { + return; + } + + const newValue = normalizeValue(data[tagName]); + + const sdtContent = select( + "./w:sdtContent", + asXpathNode(sdt), + true, + ) as unknown as XmlElement | undefined; + + if (!sdtContent) return; + + const { type: controlType, multiLine } = + detectSdtControlType(sdt, select); + + const structure = detectSdtStructure(sdtContent, select); + + if (controlType === "other") { + insertFallbackContent(sdtContent, select, newValue); + return; + } + + if (structure === "inline") { + const allowLineBreak = + controlType === "plain" ? multiLine : true; + + insertInlineContent( + doc, + sdtContent, + select, + newValue, + allowLineBreak, + ); + } else { + insertBlockContent( + doc, + sdtContent, + select, + newValue, + controlType === "rich", + ); + } + }); + + const serializer = new XMLSerializer(); + zip.file( + "word/document.xml", + serializer.serializeToString(doc), + ); + + return zip.generate({ + type: "nodebuffer", + compression: "DEFLATE", + }); +} diff --git a/app/api/template/render/route.ts b/app/api/template/render/route.ts index c738afe..df6501c 100644 --- a/app/api/template/render/route.ts +++ b/app/api/template/render/route.ts @@ -1,71 +1,94 @@ import { NextResponse } from "next/server"; -import { DocumentTemplate,applyTemplate} from "@/lib/lib-tmpl"; -export const runtime = "nodejs"; +import { + templateContentControls, + type TemplateData, +} from "../lib-tmpl"; +export const runtime = "nodejs"; const DOCX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + // cap เดียวกับ N4 (app/actions/files.ts) — route นี้เปิดกว้างให้ภายนอกยิงได้ // ต้องเช็คขนาดก่อนอ่านเข้าหน่วยความจำ กัน DoS (bodySizeLimit ครอบเฉพาะ Server Actions) const MAX_UPLOAD_BYTES = 100 * 1024 * 1024; // 100MB -const TICKET_SEC = Number(process.env.TICKET_SEC||'300') export async function POST(request: Request) { try { const formData = await request.formData(); + const template = formData.get("template"); const dataFile = formData.get("data"); const dataJson = formData.get("dataJson"); - const TEST_SECRET = String(formData.get("secret")||"") - const TEST_PRIVATE_KEY_HEX = String(formData.get("private_key")||"") + // ----------------------------- // ตรวจสอบ Template + // ----------------------------- + if (!(template instanceof File)) { return NextResponse.json( { + success: false, error: 'Missing DOCX file in field "template".', }, { status: 400 }, ); } - if (template.type !== DOCX_CONTENT_TYPE && - !template.name.toLowerCase().endsWith(".docx") && - template.size > MAX_UPLOAD_BYTES + if ( + template.type !== DOCX_CONTENT_TYPE && + !template.name.toLowerCase().endsWith(".docx") ) { return NextResponse.json( { - error: 'Field "template" must be a .docx file and not exceeds the 100MB size limit.', + success: false, + error: 'Field "template" must be a .docx file.', }, { status: 400 }, ); } + if (template.size > MAX_UPLOAD_BYTES) { + return NextResponse.json( + { + success: false, + error: 'Field "template" exceeds the 100MB size limit.', + }, + { status: 400 }, + ); + } + // ----------------------------- // อ่าน JSON - let templateData: DocumentTemplate; + // ----------------------------- + + let data: TemplateData; + // JSON ผิด syntax เป็นความผิดของฝั่งผู้เรียก — คืน 400 ไม่ใช่ 500 // (catch กลางท้ายฟังก์ชันสงวนไว้ให้ error ของระบบเท่านั้น) try { if (typeof dataJson === "string" && dataJson.length > 0) { // กรณีส่ง JSON ผ่าน form field - templateData = JSON.parse(dataJson) as DocumentTemplate; + data = JSON.parse(dataJson) as TemplateData; } else if (dataFile instanceof File) { if (dataFile.size > MAX_UPLOAD_BYTES) { return NextResponse.json( { + success: false, error: 'Field "data" exceeds the 100MB size limit.', }, { status: 400 }, ); } + // กรณี upload data.json const text = await dataFile.text(); - templateData = JSON.parse(text) as DocumentTemplate; + data = JSON.parse(text) as TemplateData; } else { return NextResponse.json( { + success: false, error: 'Missing JSON data. Send either "data" file or "dataJson" field.', }, @@ -75,20 +98,24 @@ export async function POST(request: Request) { } catch (error) { return NextResponse.json( { - error: `Invalid JSON data (${error instanceof Error ? error.message : "parse error" - }).`, + success: false, + error: `Invalid JSON data (${ + error instanceof Error ? error.message : "parse error" + }).`, }, { status: 400 }, ); } + // JSON ต้องเป็น object if ( - templateData === null || - typeof templateData !== "object" || - Array.isArray(templateData) + data === null || + typeof data !== "object" || + Array.isArray(data) ) { return NextResponse.json( { + success: false, error: "JSON data must be an object.", }, { status: 400 }, @@ -99,21 +126,31 @@ export async function POST(request: Request) { // Template File → Buffer // ----------------------------- - let templateBuffer: Buffer = Buffer.from( + const templateBuffer = Buffer.from( await template.arrayBuffer(), ); - const output = await applyTemplate(templateBuffer,templateData,TEST_SECRET,TEST_PRIVATE_KEY_HEX,TICKET_SEC); + + // ----------------------------- + // ใช้ lib-tmpl.ts เดิม + // ----------------------------- + + const output = templateContentControls( + templateBuffer, + data, + ); // ----------------------------- // ส่ง DOCX กลับ // ----------------------------- - if(templateData?.info?.ticket_data) + return new NextResponse(output as BodyInit, { status: 200, headers: { "Content-Type": DOCX_CONTENT_TYPE, + "Content-Disposition": 'attachment; filename="generated-document.docx"', + "Content-Length": String(output.length), }, }); @@ -122,7 +159,7 @@ export async function POST(request: Request) { return NextResponse.json( { - + success: false, error: error instanceof Error ? error.message @@ -131,5 +168,4 @@ export async function POST(request: Request) { { status: 500 }, ); } -} - +} \ No newline at end of file diff --git a/app/api/template/test/T2.json b/app/api/template/test/T2.json new file mode 100644 index 0000000..799c36f --- /dev/null +++ b/app/api/template/test/T2.json @@ -0,0 +1,29 @@ +{ + "notname": "คำสั่งเรียก ให้ทำคำให้การ", + "nottype": "(ต. ๑)", + "blackno": "คดีหมายเลขดำที่", + "blackyear": "๑๐/๒๕๖๙", + "redno": "คดีหมายเลขแดงที่", + "redyear": "", + "court": "ศาลปกครองสุพรรณบุรี", + "curdate": "วันที่ ๗ เดือน สิงหาคม พุทธศักราช ๒๕๖๙", + "accuser": "นายธัญวิสิษฐ์ ศรแก้ว", + "acc1": "ผู้ฟ้องคดี", + "accused": "เจ้าคุณมด ที่ ๑ กับพวกรวม ๓ คน", + "acc2": "ผู้ถูกฟ้องคดี", + "nottitle": "คำสั่งถึง", + "commto": "เจ้าคุณมด", + "groupname": "ผู้ถูกฟ้องคดีที่ ๑", + "detail1": "ด้วยผู้ฟ้องคดีได้ยื่นฟ้องผู้ถูกฟ้องคดีต่อศาล มีข้อความตามที่ปรากฏ ในระบบงานคดีปกครองอิเล็กทรอนิกส์\nฉะนั้น ให้ ท่านทำคำให้การแก้คำฟ้อง พร้อมด้วยพยานหลักฐาน ๑ ชุด ต่อศาลภายใน ๓๐ วัน นับแต่วันที่ได้รับคำสั่งนี้", + "pname": "นางนพเก้า เถื่อนชัย", + "courtname": "ศาลปกครองสุพรรณบุรี", + "courtno": "๓๓๓ หมู่ ๖ ตำบลสนามชัย อำเภอเมืองสุพรรณบุรี", + "courttam": "จังหวัดสุพรรณบุรี ๗๒๐๐๐", + "courttel": "กลุ่มออกหมาย โทรศัพท์ ๐๓๕ ๔๓๒ ๓๓๓ ต่อ ๑๐๐๑,๒๒๐๓", + "courttel2": "สอบถามติดตามคดี ขอคำปรึกษาแนะนำ โทรสายด่วน “๑๓๕๕”", + "sendto": "เจ้าคุณมด", + "addrno": "เลขที่ - หมู่ที่ - ตรอก/ซอย - ถนน - ตำบล/แขวง - อำเภอ/เขต - จังหวัด - โทรศัพท์ -", + "remarkhead": "", + "remark": "", + "warning": "หากผู้ถูกฟ้องคดีไม่ปฏิบัติตามคำสั่งศาลภายในระยะเวลาที่กำหนด ศาลจะถือว่าผู้ถูกฟ้องคดียอมรับข้อเท็จจริงตามข้อหาของผู้ฟ้องคดี" +} \ No newline at end of file diff --git a/public/tmpl/T12_X.docx b/app/api/template/test/T3.docx old mode 100755 new mode 100644 similarity index 75% rename from public/tmpl/T12_X.docx rename to app/api/template/test/T3.docx index 93478ec..7573a45 Binary files a/public/tmpl/T12_X.docx and b/app/api/template/test/T3.docx differ diff --git a/app/api/template/test/Test.docx b/app/api/template/test/Test.docx new file mode 100644 index 0000000..f078f00 Binary files /dev/null and b/app/api/template/test/Test.docx differ diff --git a/app/api/template/test/make-protected.mjs b/app/api/template/test/make-protected.mjs new file mode 100644 index 0000000..c35f2b2 --- /dev/null +++ b/app/api/template/test/make-protected.mjs @@ -0,0 +1,298 @@ +// ทดลอง: เปิด Restrict Edit (readOnly + enforcement) และฝัง custom properties +// (token / primary-key / owner) ลงไฟล์ docx — ตามแนวทาง Token-Based ใน note.md +// +// ใช้: node app/api/template/test/make-protected.mjs [in.docx] [out.docx] +// ไม่ส่ง argument = อ่านและเขียนทับ output-T3.docx (ผล render ล่าสุด) +// +// สิ่งที่แก้ในไฟล์ docx: +// 1. word/settings.xml — w:documentProtection edit="readOnly" enforcement="1" +// (เอกสารทั้งฉบับอ่านอย่างเดียว — ทดสอบแล้ว: readOnly ล็อค CC ด้วย +// จึงต้องคู่กับข้อ 2 ถึงจะแก้ใน CC ได้) +// 2. word/document.xml ฯลฯ — ครอบ w:sdt ทุกตัวที่มี w:tag ด้วย +// w:permStart(w:edGrp="everyone") / w:permEnd — กลไก "Exceptions: Everyone" +// ของแผง Restrict Editing ใน Word (บริเวณใน perm แก้ได้ นอกนั้นล็อค) +// พร้อมเติม w:lock="sdtLocked" ให้ CC ที่ยังไม่มี กันลบช่องตอนอยู่ในโหมดนี้ +// 3. docProps/custom.xml — สร้างใหม่ เก็บค่า custom properties แบบ vt:lpwstr +// 4. [Content_Types].xml — เพิ่ม Override สำหรับ custom.xml +// 5. _rels/.rels — เพิ่ม Relationship ชี้ docProps/custom.xml +import PizzaZip from "pizzip"; +import { DOMParser, XMLSerializer } from "@xmldom/xmldom"; +import xpath from "xpath"; +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const inPath = process.argv[2] ?? join(__dirname, "output-T3.docx"); +const outPath = process.argv[3] ?? inPath; + +// ตัวอย่างข้อมูลที่จะฝัง (ใน production จะมาจาก backend ตอน render เอกสาร) +const CUSTOM_PROPS = { + token: "eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJ1MTAwMSIsImZpZCI6IkRPTi0yMDI2LTAwMDEyMyIsImV4cCI6MTc5NjI0NzIwMH0.demo", + "primary-key": "DOC-2026-000123", + owner: "u1001", +}; + +const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; +const CUSTOM_NS = + "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"; +const VT_NS = + "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"; +const REL_CUSTOM = + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"; +const CT_CUSTOM = + "application/vnd.openxmlformats-officedocument.custom-properties+xml"; +const FMTID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"; + +const parser = new DOMParser(); +const serializer = new XMLSerializer(); + +const parseXml = (text) => parser.parseFromString(text, "text/xml"); + +// xmldom serializer อาจคง xml declaration ไว้แล้ว — เติมเฉพาะเมื่อยังไม่มี +// (ถ้า prepend ซ้ำจะได้ declaration สองอัน ไฟล์ XML invalid) +const serialize = (doc) => { + const text = serializer.serializeToString(doc); + return text.startsWith("\r\n' + text; +}; + +// ครอบ w:sdt ที่มี w:tag ทุกตัวด้วย w:permStart/w:permEnd (edGrp="everyone") +// และเติม w:lock="sdtLocked" ให้ CC ที่ยังไม่มี — คืนจำนวน CC ที่ครอบ +function unlockContentControls(zip) { + const select = xpath.useNamespaces({ w: W_NS }); + const asNode = (n) => n; + const parts = Object.keys(zip.files).filter((n) => + /^word\/(document|header\d*|footer\d*)\.xml$/.test(n), + ); + + // id ของ perm ต้องไม่ซ้ำในเอกสาร — เริ่มสูงกว่าค่าสูงสุดที่มีอยู่ (จากทุก part) + let nextId = 999; + for (const part of parts) { + const ids = select( + "//w:permStart/@w:id | //w:permEnd/@w:id", + asNode(parseXml(zip.file(part).asText())), + ); + for (const id of ids) { + const n = Number.parseInt(id.value, 10); + if (Number.isFinite(n) && n >= nextId) nextId = n; + } + } + + let wrapped = 0; + let locked = 0; + for (const part of parts) { + const doc = parseXml(zip.file(part).asText()); + + // เฉพาะ sdt ระดับบนสุด (ไม่อยู่ใน sdt อื่น) มี w:tag = ช่องข้อมูลของ template + // และยังไม่ถูกครอบด้วย permStart อยู่แล้ว (กัน double-wrap ตอนรันซ้ำ) + const sdts = select( + "//w:sdt[./w:sdtPr/w:tag][not(ancestor::w:sdt)][not(preceding-sibling::*[1][self::w:permStart])]", + asNode(doc), + ); + if (sdts.length === 0) continue; + + for (const sdt of sdts) { + // เติม lock ให้ CC ที่ยังไม่มี — กันลบช่องตอนเปิดช่องให้แก้ผ่าน permission + const sdtPr = select("./w:sdtPr", sdt)[0]; + const hasLock = select("./w:lock", sdtPr).length > 0; + if (!hasLock) { + const lock = doc.createElementNS(W_NS, "w:lock"); + lock.setAttribute("w:val", "sdtLocked"); + // w:lock ต้องอยู่หลัง w:id ตามลำดับ schema ของ sdtPr + const idEl = select("./w:id", sdtPr)[0]; + if (idEl && idEl.nextSibling) { + sdtPr.insertBefore(lock, idEl.nextSibling); + } else { + sdtPr.appendChild(lock); + } + locked++; + } + + const start = doc.createElementNS(W_NS, "w:permStart"); + start.setAttribute("w:id", String(nextId)); + start.setAttribute("w:edGrp", "everyone"); + const end = doc.createElementNS(W_NS, "w:permEnd"); + end.setAttribute("w:id", String(nextId)); + nextId++; + + sdt.parentNode.insertBefore(start, sdt); + sdt.parentNode.insertBefore(end, sdt.nextSibling); + wrapped++; + } + + zip.file(part, serialize(doc)); + } + return { wrapped, locked }; +} + +function enableReadOnlyProtection(zip) { + const file = zip.file("word/settings.xml"); + if (!file) throw new Error("word/settings.xml not found"); + + const doc = parseXml(file.asText()); + const prots = doc.getElementsByTagNameNS(W_NS, "documentProtection"); + const prot = prots.length > 0 ? prots[0] : null; + + if (prot) { + prot.setAttribute("w:edit", "readOnly"); + prot.setAttribute("w:enforcement", "1"); + } else { + // T3 มี element นี้อยู่แล้ว แต่รองรับไฟล์ที่ไม่มี: แทรกเป็น child แรกของ w:settings + const el = doc.createElementNS(W_NS, "w:documentProtection"); + el.setAttribute("w:edit", "readOnly"); + el.setAttribute("w:enforcement", "1"); + doc.documentElement.insertBefore(el, doc.documentElement.firstChild); + } + + zip.file("word/settings.xml", serialize(doc)); +} + +function setCustomProperties(zip, props) { + // --- 1. docProps/custom.xml : สร้างใหม่ หรือ merge กับของเดิม --- + const existing = zip.file("docProps/custom.xml"); + let doc; + if (existing) { + doc = parseXml(existing.asText()); + // ลบ property ชื่อซ้ำออกก่อน (เขียนทับค่าใหม่) + const all = Array.from(doc.getElementsByTagNameNS(CUSTOM_NS, "property")); + for (const p of all) { + if (Object.prototype.hasOwnProperty.call(props, p.getAttribute("name"))) { + p.parentNode.removeChild(p); + } + } + } else { + doc = parseXml( + `` + + ``, + ); + } + + // pid ต้อง >= 2 และไม่ซ้ำ — เริ่มจากค่าสูงสุดที่มีอยู่ +1 + const usedPids = Array.from( + doc.getElementsByTagNameNS(CUSTOM_NS, "property"), + ).map((p) => Number.parseInt(p.getAttribute("pid"), 10) || 1); + let nextPid = Math.max(1, ...usedPids) + 1; + + for (const [name, value] of Object.entries(props)) { + const prop = doc.createElementNS(CUSTOM_NS, "property"); + prop.setAttribute("fmtid", FMTID); + prop.setAttribute("pid", String(nextPid++)); + prop.setAttribute("name", name); + const val = doc.createElementNS(VT_NS, "vt:lpwstr"); + val.textContent = value; + prop.appendChild(val); + doc.documentElement.appendChild(prop); + } + zip.file("docProps/custom.xml", serialize(doc)); + + // --- 2. [Content_Types].xml : เพิ่ม Override (ครั้งเดียว) --- + const ct = parseXml(zip.file("[Content_Types].xml").asText()); + const hasCt = Array.from(ct.getElementsByTagName("Override")).some( + (o) => o.getAttribute("PartName") === "/docProps/custom.xml", + ); + if (!hasCt) { + const el = ct.createElement("Override"); + el.setAttribute("PartName", "/docProps/custom.xml"); + el.setAttribute("ContentType", CT_CUSTOM); + ct.documentElement.appendChild(el); + zip.file("[Content_Types].xml", serialize(ct)); + } + + // --- 3. _rels/.rels : เพิ่ม Relationship (ครั้งเดียว) --- + const rels = parseXml(zip.file("_rels/.rels").asText()); + const relNodes = Array.from(rels.getElementsByTagName("Relationship")); + if (!relNodes.some((r) => r.getAttribute("Type") === REL_CUSTOM)) { + const usedIds = new Set(relNodes.map((r) => r.getAttribute("Id"))); + let n = 1; + while (usedIds.has(`rId${n}`)) n++; + const el = rels.createElement("Relationship"); + el.setAttribute("Id", `rId${n}`); + el.setAttribute("Type", REL_CUSTOM); + el.setAttribute("Target", "docProps/custom.xml"); + rels.documentElement.appendChild(el); + zip.file("_rels/.rels", serialize(rels)); + } +} + +// อ่านกลับมาตรวจทุกอย่างหลังเขียนไฟล์ +function verify(path) { + const zip = new PizzaZip(readFileSync(path)); + + const settings = zip.file("word/settings.xml")?.asText() ?? ""; + const prot = settings.match(/]*\/>/)?.[0]; + const enforced = + prot?.includes('w:edit="readOnly"') && prot?.includes('w:enforcement="1"'); + + const customXml = zip.file("docProps/custom.xml")?.asText(); + const propsBack = {}; + if (customXml) { + const doc = parseXml(customXml); + for (const p of Array.from( + doc.getElementsByTagNameNS(CUSTOM_NS, "property"), + )) { + propsBack[p.getAttribute("name")] = p.textContent; + } + } + + const rels = zip.file("_rels/.rels")?.asText() ?? ""; + const ct = zip.file("[Content_Types].xml")?.asText() ?? ""; + + const docXml = zip.file("word/document.xml")?.asText() ?? ""; + const sdtPrs = docXml.match(/[\s\S]*?<\/w:sdtPr>/g) ?? []; + const plain = sdtPrs.filter((p) => / / `${k}="${v.length > 40 ? v.slice(0, 40) + "…" : v}"`) + .join(", ") || "(ว่าง)", + ); + console.log( + `relationship : ${rels.includes(REL_CUSTOM) ? "✓" : "✗ ไม่พบ"} | content-type override: ${ct.includes(CT_CUSTOM) ? "✓" : "✗ ไม่พบ"}`, + ); + console.log(`content controls : ${sdtPrs.length} ตัว (plain=${plain}, rich=${rich}, มี tag=${tagged})`); + console.log( + `permission ranges : permStart=${permStarts} / permEnd=${permEnds} (ต้องเท่ากับมี tag=${tagged}) ` + + (permStarts === tagged && permEnds === tagged ? "✓" : "✗"), + ); + console.log( + `CC ไม่มี lock : ${missingLock} ตัว (ต้องเป็น 0 — กันลบช่อง) ${missingLock === 0 ? "✓" : "✗"}`, + ); + + return ( + enforced && + !!customXml && + permStarts === tagged && + permEnds === tagged && + missingLock === 0 + ); +} + +const zip = new PizzaZip(readFileSync(inPath)); +enableReadOnlyProtection(zip); +const { wrapped, locked } = unlockContentControls(zip); +setCustomProperties(zip, CUSTOM_PROPS); + +if (locked > 0) { + console.log(`เติม w:lock="sdtLocked" ให้ CC อีก ${locked} ตัวที่ยังไม่มี`); +} +console.log(`ครอบ CC ด้วย permission range: ${wrapped} ตัว`); + +const out = zip.generate({ type: "nodebuffer", compression: "DEFLATE" }); +writeFileSync(outPath, out); +console.log(`เขียนไฟล์แล้ว: ${outPath} (${out.length.toLocaleString()} bytes)`); + +const ok = verify(outPath); +process.exit(ok ? 0 : 1); diff --git a/app/api/template/test/note.md b/app/api/template/test/note.md new file mode 100644 index 0000000..919190b --- /dev/null +++ b/app/api/template/test/note.md @@ -0,0 +1,127 @@ +# Docx Template +lib-tmpl.ts เป็นแชร์ไลบารีสำหรับการทำ Template ใช้ Content Control ใน MS Word +## วิธีการใช้ API +```sh +## Swagger UI — เปิดในเบราว์เซอร์ (ลองยิงจริงผ่านปุ่ม Try it out ได้เลย ไม่ต้อง login) +http://localhost:3000/docedit/api/swagger + +## template/test ไว้ทดสอบเท่านั้น +curl.exe http://localhost:3000/docedit/api/template/test + +## extract ค่าออกมาจาก content control +curl.exe -X POST http://localhost:3000/docedit/api/template/extract -F "file=@./output-T3.docx" + +## render เอกสาร (docx) = template(docx)+data(json) +### อาจจะเพิ่มการเอา template จาก url และ file(local) ยังไม่รู้ว่าทางนั้นจะส่งไฟล์แบบไหนมาให้ เอาแค่นี้ไว้ทดสอบก่อน +curl.exe -X POST http://localhost:3000/docedit/api/template/render -F "template=@./T3.docx" -F "data=@./T2.json" -o output.docx +### data เอาจาก string +curl -X POST http://localhost:3000/docedit/api/template/render \ + -F "template=@template.docx" \ + -F 'dataJson={"member_name":"นายสมชาย","member_id":"123456"}' \ + -o generated-document.docx + +``` +data(json) ตอนนี้อยู่ในรูปแบบ +```json +{ + "notname": "คำสั่งเรียก ให้ทำคำให้การ", + "nottype": "(ต. ๑)", + "blackno": "คดีหมายเลขดำที่", + "blackyear": "๑๐/๒๕๖๙", + ... +} +``` +data(json)น่าจะปรับเป็นแบบนี้เพื่อรองรับการทำ Token-Based Seamless Integration +หลักผ่าน API +- ส่วนของ cc จะไปกรอกใน Content Control +- uid,uname,fid,ott,expire สร้างเป็น token ฝังใน custom property ของเอกสาร +```json +{ + "uid":"id ผู้มีสิทธิ์แก้ไขเอกสาร", + "uname": "ชื่อผู้มีสิทธิ์แก้ไขเอกสาร สำหรับแสดงผล", + "fid":"file id จะได้เซฟข้อมูลกลับไปที่ Oracle ถูกตารางและหน่วยงาน", + "ott":"อายุของ one time token แบบระยะสั้น", + "expire": "อายุเอกสารแบบระยะยาวหลังเอา ott ไปแลก (optional)", + cc:{ + "notname": "คำสั่งเรียก ให้ทำคำให้การ", + "nottype": "(ต. ๑)", + "blackno": "คดีหมายเลขดำที่", + "blackyear": "๑๐/๒๕๖๙", + ... + } +} +``` +ที่ต้องทำเพิ่ม +- render ปัจจุบันมีไว้ให้ทางศาลทดสอบ อันที่ใช้จริงหลังกดสร้าง Template เพื่อ Download จะสร้างเอกสารเวอร์ชั่นแรกใน object storage หรือถ้ามีอยู่แล้ว จะเป็นการอัปเดต +- Backend จัดการ version +- อัปเดตข้อมูลและเอกสารกลับระบบเดิม ส่วนนี้ยังไม่ชัดเจนอาจจะทำ mock api เตรียมไว้ก่อน + +# ทดลอง Restrict Edit + Custom Properties (2026-10-07) + +สคริปต์ `make-protected.mjs` — อ่าน docx → เปิด protection + ฝัง custom properties → เขียนไฟล์ → อ่านกลับ verify ทุกครั้ง + +```sh +node app/api/template/test/make-protected.mjs [in.docx] [out.docx] +# ไม่ส่ง argument = อ่านและเขียนทับ output-T3.docx (ค่าตัวอย่างฝังอยู่ในตัวสคริปต์ CUSTOM_PROPS) +``` + +## Restrict Edit (เปิดผ่านโค้ด) + +- อยู่ที่ `word/settings.xml` → `` +- T3.docx มี element นี้อยู่เดิมแต่เป็น `w:enforcement="0"` (ตั้งค่าไว้ใน Word แต่ไม่ได้กดบังคับใช้) — สคริปต์แค่พลิกเป็น 1 ไม่ต้องแทรก element ใหม่ +- **บทเรียนจากทดสอบใน Word จริง (2026-10-07): `readOnly` + enforcement ล็อคทั้งเอกสารรวมถึง Content Control ด้วย** — CC ไม่ได้ถูกยกเว้นให้แก้อัตโนมัติ ต้องคู่กับข้อถัดไป +- **กลไกที่ถูกต้อง: ครอบ `w:sdt` ทุกตัวที่มี `w:tag` ด้วย `` … ``** — คือกลไก "Exceptions (optional): Everyone" ที่แผง Restrict Editing ของ Word ใช้เวลาเราเลือกบริเวณแล้วติ๊ก Everyone (บริเวณใน permission range แก้ได้ นอกนั้นล็อค) — แทรกที่ระดับเดียวกับ sdt (inline ใน `w:p` หรือ block ใน `w:body` ก็ valid ตาม grammar) +- สคริปต์เติม `w:lock="sdtLocked"` ให้ CC ที่ยังไม่มีด้วย (T3 ขาด acc1, nottitle) — พอช่องถูกเปิดให้แก้ผ่าน permission แล้ว ช่องที่ไม่มี lock จะถูก**ลบ**ได้ตามไปด้วย +- ไม่ใส่ password → ผู้ใช้ปลดเองได้จาก Review → Restrict Editing → Stop Protection — ตั้งใจไว้แบบนี้เพราะ concept คือ token ใน custom property เป็นตัวกำกับสิทธิ์จริง ส่วน protection เป็นแค่เกราะกันแก้พลาด (ถ้าจะใส่ password ต้องคำนวณ legacy hash: SHA-1 ของ password+salt วนตาม `w:cryptSpinCount` ตาม ECMA-376 — ยังไม่ทำ) + +## ฝังข้อมูลใน Property เอกสาร (token / primary-key / owner) + +ทางเลือกที่ศึกษา: +1. **`docProps/custom.xml` (custom properties) — ที่เลือกใช้** เห็นได้ใน Word: File → Info → Properties → Advanced Properties → แท็บ Custom; add-in ฝั่ง Word (Office.js) อ่าน/เขียนได้ผ่าน `Document.properties.customProperties` (WordApi 1.3+) + - ไฟล์ที่ไม่มี part นี้อยู่ (กรณีปกติของ docx) ต้องทำครบ 3 อย่างพร้อมกัน ไม่งั้น Word ถือว่าไฟล์เสีย: + 1. สร้าง `docProps/custom.xml` — property แต่ละตัว: `fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"` คงที่ + `pid` ≥ 2 ไม่ซ้ำ + `name` + ค่า string ใน `vt:lpwstr` + 2. `[Content_Types].xml` เพิ่ม `` + 3. `_rels/.rels` เพิ่ม Relationship ชนิด `.../relationships/custom-properties` Target `docProps/custom.xml` +2. `docProps/core.xml` (core properties: title/subject/creator/description) — แก้ง่าย เห็นในหน้า Properties ปกติ เหมาะกับข้อมูลที่แสดงต่อผู้ใช้ ไม่ใช่ token +3. `w:docVar` ใน `word/settings.xml` (document variables) — ไม่โผล่ใน UI เลย อ่านได้ผ่าน VBA/VSTO (`Document.Variables`) แต่ Word JS API ไม่มีทางตรง — ไม่เลือก + +gotcha ที่เจอ: `XMLSerializer` ของ @xmldom/xmldom คง `` declaration ไว้ในผลลัพธ์อยู่แล้ว — ห้าม prepend เพิ่มเอง จะได้ declaration ซ้อนเป็น XML invalid (Word/node อ่านไม่ได้) + +## extract บอก alignment รายช่อง — key `@align` (2026-10-07) — **ถอดออกแล้วตามคำขอ (ยังไม่เอา)** + +- เคย implement ใน `lib-tmpl.ts` + spec `lib/openapi.ts` แล้ว**ถอดกลับสถานะเดิม** — extract ปัจจุบันคืนแค่ `{ tag: text }` เหมือนเดิม +- แนวทางที่พิสูจน์แล้วว่าใช้ได้ (เผื่อกลับมาทำภายหลัง): คืน key `@align` (ค่า w:jc เช่น `thaiDistribute`) เฉพาะช่องที่ต่างจาก default ของเอกสาร โดย resolve: `w:pPr/w:jc` ตรงของย่อหน้า → style ที่ใช้ (ไล่ `w:pStyle` → `w:basedOn`) → `docDefaults` ("left"/"start" นับเป็นไม่ตั้ง) — บริบทย่อหน้าของช่อง: CC block = `w:p` แรกใน `w:sdtContent` / CC inline = `w:p` ที่ครอบ sdt +- ผลทดสอบตอน implement: direct pPr ชนะ style ✓ inline/block จับได้ทั้งคู่ ✓ และ T3 จัดช่อง 8 ตัวผ่าน style อยู่แล้ว (nottype/blackyear/redyear/acc1/acc2/groupname/detail1/pname — ชิดขวา/กลาง) — ช่องเหล่านี้จะโผล่ `@align` ตามจริง + +## จัด paragraph แบบ Thai Distributed — ตัดสินใจ: จัดเองใน Word ตอนทำ template (2026-10-07) + +- ค่า OOXML คือ `` ใน `w:pPr` (ใน Word UI: Paragraph → Alignment → Thai Distributed, ทางลัด Ctrl+Shift+J) — ต่างจาก `distribute` ธรรมดาตรงที่จัดกระจายช่องว่างระหว่างคำเป็นหลัก ไม่บีบยืดตัวอักษร/สระ-วรรณยุกต์ +- **ไม่ตั้งผ่านโค้ด** — ผู้ออกแบบ template จัดเองใน Word ตอนทำ template ทางเดียวกับที่ตั้งฟอนต์/ย่อหน้าอื่น ๆ (เคยทดลองตั้ง default ทั้งเอกสารผ่าน `styles.xml` → `w:docDefaults/w:pPrDefault/w:pPr` แล้วถอดออกตามคำขอ) +- วิธีจัดเองใน Word: + - รายย่อหน้า/ราย CC: คลิกในย่อหน้า → `Ctrl+Shift+J` หรือ Home → Paragraph launcher → Alignment เลือก Thai Distributed + - ทั้งเอกสาร: คลิกขวา style Normal → Modify → Format → Paragraph → Thai Distributed + - จัด "เฉพาะช่อง CC" ได้เฉพาะ CC แบบ block (มี `w:p` ใน `w:sdtContent` ของตัวเอง เช่น `detail1`) — CC แบบ inline อยู่ในบรรทัดเดียวกับข้อความอื่น การจัดมีผลทั้งบรรทัด +- จุดสำคัญต่อ render pipeline: `insertBlockContent` ของ lib-tmpl clone `w:pPr` ของย่อหน้าแรกให้ทุกบรรทัดที่แทนค่า — จัดไว้ใน CC ตั้งแต่ต้นค่าจะสืบทอดไปทุกบรรทัดที่ render ใส่ข้อมูลอัตโนมัติ ไม่ต้องทำอะไรฝั่ง render +- (อ้างอิงเชิงเทคนิคกรณีต้องตั้งผ่านโค้ดภายหลัง: `w:jc` ใน `w:pPr` เป็น fixed sequence — ต้องอยู่หลัง `w:autoSpaceDN`/`w:spacing`/`w:ind` และก่อน `w:textDirection`/`w:rPr`/`w:sectPr`) + +## ผล verify (อัตโนมัติทุกครั้งที่รันสคริปต์) + +- `documentProtection` เปิดใช้ (readOnly + enforcement=1) ✓ +- `docProps/custom.xml` มี + relationship + content-type override ครบ ✓ +- Content Control 27 ตัวครบเดิม (plain 26 = มี `w:text`, rich 1 = `detail1`) ✓ +- permission ranges: permStart/permEnd ครบตามจำนวน CC ที่มี tag (27/27) ✓ +- ทุก CC มี `w:lock="sdtLocked"` (กันลบช่อง) ✓ +- `extractContentControls` (lib-tmpl) อ่านคืนได้ทุก tag ตรงกับ output.docx ก่อน protect 100% ✓ + +## ผลทดสอบใน Word จริง (2026-10-07 รอบ 1) + +- ✅ แถบเหลือง protection ขึ้น / แก้นอก CC ไม่ได้ / ลบ CC ไม่ได้ +- ❌ B5: **CC แก้ไม่ได้** เพราะ readOnly ล็อคทั้งเอกสาร → แก้ด้วย permStart/permEnd ตามด้านบนแล้ว (รอบ 2) +- ✅ custom properties ครบ / ปลด protection ได้แบบไม่มี password + +## เหลือตรวจใน Word จริง (รอบ 2) + +- เปิด `output-T3.docx` → พิมพ์/แก้ในช่อง CC ต้องได้แล้ว (B5) +- ลบ CC ต้องไม่ได้ทุกตัว (รวม acc1, nottitle ที่เติม lock ให้) +- ขั้น E เดิม: F12 เซฟสำเนา → เปิดใหม่ → properties ยังอยู่ครบ +- (ผ่านแล้วทั้งหมด → ย้าย logic ไปเป็นฟังก์ชันใน lib-tmpl.ts แล้วต่อเข้า render pipeline) diff --git a/public/tmpl/T_X.docx b/app/api/template/test/output-T3.docx old mode 100755 new mode 100644 similarity index 73% rename from public/tmpl/T_X.docx rename to app/api/template/test/output-T3.docx index 06182a4..c7619be Binary files a/public/tmpl/T_X.docx and b/app/api/template/test/output-T3.docx differ diff --git a/app/api/template/test/output.docx b/app/api/template/test/output.docx new file mode 100644 index 0000000..2622682 Binary files /dev/null and b/app/api/template/test/output.docx differ diff --git a/app/api/template/test/test.http b/app/api/template/test/test.http new file mode 100644 index 0000000..d615855 --- /dev/null +++ b/app/api/template/test/test.http @@ -0,0 +1,43 @@ +@baseUrl = http://localhost:3000/docedit/api + +### 1. Extract Template +POST {{baseUrl}}/template/extract +Content-Type: multipart/form-data; boundary=WebAppBoundary + +--WebAppBoundary +Content-Disposition: form-data; name="file"; filename="output-T3.docx" +Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document + +< ./output-T3.docx +--WebAppBoundary-- + +### 2. Render Template with JSON File +POST {{baseUrl}}/template/render +Content-Type: multipart/form-data; boundary=WebAppBoundary + +--WebAppBoundary +Content-Disposition: form-data; name="template"; filename="T3.docx" +Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document + +< ./T3.docx +--WebAppBoundary +Content-Disposition: form-data; name="data"; filename="T2.json" +Content-Type: application/json + +< ./T2.json +--WebAppBoundary-- + +### 3. Render Template with Raw JSON String +POST {{baseUrl}}/template/render +Content-Type: multipart/form-data; boundary=WebAppBoundary + +--WebAppBoundary +Content-Disposition: form-data; name="template"; filename="template.docx" +Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document + +< ./T3.docx +--WebAppBoundary +Content-Disposition: form-data; name="dataJson" + +{"notname": "คำสั่งเรียก ให้ทำคำให้การ","nottype": "(ต. ๑)","blackno": "คดีหมายเลขดำที่","blackyear": "๑๐/๒๕๖๙","redno": "คดีหมายเลขแดงที่","redyear": "","court": "ศาลปกครองสุพรรณบุรี","curdate": "วันที่ ๗ เดือน สิงหาคม พุทธศักราช ๒๕๖๙","accuser": "นายธัญวิสิษฐ์ ศรแก้ว","acc1": "ผู้ฟ้องคดี","accused": "เจ้าคุณมด ที่ ๑ กับพวกรวม ๓ คน","acc2": "ผู้ถูกฟ้องคดี","nottitle": "คำสั่งถึง","commto": "เจ้าคุณมด","groupname": "ผู้ถูกฟ้องคดีที่ ๑","detail1": "ด้วยผู้ฟ้องคดีได้ยื่นฟ้องผู้ถูกฟ้องคดีต่อศาล มีข้อความตามที่ปรากฏ ในระบบงานคดีปกครองอิเล็กทรอนิกส์\nฉะนั้น ให้ ท่านทำคำให้การแก้คำฟ้อง พร้อมด้วยพยานหลักฐาน ๑ ชุด ต่อศาลภายใน ๓๐ วัน นับแต่วันที่ได้รับคำสั่งนี้","pname": "นางนพเก้า เถื่อนชัย","courtname": "ศาลปกครองสุพรรณบุรี","courtno": "๓๓๓ หมู่ ๖ ตำบลสนามชัย อำเภอเมืองสุพรรณบุรี","courttam": "จังหวัดสุพรรณบุรี ๗๒๐๐๐","courttel": "กลุ่มออกหมาย โทรศัพท์ ๐๓๕ ๔๓๒ ๓๓๓ ต่อ ๑๐๐๑,๒๒๐๓","courttel2": "สอบถามติดตามคดี ขอคำปรึกษาแนะนำ โทรสายด่วน “๑๓๕๕”","sendto": "เจ้าคุณมด","addrno": "เลขที่ - หมู่ที่ - ตรอก/ซอย - ถนน - ตำบล/แขวง - อำเภอ/เขต - จังหวัด - โทรศัพท์ -","remarkhead": "","remark": "","warning": "หากผู้ถูกฟ้องคดีไม่ปฏิบัติตามคำสั่งศาลภายในระยะเวลาที่กำหนด ศาลจะถือว่าผู้ถูกฟ้องคดียอมรับข้อเท็จจริงตามข้อหาของผู้ฟ้องคดี"} +--WebAppBoundary-- diff --git a/app/tmpl/page.tsx b/app/tmpl/page.tsx deleted file mode 100644 index 0eaec44..0000000 --- a/app/tmpl/page.tsx +++ /dev/null @@ -1,100 +0,0 @@ -// app/page.tsx (ระบบ B) -'use client'; -import type { ChangeEvent } from 'react'; -// import { createDownloadFormAction } from './actions'; -import DependentDropdown, { DropdownData } from '@/components/utils/DependentDropdown'; - - - -export default function HomePage() { - const templateData: DropdownData = { - T_X: ['T_no_warning', 'TF_no_warning_P2', 'TS_P2_IN'], - T12_X: ['T12_b2_IN', 'T12_FD155_3s-long'], - }; - const handleTmplSelect = (template: string, data: string) => { - //console.log('Location Selected:', { template, data }); - }; - const handleInput = (e: ChangeEvent)=>{ - // console.log(e.target) - return true; - } - return ( -
-
-

ตัวอย่างการใช้ สร้างเอกสารจาก Template

-
- ตัวอย่าง Frontend ส่งข้อมูลผ่าน <form>(POST) หรือ <a>(GET) - เพื่อรองรับ Vanilla JavaScript และ Classic Framework ใช้การเปลี่ยนหน้าเพื่อดาว์นโหลดไฟล์ - ต้องทำให้ ASP.NET มีฟังก์ชั่นเหมือน /docedit/api/template/download-docx -
-
-
-

Form POST

- -
-
- uid: -
-
- uname: -
- - -
- -
-

Template & Data

- Template: T_X.docx,   - T12_X.docx
- Data: TF_no_warning_P2.json,   - T_no_warning.json,   - TS_P2_IN.json
- T12_b2_IN.json,   - T12_FD155_3s-long.json -
-
-

Flow

-
- หน้านี้(เลือกไฟล์)
- ↓
- download-docx(ตรวจสิทธิ์,ส่งเทมเพลต ID,ส่งข้อมูล)
- ↓
- generate(เปิดเทมเพลต,สร้าง docx)
- ↓
- ดาว์นโหลด docx เปิด MS Word อัตโนมัติ
- ↓
- Add-ins ตรวจสอบไฟล์, ผู้ใช้แก้ไข docx
- ↓
- Versioning(คุมเวอร์ชั่น)
- ↓
- ecourt(อัปเดตฐานข้อมูลและไฟล์)
- -
- -
- -
-
- ); -} \ No newline at end of file diff --git a/components/utils/DependentDropdown.tsx b/components/utils/DependentDropdown.tsx deleted file mode 100644 index 7331bd9..0000000 --- a/components/utils/DependentDropdown.tsx +++ /dev/null @@ -1,91 +0,0 @@ -'use client'; - -import { useState, ChangeEvent, useEffect } from 'react'; - -export interface DropdownData { - [key: string]: string[]; -} - -interface DependentDropdownProps { - data: DropdownData; - name1: string; // เพิ่มมาเพื่อใช้กับ Form Action - name2: string; // เพิ่มมาเพื่อใช้กับ Form Action - label1?: string; - label2?: string; - placeholder1?: string; - placeholder2?: string; - onSelectChange?: (mainValue: string, subValue: string) => void; -} - -export default function DependentDropdown({ - data, - name1, - name2, - label1 = 'เลือกตัวเลือกหลัก:', - label2 = 'เลือกตัวเลือกย่อย:', - placeholder1 = '-- กรุณาเลือก --', - placeholder2 = '-- กรุณาเลือกตัวเลือกหลักก่อน --', - onSelectChange, -}: DependentDropdownProps) { - const [mainSelection, setMainSelection] = useState(''); - const [subSelection, setSubSelection] = useState(''); - - const handleMainChange = (e: ChangeEvent) => { - const value = e.target.value; - setMainSelection(value); - setSubSelection(''); - }; - - const handleSubChange = (e: ChangeEvent) => { - setSubSelection(e.target.value); - }; - - useEffect(() => { - if (onSelectChange) { - onSelectChange(mainSelection, subSelection); - } - }, [mainSelection, subSelection, onSelectChange]); - - const subOptions = mainSelection ? data[mainSelection] : []; - - return ( -
- {/* Dropdown ที่ 1 */} -
- - -
- - {/* Dropdown ที่ 2 */} -
- - -
-
- ); -} diff --git a/docker/web.Dockerfile b/docker/web.Dockerfile index 84788ea..38f1602 100644 --- a/docker/web.Dockerfile +++ b/docker/web.Dockerfile @@ -23,20 +23,8 @@ ENV NEXT_TELEMETRY_DISABLED=1 # db/index.ts throw ถ้าไม่มี DATABASE_URL ตอน next build เก็บ page data — # ใส่ค่า dummy (postgres.js เชื่อมแบบ lazy ไม่มี query ตอน build) ตัวจริงฉีดตอน docker run ENV DATABASE_URL=postgresql://placeholder:placeholder@127.0.0.1:5432/placeholder -ENV MINIO_ENDPOINT=127.0.0.1 -ENV MINIO_PORT=19000 -ENV MINIO_USE_SSL=false -ENV MINIO_ACCESS_KEY=minioadmin -ENV MINIO_SECRET_KEY=minioadmin -ENV DOCVER_API_KEY=2d9b76d49a4f5311f6a0dacf759ecec53f34b6d102f24c696890a4052c99cf54 -ENV DOCVER_TOKEN_SECRET=487023dd1c1c4d6a07475e96b1cc300fec1e68ac9c2dd7d5ac9b1bf75da1053e -ENV DOCVER_TOKEN_TTL_SECONDS=604800 -ENV MINIO_BUCKET=docver -ENV DOCVER_LINK_TTL_SECONDS=900 - RUN npm run build - # ─── Stage 3: runtime ตัวเบา (แค่ standalone + public/) ─── FROM node:24-alpine AS runner WORKDIR /app diff --git a/docs/PLAN-DOCVER.md b/docs/PLAN-DOCVER.md index fff1526..771669a 100644 --- a/docs/PLAN-DOCVER.md +++ b/docs/PLAN-DOCVER.md @@ -1,28 +1,20 @@ # PLAN-DOCVER — Document Versioning API (stand-alone) -> วันที่ตกลงแผน: 2026-10-20 · เพิ่ม auth ด้วย access_token: 2026-10-22 · สถานะ: implement -> รอบแรก (API ก่อน — ยังไม่ต่อ wordaddin) +> วันที่ตกลงแผน: 2026-10-20 · สถานะ: implement รอบแรก (API ก่อน — ยังไม่ต่อ wordaddin) ## 0. บริบท/เป้าหมาย ทำ backend จริงของฟีเจอร์ **"e-Court Documents Versioning"** (ตอนนี้ wordaddin ใช้ mock ใน `wordaddin/services/versioningService.ts`): -- ตารางเอกสารคีย์ด้วย `docId` **ห้ามซ้ำ** (unique index) — **`docId` = claim `fid` - (string) จาก Bearer token ตรง ๆ** (2026-10-26 ถอด path param `{docId}` ออกจาก URL ทุก - endpoint — fileId ของ e-Court ใน token เป็นที่มาเดียวของรหัสเอกสาร) +- รับ `docId` จากผู้เรียก → ตารางเอกสารที่ `docId` **ห้ามซ้ำ** (unique index) - แต่ละ version อ้าง **object ใน MinIO** — เก็บทั้ง `.docx` และ `.pdf` (pdf เป็น optional/best-effort ตาม contract ของ add-in — `pdf.size === 0` = ไม่มี, มี `hasPdf` บอกใน response) - เก็บ `userId`, `userName`, `createdAt` ต่อ version เพื่อรู้ว่าใครอัปโหลด -- **identity (`userId`/`userName`/`templateId`) และ `docId` แกะจาก Bearer token** (2026-10-22 - — เดิมส่ง plain field ใน multipart): ขอ token ที่ `POST /api/documents/token` ด้วย header - `x-api-key` แล้วแนบ `Authorization: Bearer ` กับทุก request — ระบบ stand-alone - ไม่มี session ร่วมกับใคร (ผู้เรียกคือ add-in ใน Word หรือระบบภายนอก) token จึงเป็น - พาหะ identity เดียว -- **auth ทุก endpoint ของกลุ่มนี้** = Bearer JWT HS256 เซ็นด้วย `DOCVER_TOKEN_SECRET` - (ออก token ผ่าน `x-api-key` เทียบ env `DOCVER_API_KEY` — pattern `lib/external-api-auth.ts` - ของ N10) · ไม่มี CORS +- **endpoint รับ `userId`/`userName` ใน request** เพราะเป็นระบบ stand-alone ไม่มี session + ร่วมกับใคร (ผู้เรียกคือ add-in ใน Word หรือระบบภายนอก — identity ส่งมาแบบ plain field) +- **ไม่มี auth** — เปิดกว้างเหมือนกลุ่ม `api/template` (intranet) · ไม่มี CORS ข้อตกลงกับผู้ใช้ (2026-10-20): ทำ API ก่อน **ไม่แต้ wordaddin รอบนี้** — contract ตรง surface ที่ mock คงไว้เป๊ะ วันหน้าสลับของจริงแค่ตั้ง `NEXT_PUBLIC_ADDIN_API_BASE_URL` แล้วก้อบ body @@ -36,33 +28,7 @@ Contract อ้างอิง: `docs/addin-ui-flow.md` §7 + `wordaddin/service ## 1. Endpoint (URL เต็ม = `/docedit/api/documents/…` — prefix ใหม่ ได้รับยกเว้นจาก middleware เหมือน `api/template`) -### `POST /api/documents/token` — ขอ access_token (2026-10-22) - -auth: header `x-api-key` เทียบ env `DOCVER_API_KEY` (ไม่ตั้ง/ว่าง = 401 fail closed · -ไม่ใส่/ผิด = 401 ข้อความเดียวกันเป๊ะ กัน enumerate) — ตรวจก่อนอ่าน body เสมอ - -JSON body (allowlist — key อื่น = 400 กัน typo): - -| field | type | หมายเหตุ | -|---|---|---| -| `userid` | string | บังคับ — trim 1–100 (claim `userid` → column `user_id`) | -| `username` | string | บังคับ — trim 1–200 (claim `username` → column `user_name`) | -| `fid` | string | **บังคับ** — trim 1–200 · **fileId ของ e-Court** เป็น `docId` ของทุก operation ตรง ๆ · verify ไม่รับ token ที่ไม่มี fid | -| `template` | string | optional ≤100 (claim `template` → column `template_id` ตอน upload) | - -- 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) - · verify บังคับ alg=HS256 (กัน alg-confusion) และเทียบ signature แบบ timing-safe -- **endpoint อื่นของกลุ่มนี้ทั้งหมดต้องแนบ `Authorization: Bearer `** — - ไม่มี header / รูปผิด / signature ผิด / หมดอายุ / secret ไม่ตั้ง = 401 ข้อความไทย - เดียวกันทุกกรณี (ไม่เปิดให้ใช้ endpoint เป็น oracle รู้สาเหตุ) - -### `POST /api/documents/versions` — อัปโหลด version ใหม่ - -auth: `Authorization: Bearer` (ดู /token ด้านบน) — identity/templateId ของ row ใหม่และ -`docId` (= `fid` — string) มาจาก claims ของ token ทั้งหมด ไม่รับจาก URL/form อีกต่อไป +### `POST /api/documents/{docId}/versions` — อัปโหลด version ใหม่ multipart/form-data: @@ -70,21 +36,22 @@ multipart/form-data: |---|---|---| | `file` | File (.docx) | บังคับ — ≤ 100MB, magic bytes `PK\x03\x04` | | `pdf` | File (.pdf) | optional — `size === 0` = ถือว่าไม่มี (add-in แนบ blob เปล่าได้) | +| `userId` | string | **บังคับ** (stand-alone) — trim 1–100 | +| `userName` | string | **บังคับ** (stand-alone) — trim 1–200 | | `clientVersionId` | string | optional ≤100 — GUID ที่ client generate เอง | | `fields` | string (JSON) | optional — map `{ [sdtTag]: text }` (≤100 keys, value ≤10,000) | | `message` | string | optional — commit message ≤2048 (trim ว่าง = NULL) | +| `templateId` | string | optional ≤100 | | `fileName` | string | optional — ชื่อไฟล์ดั้งเดิม (ผ่าน `decodeUploadFilename` แล้ว) | -- field ที่ไม่อยู่ในตาราง = **400 กัน typo เงียบ ๆ** (pattern `parseQueryBody` ของ N10) — - **breaking change 2026-10-22:** `userId`/`userName`/`templateId` ถอดออกจาก form แล้ว - (ส่งมา = 400) identity/template มาจาก Bearer token ของ request นั้นแทน +- field ที่ไม่อยู่ในตาราง = **400 กัน typo เงียบ ๆ** (pattern `parseQueryBody` ของ N10) - เอกสาร (`documents` row) สร้าง **แบบ implicit** ตอน upload version แรก — ไม่มี endpoint - "สร้างเอกสาร" แยก (เอกสารถูกระบุที่ fid ใน token ตั้งแต่ขอ token) + "สร้างเอกสาร" แยก (ตาม flow ของ add-in ที่ generate documentId เองแล้วอัปโหลดทันที) - response 200: `{ "hasPdf": boolean, "versionId": string, "versionNumber": number }` (= `UploadVersionResult` เป๊ะ) — `versionNumber` เป็นค่า **authoritative จาก server** (client เดา `latest+1` แล้วแก้ XML part ตัวเองเมื่อไม่ตรง — กรณีอัปโหลดชนกัน) -### `GET /api/documents/versions` — history (ของเอกสาร fid ใน token) +### `GET /api/documents/{docId}/versions` — history response: `DocumentVersionDTO[]` เรียง **ใหม่ → เก่า** (ตาม `VersionRecord[]` ของ add-in + ส่วนเสริม `createdByUserName`): @@ -103,14 +70,13 @@ response: `DocumentVersionDTO[]` เรียง **ใหม่ → เก่า } ``` -- `docId` (จาก fid ใน token) ไม่รู้จัก (ยังไม่เคยมี version) = **404** (สัญญาณ REST ตรง ๆ แบบ - N10 — UI ของ add-in แสดง error + ปุ่มลองใหม่ ไม่พัง) +- `docId` ไม่รู้จัก (ยังไม่เคยมี version) = **404** (สัญญาณ REST ตรง ๆ แบบ N10 — UI ของ + add-in แสดง error + ปุ่มลองใหม่ ไม่พัง) -### `GET /api/documents/versions/{versionId}/download-link?format=docx|pdf` +### `GET /api/documents/{docId}/versions/{versionId}/download-link?format=docx|pdf` - format ไม่ส่ง/"" = `docx` · ค่าอื่น = 400 -- query version row **เช็ค docId (จาก fid ใน token) กับ versionId พร้อมกัน** (กันโยน id - ข้ามเอกสาร) — ไม่เจอ = 404 +- query version row **เช็คทั้ง docId และ versionId พร้อมกัน** (กันโยน id ข้ามเอกสาร) — ไม่เจอ = 404 - `format=pdf` แต่ version นั้นไม่มี pdf = 404 "version นี้ไม่มีสำเนา PDF" - response 200: `{ "url": string, "expiresAt": string }` — url เป็น **MinIO presigned GET** (อายุตาม env `DOCVER_LINK_TTL_SECONDS` default 900 วิ) ให้ UI เปิดในเบราว์เซอร์จริงของ @@ -122,11 +88,10 @@ response: `DocumentVersionDTO[]` เรียง **ใหม่ → เก่า | เหตุ | สถานะ | |---|---| -| รูป versionId/format ผิด · field ไม่รู้จัก (รวม userId/userName/templateId เก่า) · ไม่ใช่ multipart · magic bytes ผิด · body ขอ /token ผิดกติกา (รวมขาด fid) | 400 | -| `x-api-key` ไม่ตั้ง/ไม่ใส่/ผิด (ที่ /token) · Bearer ขาด/เน่า/หมดอายุ/secret ไม่ตั้ง (ทุก endpoint อื่น) | 401 | +| รูป docId/versionId/format ผิด · field ไม่รู้จัก · ไม่ใช่ multipart · magic bytes ผิด | 400 | | ไฟล์เกิน 100MB (เช็ค `file.size` ก่อน buffer เสมอ — route ไม่มี `bodySizeLimit` คุม) | 413 | | เอกสาร/version ไม่มี · ขอ pdf ที่ version ไม่มี | 404 | -| MinIO env ไม่ตั้ง/ล้ม · DB ล้ม (หลังเก็บกวาด object กำพร้า) · `DOCVER_TOKEN_SECRET` ไม่ตั้ง (ที่ /token) | 500 (log เต็มฝั่ง server + ข้อความกลาง ๆ ไทย) | +| MinIO env ไม่ตั้ง/ล้ม · DB ล้ม (หลังเก็บกวาด object กำพร้า) | 500 (log เต็มฝั่ง server + ข้อความกลาง ๆ ไทย) | ## 2. ตาราง (db/schema.ts) @@ -191,9 +156,6 @@ read-only ของ N10 แต่ตัวนี้เขียนได้) | `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` | — บังคับ | dev compose = `minioadmin` | | `MINIO_BUCKET` | `docver` | app สร้างเองตอน first use (`ensureBucket` — จับ race ข้าม process) | | `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 ค่าดิบ | -| `DOCVER_TOKEN_SECRET` | — บังคับ | secret เซ็น/ตรวจ JWT HS256 — ไม่ตั้ง: /token = 500 · endpoint อื่น = 401 · ห้าม log ค่าดิบ | -| `DOCVER_TOKEN_TTL_SECONDS` | `3600` | อายุ access_token (วินาที) | `putObject` แนบ `Content-Type` + `Content-Disposition` (ชื่อไฟล์ไทยผ่าน `filename*=UTF-8''` best-effort + ASCII fallback `document.docx`/`document.pdf`) — presigned GET ได้ชื่อไฟล์สวย @@ -204,16 +166,13 @@ best-effort + ASCII fallback `document.docx`/`document.pdf`) — presigned GET - `db/schema.ts` — ตาราง + `DocumentVersionDTO`/`toDocumentVersionDTO` - `lib/document-versioning.ts` — pure validation (ห้าม import `@/db`/`minio` — unit test ได้ โดยไม่ต้องตั้ง env อะไรเลย ตาม pattern `lib/external-api-auth.ts`) -- `lib/docver-token.ts` — JWT HS256 issue/verify + `requireDocverApiKey`/`requireDocverBearer` - + `parseTokenRequestBody` (pure เหมือนกัน — ไม่ import `@/db`/`minio`) - `lib/docver-store.ts` — MinIO client (เก็บบน `globalThis` ใน dev แบบ `db/index.ts`) + `ensureBucket` + put/remove/presign + `insertVersionRecord` (tx) + query helpers -- `app/api/documents/token/route.ts` — POST ขอ access_token (x-api-key) -- `app/api/documents/versions/route.ts` — POST + GET (docId = fid จาก token) -- `app/api/documents/versions/[versionId]/download-link/route.ts` — GET +- `app/api/documents/[docId]/versions/route.ts` — POST + GET +- `app/api/documents/[docId]/versions/[versionId]/download-link/route.ts` — GET - `docker-compose.yml` — db (ชื่อ/วอลุ่มเดิมกับ `docker run` ใน README) + minio (โฮสต์ 19000 API / 19001 console — ข้างใน container 9000/9001; image จาก quay.io) -- `tests/lib/document-versioning.test.ts` + `tests/lib/docver-token.test.ts` — unit test ส่วน pure +- `tests/lib/document-versioning.test.ts` — unit test ส่วน pure ## 8. Out of scope รอบนี้ diff --git a/lib/document-versioning.ts b/lib/document-versioning.ts index bbbb054..9973286 100644 --- a/lib/document-versioning.ts +++ b/lib/document-versioning.ts @@ -17,21 +17,22 @@ export const DOCX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; export const PDF_CONTENT_TYPE = "application/pdf"; -/** รหัสเอกสาร (docId) ไม่มี validator ที่นี่แล้ว — ตอนนี้มาจาก fid ใน Bearer token - * (String(fid) — integer ที่ verify แล้ว ผ่าน lib/docver-token.ts) ไม่ใช่ path param - * ของผู้เรียก · parseDocId เดิมถอดออกพร้อมการถอด segment [docId] ออกจาก URL */ +/** รหัสเอกสารจากผู้เรียก — URL-path-safe ล้วน (ไม่มี / .. ช่องว่าง ไทย) กันถูก + * ฝังลง path/log แปลก ๆ และตัดขนาดให้จำกัด */ +const DOC_ID_RE = /^[A-Za-z0-9._:-]{1,200}$/; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; -/** field ที่รับใน multipart ของ POST …/versions — อะไรนอกนี้ = 400 กัน typo เงียบ ๆ - * (userId/userName/templateId ถอดออกแล้ว — identity/template มาจาก Bearer token - * ผ่าน lib/docver-token.ts ส่ง field เก่ามา = 400 ตาม convention) */ +/** field ที่รับใน multipart ของ POST …/versions — อะไรนอกนี้ = 400 กัน typo เงียบ ๆ */ const FORM_FIELDS = [ "file", "pdf", + "userId", + "userName", "clientVersionId", "fields", "message", + "templateId", "fileName", ] as const; @@ -46,6 +47,22 @@ export function docverError(message: string, status: number): Response { }); } +/** ตรวจ docId จาก path param — trim แล้วว่าง/เกิน 200/มีตัวอักษรนอก charset = ไม่ผ่าน + * (+ ปฏิเสธ "." / ".." เป็นพิเศษแม้อยู่ใน charset — path-traversal specials) */ +export function parseDocId(raw: string): + | { ok: true; docId: string } + | DocverError { + const docId = raw.trim(); + if (docId === "." || docId === ".." || !DOC_ID_RE.test(docId)) { + return { + ok: false, + error: + "รหัสเอกสาร (docId) ต้องยาว 1–200 ตัวอักษร และใช้ได้เฉพาะ A-Z a-z 0-9 . _ : -", + }; + } + return { ok: true, docId }; +} + /** magic bytes ของ .docx — OOXML เป็น ZIP จึงขึ้นต้น PK\x03\x04 (mirror รูปแบบ * matchesSignature ของ app/actions/files.ts — utils/file-kind.ts เป็นของ note files) */ export function isDocxBytes(bytes: Buffer): boolean { @@ -143,8 +160,11 @@ export function parseFieldsJson(raw: string): } export type ParsedVersionUploadForm = { + userId: string; + userName: string; clientVersionId: string | null; message: string | null; + templateId: string | null; /** ชื่อไฟล์ดั้งเดิมจาก field "fileName" (แยกจากชื่อของ File เอง — add-in ส่งมาเอง * เพราะ desktop Word ได้ชื่อจาก path จริง) — strip path + cap แล้ว */ fileName: string | null; @@ -152,8 +172,7 @@ export type ParsedVersionUploadForm = { }; /** ตรวจ text field ทั้งหมดของ POST …/versions (file/pdf เช็คที่ route เพราะต้องอ่าน - * size ก่อน buffer) — identity (userId/userName) กับ templateId ไม่รับใน form - * แล้ว: route แกะจาก Bearer token ผ่าน lib/docver-token.ts ก่อนเรียกตรงนี้ */ + * size ก่อน buffer) — userId/userName บังคับเพราะระบบ stand-alone ไม่มี session */ export function parseVersionUploadForm(form: FormData): | { ok: true; form: ParsedVersionUploadForm } | DocverError { @@ -168,12 +187,36 @@ export function parseVersionUploadForm(form: FormData): } } + const userId = formString(form, "userId"); + if (!userId.ok) return userId; + if (userId.value === null) { + return { ok: false, error: "ต้องระบุ userId (ระบบ stand-alone — บอกผู้อัปโหลดเอง)" }; + } + if (userId.value.length > 100) { + return { ok: false, error: "userId ยาวเกิน 100 ตัวอักษร" }; + } + + const userName = formString(form, "userName"); + if (!userName.ok) return userName; + if (userName.value === null) { + return { ok: false, error: "ต้องระบุ userName (ระบบ stand-alone — บอกผู้อัปโหลดเอง)" }; + } + if (userName.value.length > 200) { + return { ok: false, error: "userName ยาวเกิน 200 ตัวอักษร" }; + } + const clientVersionId = formString(form, "clientVersionId"); if (!clientVersionId.ok) return clientVersionId; if (clientVersionId.value && clientVersionId.value.length > 100) { return { ok: false, error: "clientVersionId ยาวเกิน 100 ตัวอักษร" }; } + const templateId = formString(form, "templateId"); + if (!templateId.ok) return templateId; + if (templateId.value && templateId.value.length > 100) { + return { ok: false, error: "templateId ยาวเกิน 100 ตัวอักษร" }; + } + const message = formString(form, "message"); if (!message.ok) return message; if (message.value && message.value.length > DOCVER_MESSAGE_MAX) { @@ -195,8 +238,11 @@ export function parseVersionUploadForm(form: FormData): return { ok: true, form: { + userId: userId.value, + userName: userName.value, clientVersionId: clientVersionId.value, message: message.value, + templateId: templateId.value, fileName: sanitizeDisplayFileName(rawFileName.value), fields, }, diff --git a/lib/docver-token.ts b/lib/docver-token.ts deleted file mode 100644 index 92bfc41..0000000 --- a/lib/docver-token.ts +++ /dev/null @@ -1,223 +0,0 @@ -// PLAN-DOCVER.md §auth — access_token ของกลุ่ม /api/documents (document versioning) -// - POST /api/documents/token ออก JWT HS256 ให้ผู้เรียกที่ยืนยัน header "x-api-key" -// กับ env DOCVER_API_KEY (fail closed เมื่อ env ไม่ตั้ง/ว่าง — pattern เดียวกับ -// lib/external-api-auth.ts ของ N10: hash SHA-256 ทั้งสองค่า + timingSafeEqual, -// ข้อความ 401 ของ "ไม่ใส่ key" กับ "key ผิด" เป็นอันเดียวกัน, ห้าม log ค่าดิบ) -// - token เซ็นด้วย env DOCVER_TOKEN_SECRET (มือเขียน HS256 ผ่าน node:crypto ล้วน — -// ไม่เพิ่ม dependency) แล้วทุก endpoint อื่นของกลุ่มบังคับ Authorization: Bearer — -// identity (userid/username) กับ templateId แกะจาก claims ไม่รับจาก payload -// - claims "fid" = fileId ของ e-Court (string) — เป็น docId ของทุก operation ตรง ๆ -// — บังคับตั้งแต่ขอ token และ verify ไม่รับ token ที่ไม่มี fid -// -// ห้าม import อะไรที่ลาก @/db หรือ minio มาด้วย — unit test (tests/lib/) ต้องรันได้ -// โดยไม่ต้องตั้ง DATABASE_URL/MINIO_* (pattern เดียวกับ lib/external-api-auth.ts) -import { createHash, createHmac, timingSafeEqual } from "node:crypto"; - -import { docverError, type DocverError } from "@/lib/document-versioning"; - -/** claims ของ access_token docver — โครงสร้างตามผู้เรียกกำหนด (e-Court) */ -export type DocverTokenClaims = { - userid: string; - username: string; - fid: string; - template: string | null; -}; - -function base64url(input: string): string { - return Buffer.from(input, "utf8").toString("base64url"); -} - -function decodeJsonSegment(segment: string): Record | null { - let text: string; - try { - text = Buffer.from(segment, "base64url").toString("utf8"); - } catch { - return null; - } - try { - const parsed = JSON.parse(text) as unknown; - if ( - parsed === null || - typeof parsed !== "object" || - Array.isArray(parsed) - ) { - return null; - } - return parsed as Record; - } catch { - return null; - } -} - -/** ออก JWT HS256 (base64url 3 segment) — payload = claims + iat/exp (วินาที) */ -export function issueDocverToken( - claims: DocverTokenClaims, - secret: string, - ttlSeconds: number, -): string { - const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" })); - const iat = Math.floor(Date.now() / 1000); - const payload = base64url( - JSON.stringify({ ...claims, iat, exp: iat + ttlSeconds }), - ); - const signature = createHmac("sha256", secret) - .update(`${header}.${payload}`) - .digest("base64url"); - return `${header}.${payload}.${signature}`; -} - -/** claims ที่ถอดได้จาก payload ที่ตรวจ signature ผ่านแล้ว — รูปร่างผิด = invalid - * (กัน token ที่เซ็นถูกแต่ claims ไม่ครบ/ผิด type หลุดไปใช้ต่อ) */ -function claimsFromPayload( - payload: Record, -): DocverTokenClaims | null { - const { userid, username, fid, template } = payload; - if (typeof userid !== "string" || userid === "") return null; - if (typeof username !== "string" || username === "") return null; - if (typeof fid !== "string" || fid === "") return null; - if (template !== null && typeof template !== "string") return null; - return { userid, username, fid, template }; -} - -/** ตรวจ token: 3 segment → alg บังคับ HS256 (กัน alg-confusion เช่น "none") → - * เทียบ signature แบบ timing-safe (hash ทั้งสองฝั่งก่อน — digest ยาวเท่ากันเสมอ) → - * exp ต้องยังไม่ผ่านมาแล้ว → claims ต้องครบรูปร่าง */ -export function verifyDocverToken( - token: string, - secret: string, -): { ok: true; claims: DocverTokenClaims } | { ok: false } { - const segments = token.split("."); - if (segments.length !== 3) return { ok: false }; - const [headerSegment, payloadSegment, signatureSegment] = segments; - - const header = decodeJsonSegment(headerSegment); - if (header === null || header.alg !== "HS256") return { ok: false }; - - const expected = createHmac("sha256", secret) - .update(`${headerSegment}.${payloadSegment}`) - .digest(); - // ถอด base64url กลับเป็น byte ดิบของ signature ก่อน — แล้ว hash ทั้งสองฝั่ง - // (SHA-256 = 32 byte เท่ากันเสมอ แม้ segment จะเป็นขยะ decode ได้สั้น/ว่าง — - // timingSafeEqual จึงไม่ throw เรื่องความยาวต่างกัน) - const provided = createHash("sha256") - .update(Buffer.from(signatureSegment, "base64url")) - .digest(); - const expectedHash = createHash("sha256").update(expected).digest(); - if (!timingSafeEqual(provided, expectedHash)) return { ok: false }; - - const payload = decodeJsonSegment(payloadSegment); - if (payload === null) return { ok: false }; - const exp = payload.exp; - if (typeof exp !== "number" || !Number.isFinite(exp)) return { ok: false }; - if (exp <= Math.floor(Date.now() / 1000)) return { ok: false }; - - const claims = claimsFromPayload(payload); - if (claims === null) return { ok: false }; - return { ok: true, claims }; -} - -/** auth ของ POST /api/documents/token — mirror requireExternalApiKey (N10) แต่ - * env เป็น DOCVER_API_KEY และตอบผ่าน docverError ของกลุ่มนี้ - * คืน null เมื่อผ่าน / Response 401 เมื่อไม่ผ่าน */ -export function requireDocverApiKey(request: Request): Response | null { - // เทียบกับค่า trim แล้ว — กัน trailing newline จาก K8s secret / docker env_file - const envKey = process.env.DOCVER_API_KEY?.trim(); - // const envKey = - // "2d9b76d49a4f5311f6a0dacf759ecec53f34b6d102f24c696890a4052c99cf54"; - if (envKey === undefined || envKey === "") { - return docverError("ระบบยังไม่ได้ตั้งค่า API key", 401); - } - const provided = request.headers.get("x-api-key"); - if (provided === null || provided === "") { - return docverError("API key ไม่ถูกต้อง", 401); - } - const providedHash = createHash("sha256").update(provided).digest(); - const envHash = createHash("sha256").update(envKey).digest(); - return timingSafeEqual(providedHash, envHash) - ? null - : docverError("API key ไม่ถูกต้อง", 401); -} - -/** 401 เดียวสำหรับทุกกรณี — ไม่แยกสาเหตุ (ไม่มี header / รูปผิด / signature ผิด / - * หมดอายุ / secret ไม่ตั้ง) กันใช้ endpoint เป็น oracle สำรวจ */ -const BEARER_DENIED = "โทเคนไม่ถูกต้องหรือหมดอายุ"; - -/** บังคับ Authorization: Bearer ของกลุ่ม docver — เรียกเป็นอย่างแรกของ - * ทุก endpoint ก่อน parse path param · env DOCVER_TOKEN_SECRET ไม่ตั้ง = ไม่มี - * token ใดผ่าน (fail closed — ไม่ใช่ 500 เพราะไม่ใช่ความผิดของผู้เรียก) */ -export function requireDocverBearer( - request: Request, -): { ok: true; claims: DocverTokenClaims } | { ok: false; response: Response } { - const denied = () => - ({ ok: false, response: docverError(BEARER_DENIED, 401) }) as const; - const secret = process.env.DOCVER_TOKEN_SECRET?.trim(); - // const secret = - // "487023dd1c1c4d6a07475e96b1cc300fec1e68ac9c2dd7d5ac9b1bf75da1053e"; - if (secret === undefined || secret === "") return denied(); - const header = request.headers.get("authorization"); - if (header === null) return denied(); - const match = /^Bearer\s+(\S+)$/i.exec(header); - if (match === null) return denied(); - const verified = verifyDocverToken(match[1], secret); - if (!verified.ok) return denied(); - return { ok: true, claims: verified.claims }; -} - -/** body ของ POST /api/documents/token — strict allowlist กัน typo เงียบ ๆ ตาม - * convention ของกลุ่ม (key อื่น = 400) · trim-ว่าง = ถือว่าไม่ส่ง */ -const TOKEN_BODY_FIELDS = ["userid", "username", "fid", "template"] as const; - -export function parseTokenRequestBody( - raw: unknown, -): { ok: true; claims: DocverTokenClaims } | DocverError { - if (raw === null || typeof raw !== "object" || Array.isArray(raw)) { - return { ok: false, error: "body ต้องเป็น JSON object" }; - } - const record = raw as Record; - for (const key of Object.keys(record)) { - if (!(TOKEN_BODY_FIELDS as readonly string[]).includes(key)) { - return { - ok: false, - error: `ไม่รู้จัก field '${key}' (รับเฉพาะ ${TOKEN_BODY_FIELDS.join(", ")})`, - }; - } - } - - const rawUserid = record.userid; - if (typeof rawUserid !== "string" || rawUserid.trim() === "") { - return { ok: false, error: "ต้องระบุ userid (string)" }; - } - const userid = rawUserid.trim(); - if (userid.length > 100) { - return { ok: false, error: "userid ยาวเกิน 100 ตัวอักษร" }; - } - - const rawUsername = record.username; - if (typeof rawUsername !== "string" || rawUsername.trim() === "") { - return { ok: false, error: "ต้องระบุ username (string)" }; - } - const username = rawUsername.trim(); - if (username.length > 200) { - return { ok: false, error: "username ยาวเกิน 200 ตัวอักษร" }; - } - - const rawFid = record.fid; - if (typeof rawFid !== "string" || rawFid.trim() === "") { - return { ok: false, error: "ต้องระบุ fid (string)" }; - } - const fid = rawFid.trim(); - if (fid.length > 200) { - return { ok: false, error: "fid ยาวเกิน 200 ตัวอักษร" }; - } - - const rawTemplate = record.template ?? null; - if (rawTemplate !== null && typeof rawTemplate !== "string") { - return { ok: false, error: "template ต้องเป็น string" }; - } - const template = rawTemplate?.trim() || null; - if (template !== null && template.length > 100) { - return { ok: false, error: "template ยาวเกิน 100 ตัวอักษร" }; - } - - return { ok: true, claims: { userid, username, fid, template } }; -} diff --git a/lib/lib-tmpl.ts b/lib/lib-tmpl.ts deleted file mode 100755 index bb293d0..0000000 --- a/lib/lib-tmpl.ts +++ /dev/null @@ -1,1314 +0,0 @@ -import { generateHmacSha256, signEd25519, sortObjectKeys } from '@/lib/lib-utils'; -import createReport from 'docx-templates'; -import PizzaZip from "pizzip"; -import { - DOMParser, - XMLSerializer, - type Document as XmlDocument, - type Element as XmlElement, - type Node as XmlNode, - type Attr as XmlAttr, -} from "@xmldom/xmldom"; -import xpath from "xpath"; -const WORD_NS = - "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; - -export interface ExtractedData { - [tagName: string]: string; -} - -const asXpathNode = (node: XmlNode): globalThis.Node => - node as unknown as globalThis.Node; - -function createSelector() { - return xpath.useNamespaces({ w: WORD_NS }); -} - - -/** - * ฟังก์ชันสำหรับดึงข้อมูลจาก docx ออกมาเป็น JSON โดยอิงจาก Bookmark (w:bookmarkStart) - */ -export function extractBookmark(docxBuffer: Buffer,): ExtractedData { - const zip = new PizzaZip(docxBuffer); - const docXmlFile = zip.file("word/document.xml"); - - if (!docXmlFile) { - throw new Error("Invalid docx file: word/document.xml not found."); - } - const docXmlText: string = docXmlFile.asText(); - - // 3. Parse ข้อความ XML ให้เป็น DOM Object - const parser = new DOMParser(); - const doc = parser.parseFromString(docXmlText, "text/xml"); - - // 4. ตั้งค่า Namespace ของ Word XML เพื่อใช้ค้นหาด้วย XPath - const select = xpath.useNamespaces({ - 'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' - }); - - const result: ExtractedData = {}; - - // ค้นหา Bookmark หรือ Tag ที่ใช้แบ่ง Section - // const bookmarks = select('//w:bookmarkStart', doc as unknown as globalThis.Node) as Element[]; - const bookmarks = select( - "//w:bookmarkStart", - doc as unknown as globalThis.Node - ) as unknown as XmlElement[]; - - bookmarks.forEach((bookmark: XmlElement) => { - const sectionName = bookmark.getAttribute('w:name'); - const bookmarkId = bookmark.getAttribute('w:id'); - - // กรองเอาเฉพาะ Bookmark ที่เราสนใจ (ไม่เอาพวกระบบสร้างเองอย่าง _GoBack) - if (sectionName && !sectionName.startsWith('_')) { - let extractedText = ""; - let currentNode: XmlNode | null = bookmark.nextSibling; - - // วนลูปอ่าน Node ถัดไปเรื่อยๆ จนกว่าจะเจอจุดสิ้นสุดของ Bookmark - while (currentNode) { - // ถ้าเจอจุดจบของ Bookmark ตัวเอง ให้หยุด - if ( - currentNode.nodeType === 1 && // Element node - (currentNode as XmlElement).nodeName === 'w:bookmarkEnd' && - (currentNode as XmlElement).getAttribute('w:id') === bookmarkId - ) { - break; - } - - // ดึง Text (w:t) ที่อยู่ภายใน Node ปัจจุบัน - const textNodes = select('.//w:t/text()', currentNode as unknown as globalThis.Node) as unknown as XmlNode[]; - textNodes.forEach((textNode: XmlNode) => { - if (textNode.nodeValue) { - extractedText += textNode.nodeValue; - } - }); - - currentNode = currentNode.nextSibling; - } - - // บันทึกลงผลลัพธ์ JSON - result[sectionName] = extractedText.trim(); - } - }); - - return result; -} - - - -/** - * อ่าน Content Control (w:sdt) จาก DOCX Buffer - * ปัจจุบันอ่านจาก word/document.xml ตาม logic ของโค้ดต้นฉบับ - */ -export function extractContentControls( - docxBuffer: Buffer, -): ExtractedData { - const zip = new PizzaZip(docxBuffer); - const docXmlFile = zip.file("word/document.xml"); - - if (!docXmlFile) { - throw new Error("Invalid docx file: word/document.xml not found."); - } - - const parser = new DOMParser(); - const doc = parser.parseFromString(docXmlFile.asText(), "text/xml"); - const select = createSelector(); - const result: ExtractedData = {}; - - const sdtNodes = select( - "//w:sdt", - asXpathNode(doc), - ) as unknown as XmlElement[]; - - sdtNodes.forEach((sdt) => { - const tagAttr = select( - "./w:sdtPr/w:tag/@w:val", - asXpathNode(sdt), - true, - ) as unknown as XmlAttr | undefined; - - const tagName = tagAttr?.value; - - if (!tagName) return; - - const sdtContent = select( - "./w:sdtContent", - asXpathNode(sdt), - true, - ) as unknown as XmlElement | undefined; - - if (!sdtContent) { - result[tagName] = ""; - return; - } - - const nodes = select( - ".//w:p | .//w:t | .//w:tab | .//w:br | .//w:noBreakHyphen | .//w:softHyphen | .//w:sym", - asXpathNode(sdtContent), - ) as unknown as XmlElement[]; - - let extractedText = ""; - - nodes.forEach((node) => { - switch (node.nodeName) { - case "w:p": - if ( - extractedText.length > 0 && - !extractedText.endsWith("\n") - ) { - extractedText += "\n\n"; - } - break; - - case "w:t": - extractedText += node.textContent || ""; - break; - - case "w:tab": - extractedText += "\t"; - break; - - case "w:br": - extractedText += "\n"; - break; - - case "w:noBreakHyphen": - extractedText += "\u2011"; - break; - - case "w:softHyphen": - extractedText += "\u00ad"; - break; - - case "w:sym": { - const charAttr = node.getAttribute("w:char"); - if (charAttr) { - const charCode = Number.parseInt(charAttr, 16); - extractedText += String.fromCharCode(charCode); - } - break; - } - } - }); - - result[tagName] = extractedText; - }); - - return result; -} - -type SdtControlType = "plain" | "rich" | "other"; -type SdtStructure = "block" | "inline"; - -function detectSdtControlType( - sdt: XmlElement, - select: xpath.XPathSelect, -): { type: SdtControlType; multiLine: boolean } { - const sdtPr = select( - "./w:sdtPr", - asXpathNode(sdt), - true, - ) as unknown as XmlElement | undefined; - - if (!sdtPr) { - return { type: "other", multiLine: false }; - } - - const textEl = select( - "./w:text", - asXpathNode(sdtPr), - true, - ) as unknown as XmlElement | undefined; - - if (textEl) { - const multiLineAttr = select( - "./@w:multiLine", - asXpathNode(textEl), - true, - ) as unknown as XmlAttr | undefined; - - return { - type: "plain", - multiLine: - multiLineAttr?.value === "1" || - multiLineAttr?.value === "true", - }; - } - - const richTextEl = select( - "./w:richText", - asXpathNode(sdtPr), - true, - ) as unknown as XmlElement | undefined; - - if (richTextEl) { - return { type: "rich", multiLine: true }; - } - - const otherTypeTags = [ - "w:date", - "w:dropDownList", - "w:comboBox", - "w:checkbox", - "w:citation", - "w:group", - "w:docPartObj", - "w:docPartList", - ]; - - for (const tag of otherTypeTags) { - const found = select( - `./${tag}`, - asXpathNode(sdtPr), - true, - ) as unknown as XmlElement | undefined; - - if (found) { - return { type: "other", multiLine: false }; - } - } - - return { type: "rich", multiLine: true }; -} - -function detectSdtStructure( - sdtContent: XmlElement, - select: xpath.XPathSelect, -): SdtStructure { - const directP = select( - "./w:p", - asXpathNode(sdtContent), - ) as unknown as XmlElement[]; - - return directP.length > 0 ? "block" : "inline"; -} - -function buildRunChildren( - doc: XmlDocument, - text: string, -): XmlElement[] { - const nodes: XmlElement[] = []; - const tokens = text.split(/(\t|\n)/); - - tokens.forEach((token) => { - if (token === "") return; - - if (token === "\t") { - nodes.push(doc.createElementNS(WORD_NS, "w:tab")); - } else if (token === "\n") { - nodes.push(doc.createElementNS(WORD_NS, "w:br")); - } else { - const t = doc.createElementNS(WORD_NS, "w:t"); - t.setAttribute("xml:space", "preserve"); - t.textContent = token; - nodes.push(t); - } - }); - - if (nodes.length === 0) { - const t = doc.createElementNS(WORD_NS, "w:t"); - t.setAttribute("xml:space", "preserve"); - t.textContent = ""; - nodes.push(t); - } - - return nodes; -} - -function buildRun( - doc: XmlDocument, - text: string, - rPr?: XmlElement, -): XmlElement { - const run = doc.createElementNS(WORD_NS, "w:r"); - - if (rPr) { - run.appendChild(rPr.cloneNode(true)); - } - - buildRunChildren(doc, text).forEach((node) => - run.appendChild(node), - ); - - return run; -} - -function insertInlineContent( - doc: XmlDocument, - sdtContent: XmlElement, - select: xpath.XPathSelect, - text: string, - allowLineBreak: boolean, -): void { - const safeText = allowLineBreak - ? text - : text.replace(/\n/g, " "); - - const existingRuns = select( - "./w:r", - asXpathNode(sdtContent), - ) as unknown as XmlElement[]; - - const firstRPr = - existingRuns.length > 0 - ? (select( - "./w:rPr", - asXpathNode(existingRuns[0]), - true, - ) as unknown as XmlElement | undefined) - : undefined; - - existingRuns.forEach((r) => { - if (r.parentNode) r.parentNode.removeChild(r); - }); - - sdtContent.appendChild( - buildRun(doc, safeText, firstRPr), - ); -} - -// ในฟังก์ชัน insertBlockContent() -function insertBlockContent( - doc: XmlDocument, - sdtContent: XmlElement, - select: xpath.XPathSelect, - text: string, - allowMultiParagraph: boolean, -): void { - const allParagraphs = select( - "./w:p", - asXpathNode(sdtContent), - ) as unknown as XmlElement[]; - - if (allParagraphs.length === 0) { - const p = doc.createElementNS(WORD_NS, "w:p"); - sdtContent.appendChild(p); - allParagraphs.push(p); - } - - const firstP = allParagraphs[0]; - - const firstPPr = select( - "./w:pPr", - asXpathNode(firstP), - true, - ) as unknown as XmlElement | undefined; - - const firstRun = select( - "./w:r", - asXpathNode(firstP), - true, - ) as unknown as XmlElement | undefined; - - const firstRPr = firstRun - ? (select( - "./w:rPr", - asXpathNode(firstRun), - true, - ) as unknown as XmlElement | undefined) - : undefined; - - // ลบย่อหน้าเดิมทั้งหมดยกเว้นย่อหน้าแรก - allParagraphs.forEach((p) => { - if (p !== firstP && p.parentNode) { - p.parentNode.removeChild(p); - } - }); - - // ลบ Run เดิมออกจากย่อหน้าแรก - const existingRuns = select( - "./w:r", - asXpathNode(firstP), - ) as unknown as XmlElement[]; - - existingRuns.forEach((r) => { - if (r.parentNode) r.parentNode.removeChild(r); - }); - - if (!allowMultiParagraph) { - // ถ้าไม่ใช่ออกแบบให้ข้ามย่อหน้าได้ ให้เปลี่ยน \n ทั้งหมดเป็น Space หรือ buildRun ปกติ - firstP.appendChild(buildRun(doc, text.replace(/\n+/g, "\n"), firstRPr)); - return; - } - - // แยกด้วย \n\n สำหรับ Paragraph Break (w:p) - const paragraphs = text.split("\n\n"); - - paragraphs.forEach((pText, index) => { - let targetP: XmlElement; - - if (index === 0) { - targetP = firstP; - } else { - targetP = doc.createElementNS(WORD_NS, "w:p"); - if (firstPPr) { - targetP.appendChild(firstPPr.cloneNode(true)); - } - sdtContent.appendChild(targetP); - } - - const run = doc.createElementNS(WORD_NS, "w:r"); - if (firstRPr) { - run.appendChild(firstRPr.cloneNode(true)); - } - - // pText จะมี \n เดี่ยวๆ หลงเหลืออยู่ ซึ่ง buildRunChildren จะแปลงเป็น w:br ให้โดยอัตโนมัติ - buildRunChildren(doc, pText).forEach((node) => - run.appendChild(node), - ); - - targetP.appendChild(run); - }); -} - -function insertFallbackContent( - sdtContent: XmlElement, - select: xpath.XPathSelect, - text: string, -): void { - const textNodes = select( - ".//w:t", - asXpathNode(sdtContent), - ) as unknown as XmlElement[]; - - if (textNodes.length === 0) return; - - textNodes[0].textContent = text; - - for (let i = 1; i < textNodes.length; i++) { - textNodes[i].textContent = ""; - } -} - -export type PropData = Record; -export type DocumentTemplate = {info?:PropData,dc?:PropData,cc:PropData,}; - -function normalizeValue(value: unknown): string { - if (value === null || value === undefined) { - return ""; - } - - if (typeof value === "string") { - return value; - } - - if ( - typeof value === "number" || - typeof value === "boolean" || - typeof value === "bigint" - ) { - return String(value); - } - - // รองรับ JSON ที่ส่ง array/object มา แต่ Content Control ต้องการ text - return JSON.stringify(value); -} - -/** - * นำ JSON data ไปแทนค่า Content Control ใน DOCX template - * คืนค่าเป็น Buffer ของ DOCX ที่สร้างเสร็จแล้ว - */ -export function templateContentControls( - templateBuffer: Buffer|Uint8Array, - data: PropData, -): Buffer { - const zip = new PizzaZip(templateBuffer); - const docXmlFile = zip.file("word/document.xml"); - - if (!docXmlFile) { - throw new Error("Invalid docx file: word/document.xml not found."); - } - - const parser = new DOMParser(); - const doc = parser.parseFromString(docXmlFile.asText(), "text/xml"); - const select = createSelector(); - - const sdtNodes = select( - "//w:sdt", - asXpathNode(doc), - ) as unknown as XmlElement[]; - - sdtNodes.forEach((sdt) => { - const tagAttr = select( - "./w:sdtPr/w:tag/@w:val", - asXpathNode(sdt), - true, - ) as unknown as XmlAttr | undefined; - - const tagName = tagAttr?.value; - - if (!tagName || !Object.prototype.hasOwnProperty.call(data, tagName)) { - return; - } - - const newValue = normalizeValue(data[tagName]); - - const sdtContent = select( - "./w:sdtContent", - asXpathNode(sdt), - true, - ) as unknown as XmlElement | undefined; - - if (!sdtContent) return; - - const { type: controlType, multiLine } = - detectSdtControlType(sdt, select); - - const structure = detectSdtStructure(sdtContent, select); - - if (controlType === "other") { - insertFallbackContent(sdtContent, select, newValue); - return; - } - - if (structure === "inline") { - const allowLineBreak = - controlType === "plain" ? multiLine : true; - - insertInlineContent( - doc, - sdtContent, - select, - newValue, - allowLineBreak, - ); - } else { - insertBlockContent( - doc, - sdtContent, - select, - newValue, - controlType === "rich", - ); - } - }); - - const serializer = new XMLSerializer(); - zip.file( - "word/document.xml", - serializer.serializeToString(doc), - ); - - return zip.generate({ - type: "nodebuffer", - compression: "DEFLATE", - }); -} - - -const CUSTOM_NS = - "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"; - -const VT_NS = - "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"; - -const REL_NS = - "http://schemas.openxmlformats.org/package/2006/relationships"; - -const CONTENT_TYPES_NS = - "http://schemas.openxmlformats.org/package/2006/content-types"; - -const XMLNS_NS = "http://www.w3.org/2000/xmlns/"; - -const CUSTOM_REL_TYPE = - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"; - -const CUSTOM_CONTENT_TYPE = - "application/vnd.openxmlformats-officedocument.custom-properties+xml"; - -const CUSTOM_FMTID = - "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"; - -const CUSTOM_PATH = "docProps/custom.xml"; - -type CustomPropertyValue = - | string - | number - | boolean - | Date - | null - | undefined; - -function parseXml(xml: string, fileName: string): XmlDocument { - const errors: string[] = []; - - const parser = new DOMParser(); - - const doc = parser.parseFromString( - xml, - "application/xml", - ) as XmlDocument; - - if (errors.length > 0 || !doc.documentElement) { - throw new Error( - `Invalid ${fileName}: ${errors.join("; ")}`, - ); - } - - return doc; -} - -function assertRoot( - doc: XmlDocument, - namespace: string, - localName: string, - fileName: string, -): XmlElement { - const root = doc.documentElement; - - if (root===null || - root.localName !== localName || - root.namespaceURI !== namespace - ) { - throw new Error(`Invalid root element in ${fileName}`); - } - - return root; -} - -function createCustomXml(): XmlDocument { - const xml = [ - '', - ``, - ].join(""); - - return parseXml(xml, CUSTOM_PATH); -} - -function createValueElement( - doc: XmlDocument, - value: unknown, -): XmlElement { - let tagName: string; - let text: string; - - if (value == null) { - // null และ undefined เก็บเป็น String ว่าง - tagName = "vt:lpwstr"; - text = ""; - } else if (typeof value === "string") { - tagName = "vt:lpwstr"; - text = value; - } else if (typeof value === "boolean") { - tagName = "vt:bool"; - text = value ? "true" : "false"; - } else if (typeof value === "number") { - if (!Number.isFinite(value)) { - throw new TypeError( - `Invalid numeric property value: ${value}`, - ); - } - - if (Number.isInteger(value)) { - if (!Number.isSafeInteger(value)) { - throw new TypeError( - `Integer property exceeds safe integer range: ${value}`, - ); - } - - tagName = "vt:i8"; - } else { - tagName = "vt:r8"; - } - - text = String(value); - } else if (value instanceof Date) { - if (Number.isNaN(value.getTime())) { - throw new TypeError("Invalid Date property value"); - } - - tagName = "vt:filetime"; - text = value.toISOString(); - } else { - throw new TypeError( - `Unsupported custom property type: ${ - Object.prototype.toString.call(value) - }`, - ); - } - - const element = doc.createElementNS(VT_NS, tagName); - - element.appendChild(doc.createTextNode(text)); - - return element; -} - -function ensureCustomRelationship(zip: PizzaZip): void { - const fileName = "_rels/.rels"; - const file = zip.file(fileName); - - if (!file) { - throw new Error(`DOCX is missing ${fileName}`); - } - - const doc = parseXml(file.asText(), fileName); - - const root = assertRoot( - doc, - REL_NS, - "Relationships", - fileName, - ); - - const relationships = Array.from( - root.getElementsByTagNameNS(REL_NS, "Relationship"), - ) as XmlElement[]; - - const existing = relationships.find( - (item) => item.getAttribute("Type") === CUSTOM_REL_TYPE, - ); - - if (existing) { - if (existing.getAttribute("Target") !== CUSTOM_PATH) { - throw new Error( - "Existing custom-properties relationship has an unexpected Target", - ); - } - - return; - } - - const ids = new Set( - relationships.map((item) => item.getAttribute("Id")), - ); - - let index = 1; - let relationshipId = `rId${index}`; - - while (ids.has(relationshipId)) { - index++; - relationshipId = `rId${index}`; - } - - const relationship = doc.createElementNS( - REL_NS, - "Relationship", - ); - - relationship.setAttribute("Id", relationshipId); - relationship.setAttribute("Type", CUSTOM_REL_TYPE); - relationship.setAttribute("Target", CUSTOM_PATH); - - root.appendChild(relationship); - - zip.file( - fileName, - new XMLSerializer().serializeToString(doc), - ); -} - -function ensureCustomContentType(zip: PizzaZip): void { - const fileName = "[Content_Types].xml"; - const file = zip.file(fileName); - - if (!file) { - throw new Error(`DOCX is missing ${fileName}`); - } - - const doc = parseXml(file.asText(), fileName); - - const root = assertRoot( - doc, - CONTENT_TYPES_NS, - "Types", - fileName, - ); - - const overrides = Array.from( - root.getElementsByTagNameNS( - CONTENT_TYPES_NS, - "Override", - ), - ) as XmlElement[]; - - const existing = overrides.find( - (item) => item.getAttribute("PartName") === "/docProps/custom.xml", - ); - - if (existing) { - existing.setAttribute("ContentType", CUSTOM_CONTENT_TYPE); - } else { - const override = doc.createElementNS( - CONTENT_TYPES_NS, - "Override", - ); - - override.setAttribute( - "PartName", - "/docProps/custom.xml", - ); - - override.setAttribute( - "ContentType", - CUSTOM_CONTENT_TYPE, - ); - - root.appendChild(override); - } - - zip.file( - fileName, - new XMLSerializer().serializeToString(doc), - ); -} -/** how to use -const templateBuffer = readFileSync("template.docx"); -const outputBuffer = setDocPropsCustom(templateBuffer, { - DocumentCode: "DOC-2026-001", - Department: "ฝ่ายเทคโนโลยีสารสนเทศ", - Revision: 2, - IsApproved: true, - PublishedAt: new Date(), -}); -writeFileSync("output.docx", outputBuffer); - */ -export function setCustomDocProps( - docxBuffer: Buffer | Uint8Array, - data: Record, -): Buffer { - const zip = new PizzaZip(docxBuffer); - - // อ่าน Custom Properties เดิม หรือสร้างใหม่หากไม่มี - const customFile = zip.file(CUSTOM_PATH); - - const doc = customFile - ? parseXml(customFile.asText(), CUSTOM_PATH) - : createCustomXml(); - - const root = assertRoot( - doc, - CUSTOM_NS, - "Properties", - CUSTOM_PATH, - ); - - // รับรองว่ามี Namespace vt สำหรับ Value Elements - root.setAttributeNS(XMLNS_NS, "xmlns:vt", VT_NS); - - const properties = Array.from( - root.getElementsByTagNameNS(CUSTOM_NS, "property"), - ) as XmlElement[]; - - const propertyMap = new Map(); - const usedPids = new Set(); - - for (const property of properties) { - const name = property.getAttribute("name"); - const pidText = property.getAttribute("pid"); - const pid = Number(pidText); - - if (!name) { - throw new Error( - "Custom property is missing its name attribute", - ); - } - - if (propertyMap.has(name)) { - throw new Error(`Duplicate custom property name: ${name}`); - } - - if (pidText === null || - !/^[1-9]\d*$/.test(pidText) || - !Number.isSafeInteger(pid) || - pid < 2 - ) { - throw new Error( - `Invalid custom property pid for "${name}": ${pidText}`, - ); - } - - if (usedPids.has(pid)) { - throw new Error(`Duplicate custom property pid: ${pid}`); - } - - propertyMap.set(name, property); - usedPids.add(pid); - } - - let nextPid = 2; - - for (const [name, value] of Object.entries(data)) { - if (!name.trim()) { - throw new Error("Custom property name must not be empty"); - } - - let property = propertyMap.get(name); - - if (!property) { - while (usedPids.has(nextPid)) { - nextPid++; - } - - property = doc.createElementNS( - CUSTOM_NS, - "property", - ); - - property.setAttribute("fmtid", CUSTOM_FMTID); - property.setAttribute("pid", String(nextPid)); - property.setAttribute("name", name); - - usedPids.add(nextPid); - nextPid++; - - root.appendChild(property); - propertyMap.set(name, property); - } - - // สร้างค่าใหม่ก่อนแก้ไข Property เดิม - const valueElement = createValueElement(doc, value); - - while (property.firstChild) { - property.removeChild(property.firstChild); - } - - property.appendChild(valueElement); - } - - // อัปเดตไฟล์ Custom Properties - zip.file( - CUSTOM_PATH, - new XMLSerializer().serializeToString(doc), - ); - - // ตรวจสอบส่วนประกอบที่เชื่อมโยงกับ Custom Properties - ensureCustomRelationship(zip); - ensureCustomContentType(zip); - - return Buffer.from( - zip.generate({ - type: "nodebuffer", - compression: "DEFLATE", - }), - ); -} - -/** - * ดึงค่า Custom Document Properties จาก DOCX Buffer - */ -export function getCustomDocProps( - docxBuffer: Buffer | Uint8Array, -): Record { - const zip = new PizzaZip(docxBuffer); - const customFile = zip.file(CUSTOM_PATH); - - if (!customFile) { - return {}; - } - - const doc = parseXml(customFile.asText(), CUSTOM_PATH); - const root = assertRoot( - doc, - CUSTOM_NS, - "Properties", - CUSTOM_PATH, - ); - - const properties = Array.from( - root.getElementsByTagNameNS(CUSTOM_NS, "property"), - ) as XmlElement[]; - - const result: Record = {}; - - for (const property of properties) { - const name = property.getAttribute("name"); - if (!name) continue; - - // หา child element แรกที่อยู่ใน VT_NS namespace - let valueNode: XmlElement | undefined; - for (let i = 0; i < property.childNodes.length; i++) { - const node = property.childNodes[i] as XmlElement; - if (node.nodeType === 1 && node.namespaceURI === VT_NS) { - valueNode = node; - break; - } - } - - if (!valueNode) { - result[name] = ""; - continue; - } - - const textContent = valueNode.textContent || ""; - - // แปลงค่ากลับตาม VT Data Type - switch (valueNode.localName) { - case "bool": - result[name] = textContent === "true" || textContent === "1"; - break; - - case "i1": - case "i2": - case "i4": - case "i8": - case "int": - case "ui1": - case "ui2": - case "ui4": - case "ui8": - case "uint": - case "r4": - case "r8": - case "decimal": { - const num = Number(textContent); - result[name] = Number.isNaN(num) ? textContent : num; - break; - } - - case "filetime": - case "date": { - const date = new Date(textContent); - result[name] = Number.isNaN(date.getTime()) ? textContent : date; - break; - } - - default: - // vt:lpwstr, vt:lpstr, vt:bstr และอื่นๆ - result[name] = textContent; - break; - } - } - - return result; -} - -const CORE_NS = - "http://schemas.openxmlformats.org/package/2006/metadata/core-properties"; -const DC_NS = "http://purl.org/dc/elements/1.1/"; -const DCTERMS_NS = "http://purl.org/dc/terms/"; -const APP_NS = - "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"; - -export interface DocPropsOptions { - standard?: boolean; - extended?: boolean; - custom?: boolean; -} - -export interface StandardDocProps { - title?: string; - subject?: string; - creator?: string; - keywords?: string; - description?: string; - lastModifiedBy?: string; - revision?: string; - created?: Date; - modified?: Date; - category?: string; - contentStatus?: string; -} - -export interface ExtendedDocProps { - template?: string; - totalTime?: number; - pages?: number; - words?: number; - characters?: number; - charactersWithSpaces?: number; - lines?: number; - paragraphs?: number; - application?: string; - appVersion?: string; - company?: string; - manager?: string; - docSecurity?: number; -} - -export interface DocPropsResult { - standard?: StandardDocProps; - extended?: ExtendedDocProps; - custom?: Record; -} - -/** - * ดึงข้อมูล Properties ของเอกสาร DOCX ตามประเภทที่เลือก - * example: - * const allProps = getDocProps(docxBuffer); - * const selectedProps = getDocProps(docxBuffer, { - * standard: true, - * custom: true, - * extended: false, - * }); - * console.log(allProps); - * console.log(selectedProps.standard?.title); - */ - -export function getDocProps( - docxBuffer: Buffer | Uint8Array, - options: DocPropsOptions = { standard: true, extended: true, custom: true }, -): DocPropsResult { - const zip = new PizzaZip(docxBuffer); - const result: DocPropsResult = {}; - - // 1. Standard / Core Properties (docProps/core.xml) - if (options.standard) { - const coreFile = zip.file("docProps/core.xml"); - if (coreFile) { - const doc = parseXml(coreFile.asText(), "docProps/core.xml"); - const getText = (ns: string, localName: string) => { - const els = doc.getElementsByTagNameNS(ns, localName); - return els.length > 0 ? els[0].textContent || "" : undefined; - }; - - const parseDate = (dateStr?: string) => { - if (!dateStr) return undefined; - const d = new Date(dateStr); - return Number.isNaN(d.getTime()) ? undefined : d; - }; - - result.standard = { - title: getText(DC_NS, "title"), - subject: getText(DC_NS, "subject"), - creator: getText(DC_NS, "creator"), - keywords: getText(CORE_NS, "keywords"), - description: getText(DC_NS, "description"), - lastModifiedBy: getText(CORE_NS, "lastModifiedBy"), - revision: getText(CORE_NS, "revision"), - created: parseDate(getText(DCTERMS_NS, "created")), - modified: parseDate(getText(DCTERMS_NS, "modified")), - category: getText(CORE_NS, "category"), - contentStatus: getText(CORE_NS, "contentStatus"), - }; - } - } - - // 2. Extended / Automatically Updated Properties (docProps/app.xml) - if (options.extended) { - const appFile = zip.file("docProps/app.xml"); - if (appFile) { - const doc = parseXml(appFile.asText(), "docProps/app.xml"); - const getText = (localName: string) => { - const els = doc.getElementsByTagNameNS(APP_NS, localName); - return els.length > 0 ? els[0].textContent || "" : undefined; - }; - - const parseNum = (val?: string) => { - if (!val) return undefined; - const n = Number(val); - return Number.isNaN(n) ? undefined : n; - }; - - result.extended = { - template: getText("Template"), - totalTime: parseNum(getText("TotalTime")), - pages: parseNum(getText("Pages")), - words: parseNum(getText("Words")), - characters: parseNum(getText("Characters")), - charactersWithSpaces: parseNum(getText("CharactersWithSpaces")), - lines: parseNum(getText("Lines")), - paragraphs: parseNum(getText("Paragraphs")), - application: getText("Application"), - appVersion: getText("AppVersion"), - company: getText("Company"), - manager: getText("Manager"), - docSecurity: parseNum(getText("DocSecurity")), - }; - } - } - - // 3. Custom Properties (docProps/custom.xml) - if (options.custom) { - result.custom = getCustomDocProps(docxBuffer); - } - - return result; -} - - -//**************************** dynamic content ******************************************* -// sdtLocked(ห้ามลบ CC), contentLocked(ลบได้ห้ามแก้), sdtContentLocked(ห้ามทุกอย่าง) -type LockType = 'none' | 'sdtLocked' | 'contentLocked' | 'sdtContentLocked'; - -/** - * ฟังก์ชันสร้าง Plain Text Content Control XML String ครอบด้วย || - * ระวังการเกิดหลายรัน จะทำให้ xml เสีย - */ -function p_cc(title:string,tag:string,text:string='',isMultiLine:boolean=true,lock:LockType='sdtLocked'): string { - const lockTag = lock=='none'?"":`` - const multiLineTag = isMultiLine ? '':'' - const content = escapeXml(text) - const xmlString =`${lockTag}${multiLineTag}${content}` - return `||${xmlString}||`; -} -function br(n: number) -{ - let text="" - for(let i=0;i||`; -} -/** - * Escapes XML Special Characters - */ -function escapeXml(unsafe: string): string { - return unsafe - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} -export const templateDynamicContents = async (template: Buffer, data: Record): Promise => { - return await createReport({ - template, data, cmdDelimiter: ['{{', '}}'], - additionalJsContext: { - bm,br, - p_cc, - } - }); -} - -// สร้างเอกสารจาก เทมเพลต+data render และ generate ใช้งานเหมือนกัน -// ยังไม่ค่อยแน่ใจว่าควรอยู่ใน library นี้หรือเปล่าเพราะมีการทำ hash/sign ticket_data -// - ticket_data ต้องมีข้อมูลสมบูรณ์ {fid,tid,uid,uname} -// - สร้าง signature/hash จาก ticket_data -// - สร้าง Dynamic Content (docx-templates) -// - เอาข้อมูลใส่ Content Control -export async function applyTemplate(templateBuffer:Buffer,templateData:DocumentTemplate, - secret:string,private_key:string,ttl:number):Promise> -{ - //Calculate hash - const originalTicket_data = templateData?.info?.ticket_data //expect valid json string - // console.log("templateData",templateData?.info?.ticket_data) - if (originalTicket_data && typeof originalTicket_data === 'string') { - try { - const originalTicketObj = JSON.parse(originalTicket_data)//sort and add expire to ticket data - if (originalTicketObj && typeof originalTicketObj === 'object') { - console.log("Found valid ticket data: hash it") - // คำนวณค่า expire (ปัจจุบัน + จำนวนวินาทีที่ต้องการให้หมดอายุ) - // หาร 1000 เพราะต้องการหน่วยวินาที (sec) ไม่ใช่มิลลิวินาที (ms) - const expireTimestamp = Math.floor(Date.now() / 1000) + ttl; - const fullObj = { ...originalTicketObj, exp: expireTimestamp }; - const sortedObj = sortObjectKeys(fullObj); - - // แปลงกลับเป็น JSON string เพื่อนำไปใช้ส่งและทำ Hash - const ticket_data = JSON.stringify(sortedObj); - // console.log("check secret:",TEST_SECRET,TEST_PRIVATE_KEY_HEX,ticket_data) - const HmacSha256 = generateHmacSha256(ticket_data, secret) - - const Ed25519 = signEd25519(ticket_data, private_key) - templateBuffer = setCustomDocProps(templateBuffer, { ticket_data, HmacSha256, Ed25519 }) - } - } catch (error) { //just skip - console.error("Set custom property fail",error); - } - } - - // Apply Dynamic Content - if (templateData?.dc && typeof templateData?.dc === "object") { - console.log("Apply Dynamic Content") - templateBuffer = Buffer.from(await templateDynamicContents(templateBuffer, templateData.dc)) - } - // Apply Content Control - if (templateData?.cc && typeof templateData?.cc === "object") { - console.log("Apply Content Control") - templateBuffer = templateContentControls(templateBuffer, templateData.cc); - } - return templateBuffer - -} \ No newline at end of file diff --git a/lib/lib-utils.ts b/lib/lib-utils.ts deleted file mode 100644 index 312d3cf..0000000 --- a/lib/lib-utils.ts +++ /dev/null @@ -1,112 +0,0 @@ - -import { createHmac, createPrivateKey, createPublicKey, timingSafeEqual, verify, sign } from 'node:crypto'; -/** - * คำนวณ HMAC-SHA256 จาก string หรือ JSON Object - */ -export function generateHmacSha256(data: string | object, secretKey: string): string { - const payload = typeof data === 'string' ? data : JSON.stringify(data); - return createHmac('sha256', secretKey) - .update(payload, 'utf8') - .digest('hex'); -} - -/** - * ตรวจสอบความถูกต้องของ HMAC (ใช้ timingSafeEqual เพื่อป้องกัน Timing Attacks) - */ -export function verifyHmacSha256(data: string | object, secretKey: string, expectedHmac: string): boolean { - const calculatedHmac = generateHmacSha256(data, secretKey); - const a = Buffer.from(calculatedHmac, 'hex'); - const b = Buffer.from(expectedHmac, 'hex'); - - if (a.length !== b.length) { - return false; - } - return timingSafeEqual(a, b); -} -// ========================================== -// ตัวอย่างการใช้งาน -// ========================================== -/* -const secret = 'my-super-secret-key'; -const jsonData = { - userId: 12345, - action: 'transfer', - amount: 500 -}; - -// 1. สร้าง HMAC -const hmacSignature = generateHmacSha256(jsonData, secret); -console.log('HMAC Signature (Hex):', hmacSignature); - -// 2. ตรวจสอบ HMAC -const isValid = verifyHmacSha256(jsonData, secret, hmacSignature); -console.log('Is Signature Valid?:', isValid); // true -*/ - - -/** - * ฟังก์ชันสำหรับลงนามดิจิทัล (Sign) ข้อมูลด้วย Ed25519 Private Key (Hex) - * @param message ข้อความดิบที่ต้องการเซ็น (string หรือ Buffer) - * @param privateKeyHex คีย์ลับในรูปแบบ Hex String (ความยาว 64 ตัวอักษร) - * @returns ลายเซ็น (Signature) ในรูปแบบ Hex String (ความยาว 128 ตัวอักษร) - */ -export function signEd25519(message: string | Buffer, privateKeyHex: string): string { - const rawKeyBuffer = Buffer.from(privateKeyHex, 'hex'); - // ตัดเอาเฉพาะ 32 bytes (Seed) กรณีที่ Hex ยาว 64 bytes (ที่ต่อกับ Public Key มา) - const seed = rawKeyBuffer.length === 64 ? rawKeyBuffer.subarray(0, 32) : rawKeyBuffer; - // PKCS#8 Header สำหรับ Ed25519 Private Key - const pkcs8Header = Buffer.from('302e020100300506032b657004220420', 'hex'); - const pkcs8Buffer = Buffer.concat([pkcs8Header, seed]); - - const privateKey = createPrivateKey({ - key: pkcs8Buffer, - format: 'der', - type: 'pkcs8', - }); - - const dataBuffer = typeof message === 'string' ? Buffer.from(message, 'utf-8') : message; - const signatureBuffer = sign(null, dataBuffer, privateKey); - return signatureBuffer.toString('hex'); -} - -/** - * ฟังก์ชันสำหรับตรวจสอบลายเซ็น (Verify) ด้วย Ed25519 Public Key (Hex) - * @param message ข้อความดิบที่ใช้ในการเซ็น (string หรือ Buffer) - * @param signatureHex ลายเซ็นในรูปแบบ Hex String - * @param publicKeyHex คีย์สาธารณะในรูปแบบ Hex String (ความยาว 64 ตัวอักษร) - * @returns ผลลัพธ์ความถูกต้อง (true = ผ่าน, false = ไม่ผ่าน) - */ -export function verifyEd25519(message: string | Buffer, signatureHex: string, publicKeyHex: string): boolean { - try { - const rawPublicKey = Buffer.from(publicKeyHex, 'hex'); - - // SPKI Header สำหรับ Ed25519 Public Key (32 bytes) - const spkiHeader = Buffer.from('302a300506032b6570032100', 'hex'); - const spkiBuffer = Buffer.concat([spkiHeader, rawPublicKey]); - - const publicKey = createPublicKey({ - key: spkiBuffer, - format: 'der', - type: 'spki', - }); - - const dataBuffer = typeof message === 'string' ? Buffer.from(message, 'utf-8') : message; - const signatureBuffer = Buffer.from(signatureHex, 'hex'); - - return verify(null, dataBuffer, publicKey, signatureBuffer); - } catch (error) { - return false; - } -} - -/** - * ฟังก์ชันสำหรับเรียงคีย์ของ Object ตามตัวอักษรเพื่อไม่ให้โครงสร้าง JSON เพี้ยน - */ -export function sortObjectKeys(obj: Record): Record { - return Object.keys(obj) - .sort() - .reduce((result: Record, key: string) => { - result[key] = obj[key]; - return result; - }, {}); -} \ No newline at end of file diff --git a/lib/openapi.ts b/lib/openapi.ts index a407cdf..4cd07d3 100644 --- a/lib/openapi.ts +++ b/lib/openapi.ts @@ -32,17 +32,15 @@ export const openApiSpec = { { name: "template", description: - "Next Generation of Template : เป็นเทมเพลต MS Word(docx) รองรับรูปแบบข้อมูลที่ซับซ้อนได้ ", + "Template เอกสาร DOCX ด้วย Content Control — ส่ง template (.docx) + data (JSON) " + + "แล้วได้เอกสารที่กรอกค่าแล้วกลับมา", }, { name: "documents", description: - "จัด version เอกสาร (docver): เอกสารเดียวต่อ docId แต่ละ " + - "version เก็บ .docx + .pdf ใน MinIO พร้อมผู้อัปโหลด — auth ด้วย access_token " + - "ของกลุ่มนี้: ขอที่ POST /api/documents/token (ยืนยัน x-api-key กับ env " + - "DOCVER_API_KEY) แล้วแนบ Authorization: Bearer ทุก request — identity/template " + - "และ docId (= fid — fileId ของ e-Court, string) แกะจาก claims ของ token ทั้งหมด " + - "ไม่รับจาก URL/multipart form อีก", + "จัด version เอกสาร (docver — docs/PLAN-DOCVER.md): เอกสารเดียวต่อ docId แต่ละ " + + "version เก็บ .docx + .pdf ใน MinIO พร้อมผู้อัปโหลด — ไม่มี auth (stand-alone): " + + "ผู้เรียกบอก userId/userName เองใน multipart", }, { name: "Judge's Note", @@ -74,15 +72,53 @@ export const openApiSpec = { }, }, }, + "/api/template/extract": { + post: { + tags: ["template"], + summary: "ดึงค่า Content Control ทั้งหมดออกมาจากไฟล์ .docx", + description: + "อ่าน w:sdt (Content Control) ทุกตัวใน word/document.xml — คืน map " + + "ชื่อแท็ก (w:tag) → ข้อความปัจจุบันในนั้น (ใช้สำรวจว่า template มีแท็กอะไรบ้างก่อนเรียก render)", + requestBody: { + required: true, + content: { + "multipart/form-data": { + schema: { + type: "object", + required: ["file"], + properties: { + file: { + type: "string", + format: "binary", + description: "ไฟล์ .docx ที่มี Content Control", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + description: "รายการแท็กพร้อมข้อความ", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/ExtractResponse" }, + }, + }, + }, + 400: { $ref: "#/components/responses/BadRequest" }, + 500: { $ref: "#/components/responses/ServerError" }, + }, + }, + }, "/api/template/render": { post: { tags: ["template"], - summary: "สร้างเอกสารจาก template (.docx) + data (JSON) ใช้สำหรับนักพัฒนาเท่านั้น", + summary: "สร้างเอกสารจาก template (.docx) + data (JSON)", description: "แทนค่า JSON ลงใน Content Control ของ template แล้วคืนไฟล์ .docx ใหม่ " + '(filename ตอน download = generated-document.docx) — ส่ง data ได้ 2 ทาง: ' + - 'field "data" เป็นไฟล์ JSON หรือ field "dataJson" เป็น string ตรง ๆ (เลือกอย่างใดอย่างหนึ่ง)
'+ - 'สำหรับนักพัฒนา Template เท่านั้น ไม่ต้อง ใช้ API Key ต้องตั้งค่า secret และ private key เองไม่ใช้ของระบบของระบบ', + 'field "data" เป็นไฟล์ JSON หรือ field "dataJson" เป็น string ตรง ๆ (เลือกอย่างใดอย่างหนึ่ง)', requestBody: { required: true, content: { @@ -104,23 +140,9 @@ export const openApiSpec = { dataJson: { type: "string", description: - "JSON เป็น string ตรง ๆ (ทางเลือกแทนการแนบไฟล์ data)
{\"info:\"{\"ticket_data\":\"JSON String\"},\"dc:\"{..},\"dc:\"{\"blackyear\": \"๙๐/๒๕๖๙\",...}}", + "JSON เป็น string ตรง ๆ (ทางเลือกแทนการแนบไฟล์ data)", example: - '', - }, - secret: { - type: "string", - description: - "ค่า secret สำหรับทดสอบการทำ HMAC-SHA256", - example: - 'super_secret', - }, - private_key: { - type: "string", - description: - "Private key ในรูปแบบ HEX
openssl genpkey -algorithm ED25519 -out private.pem
openssl pkey -in private.pem -outform DER | tail -c 32 | xxd -p -c 32 > private.hex
openssl pkey -in private.pem -pubout -outform DER | tail -c 32 | xxd -p -c 32 > public.hex", - example: - '8f2077b57b39399b6a7828f35da03a63ade8e3a36a6bf437b091a8bdb3d18ee1', + '{"notname":"คำสั่งเรียก ให้ทำคำให้การ","blackno":"คดีหมายเลขดำที่","blackyear":"๑๐/๒๕๖๙"}', }, }, }, @@ -141,410 +163,6 @@ export const openApiSpec = { }, }, }, - "/api/template/extract": { - post: { - tags: ["template"], - summary: "ดึงค่า Content Control ทั้งหมดออกมาจากไฟล์ .docx ใช้สำหรับนักพัฒนาเท่านั้น", - description: - "อ่าน Content Control, bookmark , property ของ MS Word(docx) ออกมาแสดง" + - "ค่าของ Content Control จะเป็น Plain Text ไม่มี markup ซึ่งเหมาะกับการเอาไปใส่ในฐานข้อมูล"+ - 'สำหรับนักพัฒนา Template เท่านั้น ไม่ต้อง ใช้ API Key ต้องตั้งค่า secret และ Public key เองไม่ใช้ของระบบของระบบ', - requestBody: { - required: true, - content: { - "multipart/form-data": { - schema: { - type: "object", - required: ["file"], - properties: { - file: { - type: "string", - format: "binary", - description: "ไฟล์ .docx ที่มี Content Control", - }, - secret: { - type: "string", - description: - "ค่า secret สำหรับ validate HMAC-SHA256 กับข้อมูลใน custom property", - example: - "super_secret", - }, - public_key: { - type: "string", - description: - "Public key ในรูปแบบ HEX
openssl genpkey -algorithm ED25519 -out private.pem
openssl pkey -in private.pem -outform DER | tail -c 32 | xxd -p -c 32 > private.hex
openssl pkey -in private.pem -pubout -outform DER | tail -c 32 | xxd -p -c 32 > public.hex", - example: - "66b09b4a1b66271d5728c592c2409657e1c070a7bf371bf9f6ed457855270546", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - description: "รายการแท็กพร้อมข้อความ", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/ExtractResponse" }, - }, - }, - }, - 400: { $ref: "#/components/responses/BadRequest" }, - 500: { $ref: "#/components/responses/ServerError" }, - }, - }, - }, - "/api/template/generate": { - post: { - tags: ["template"], - summary: "สร้างเอกสาร DOCX จากข้อมูล JSON และ template ที่กำหนด", - description: - "เทมเพลตมีแท็กแบบพิเศษ(รองรับตัวแปร ลูป เงื่อนไข), Content Control เมื่อรวมกับข้อมูล(JSON) "+ - "จะได้เอกสาร รูปแบบฟอร์มที่กรอกค่าแล้วดึงค่ากลับมาใช้งานต่อได้เช่นอัปเดตฐานข้อมูล
    "+ - "
  • JSON payload ต้องมี info.ticket_data เป็น JSON string รูปแบบถูกต้อวมีค่าครบ(tid, fid และ uid)
  • " + - "
  • ไฟล์เทมเพลต(tid)ในระบบ + ข้อมูล(payload) เพื่อสร้างเอกสาร
  • " + - "
  • ticket_data จะทำการ hash/sign แบบอายุสั้นแล้วนำไปฝังไว้ใน custom property เพื่อนำไปใช้ร่วมกับระบบ Versioning
  • "+ - "
", - security: [{ ApiKeyAuth: [] }], - requestBody: { - required: true, - content: { - "application/json": { - schema: { - type: "object", - required: ["info"], - properties: { - info: { - type: "object", - required: ["ticket_data"], - properties: { - ticket_data: { - type: "string", - description: - "JSON string ที่ต้อง parse ได้ และต้องมี tid, fid และ uid", - example: - '{"tid":"T12","fid":"DOC-2569-0001","uid":"123"}', - }, - }, - additionalProperties: true, - }, - }, - additionalProperties: true, - }, - example: { - "info": { - "ticket_data": "{\"fid\":\"T12_b2_IN\",\"tid\":\"T12_X\",\"uid\":\"oom\",\"uname\":\"สรวิชญ์\"}" - }, - "dc": { - "Interpleader": true, - "docname": "", - "docabbr": "(ต. ๑๒)", - "blackno": "คดีหมายเลขดำที่", - "redno": "คดีหมายเลขแดงที่", - "court": "ศาลปกครองนครราชสีมา", - "panels": [ - { - "pname": "นายวิรุฬห์ จินตนะกุล", - "pmname": "ตุลาการศาลปกครองกลาง", - "pm": "ตุลาการเจ้าของสำนวน" - }, - { - "pname": "นายสัมฤทธิ์ อ่อนคำ", - "pmname": "ตุลาการศาลปกครองสูงสุด" - } - ], - "pnameT": "", - "pmnameT": "", - "pmT": "", - "acc_name": "" - - }, - "cc": { - "blackyear": "๙๐/๒๕๖๙", - "redyear": "", - "curdate": "วันที่ เดือน สิงหาคม พุทธศักราช ๒๕๖๙", - "accuser": "นายธีรพล ร่มเริง", - "acc1": "ผู้ฟ้องคดี", - "accused": "รร", - "acc2": "ผู้ถูกฟ้องคดี", - "interpleader": "สมชาย ใจดี", - "acc3": "ผู้ร้องสอด", - "detail1": "ตุลาการเจ้าของสำนวนได้พิจารณาคำฟ้องและเอกสารประกอบคำฟ้องแล้วเห็นว่าคดีนี้เป็นคดีพิพาทซึ่งไม่อยู่ในอำนาจศาลปกครองสูงสุด ผู้ฟ้องเป็นผู้มีสิทธิฟ้องคดี คดีนี้มีกฎหมายกำหนดให้ต้องดำเนินการแก้ไขความเดือดร้อนหรือเสียหายก่อนฟ้องคดี และผู้ฟ้องคดีได้ดำเนินการครบถ้วน โดยเป็นการยื่นฟ้องคดีภายในระยะเวลาการฟ้องคดีตาม พรบ. จัดตั้งศาลปกครองและวิธีพิจารณาคดีปกครองฯ คดีนี้มีค่าธรรมเนียมที่ต้องชำระ\nโดยผู้ฟ้องคดีได้ชำระค่าธรรมเนียมครบถ้วนแล้ว รวมทั้งคดีนี้ไม่เป็นการฟ้องซ้อน การดำเนินกระบวนพิจารณาซ้ำ และการฟ้องซ้ำ\n\nจึงมีคำสั่งรับคำฟ้องไว้พิจารณาและสั่งให้ส่งสำเนาคำฟ้องพร้อมทั้งสำเนาพยานหลักฐานไปยังผู้ถูกฟ้องคดีเพื่อให้ผู้ถูกฟ้องคดีทำคำให้การยื่นต่อศาลภายใน ๓๐ วัน นับแต่วันที่ได้รับคำสั่ง" - } - }, - }, - }, - }, - responses: { - 200: { - description: "ไฟล์ DOCX ที่สร้างสำเร็จ", - headers: { - "Content-Disposition": { - description: "ชื่อไฟล์สำหรับดาวน์โหลด", - schema: { type: "string" }, - }, - "Cache-Control": { - description: "ป้องกันการ cache response", - schema: { type: "string", example: "no-store, max-age=0" }, - }, - }, - content: { - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": { - schema: { type: "string", format: "binary" }, - }, - }, - }, - 400: { - description: - "JSON ไม่มี info.ticket_data, ticket_data ไม่ใช่ string, JSON string ไม่ถูกต้อง หรือไม่มี tid/fid/uid", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Error" }, - examples: { - missingTicketData: { - value: { - error: - "template/generate json.info.ticket_data not avaliable", - }, - }, - missingIdentifiers: { - value: { - error: - "template/generate require json.info.ticket_data.(fid|tid|uid)", - }, - }, - }, - }, - }, - }, - 401: { - description: "ไม่ได้ส่ง x-api-key หรือ API key ไม่ถูกต้อง", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Error" }, - example: { error: "Unauthorized: Invalid API Key" }, - }, - }, - }, - 404: { - description: "ไม่สามารถดึงข้อมูลขึ้นมาได้", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Error" }, - example: { error: "NotFound: Invalid template ID or incorrect storage path" }, - }, - }, - }, - 500: { - description: - "เกิดข้อผิดพลาดระหว่างอ่าน template หรือสร้างเอกสาร", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Error" }, - example: { error: "Failed to generate document payload" }, - }, - }, - }, - }, - }, - }, - "/api/template/download-docx": { - get: { - tags: ["template"], - summary: "ดาวน์โหลดเอกสาร DOCX ผ่าน Query Parameters (GET) ใช้เพื่อเป็นตัวอย่างจะถูกเอาออกในภายหลัง", - description: - "รับค่า fid, tid, uid, uname จาก Query Parameter อ่านข้อมูล JSON ประจำไฟล์ fid จาก TMPL_PATH แล้วส่งไปประมวลผลต่อที่ /api/template/generate เพื่อสร้างและคืนค่าเป็นไฟล์ DOCX stream ใช้เพื่อเป็นตัวอย่างสำหรับนักพัฒนา ต้องนำออกก่อน production", - parameters: [ - { - name: "fid", - in: "query", - required: true, - schema: { type: "string" }, - description: "ID เอกสาร (File ID)", - example: "T_no_warning", - }, - { - name: "tid", - in: "query", - required: true, - schema: { type: "string" }, - description: "ID เทมเพลต (Template ID)", - example: "T_X", - }, - { - name: "uid", - in: "query", - required: true, - schema: { type: "string" }, - description: "รหัสผู้ใช้งาน (User ID)", - example: "123", - }, - { - name: "uname", - in: "query", - required: true, - schema: { type: "string" }, - description: "ชื่อผู้ใช้งาน (User Name)", - example: "สมชาย ใจดี", - }, - ], - responses: { - 200: { - description: "ไฟล์ DOCX stream ที่สร้างสำเร็จ", - headers: { - "Content-Disposition": { - description: "ชื่อไฟล์สำหรับการดาวน์โหลด (attachment)", - schema: { type: "string", example: 'attachment; filename="T_no_warning_123_xxx.docx"' }, - }, - "Cache-Control": { - description: "การควบคุม Cache (no-store)", - schema: { type: "string", example: "no-store, max-age=0" }, - }, - }, - content: { - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": { - schema: { type: "string", format: "binary" }, - }, - }, - }, - 400: { - description: "พารามิเตอร์ไม่ครบถ้วน หรือรูปแบบไฟล์ JSON ไม่ถูกต้อง", - content: { - "text/plain": { - schema: { type: "string" }, - examples: { - missingParams: { value: "Missing fid,tid,uid or uname" }, - invalidData: { value: "Invalid data format" }, - }, - }, - }, - }, - 404: { - description: "ไม่พบไฟล์ข้อมูล JSON ใน TMPL_PATH", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Error" }, - example: { error: "Data not found: /path/to/TMPL_PATH/T_no_warning.json" }, - }, - }, - }, - 502: { - description: "เกิดข้อผิดพลาดในการเรียกไปยังบริการ Docedit /api/template/generate", - content: { - "text/plain": { - schema: { type: "string" }, - example: "Error from docedit generate: Expected docx stream but received text/html (Status 500)", - }, - }, - }, - 500: { - description: "เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์", - content: { - "text/plain": { - schema: { type: "string" }, - example: "Internal Server Error", - }, - }, - }, - }, - }, - post: { - tags: ["template"], - summary: "ดาวน์โหลดเอกสาร DOCX ผ่าน HTML Form / Multipart Form Data (POST) เป็นตัวอย่างเท่านั้น จะถูกเอาออกในภายหลัง", - description: - "รับค่า fid, tid, uid, uname จาก Multipart Form Data หรือ Form-urlencoded อ่านข้อมูล JSON และประมวลผลสร้างไฟล์ DOCX stream คืนกลับไป "+ - "จะเรียก generate ในเบื้องหลัง ใช้เพื่อเป็นตัวอย่างสำหรับนักพัฒนา ต้องนำออกก่อน production", - requestBody: { - required: true, - content: { - "multipart/form-data": { - schema: { - type: "object", - required: ["fid", "tid", "uid", "uname"], - properties: { - fid: { type: "string", description: "ID เอกสาร (File ID)",example:"T12_b2_IN" }, - tid: { type: "string", description: "ID เทมเพลต (Template ID)",example:"T12_X" }, - uid: { type: "string", description: "รหัสผู้ใช้งาน (User ID)", example:"john" }, - uname: { type: "string", description: "ชื่อผู้ใช้งาน (User Name)", example:"John Doe"}, - }, - }, - }, - "application/x-www-form-urlencoded": { - schema: { - type: "object", - required: ["fid", "tid", "uid", "uname"], - properties: { - fid: { type: "string", description: "ID เอกสาร (File ID)" }, - tid: { type: "string", description: "ID เทมเพลต (Template ID)" }, - uid: { type: "string", description: "รหัสผู้ใช้งาน (User ID)" }, - uname: { type: "string", description: "ชื่อผู้ใช้งาน (User Name)" }, - }, - }, - }, - }, - }, - responses: { - 200: { - description: "ไฟล์ DOCX stream ที่สร้างสำเร็จ", - headers: { - "Content-Disposition": { - description: "ชื่อไฟล์สำหรับการดาวน์โหลด (attachment)", - schema: { type: "string", example: 'attachment; filename="T12_b2_IN_john_xxx.docx"' }, - }, - "Cache-Control": { - description: "การควบคุม Cache (no-store)", - schema: { type: "string", example: "no-store, max-age=0" }, - }, - }, - content: { - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": { - schema: { type: "string", format: "binary" }, - }, - }, - }, - 400: { - description: "พารามิเตอร์ไม่ครบถ้วน หรือรูปแบบไฟล์ JSON ไม่ถูกต้อง", - content: { - "text/plain": { - schema: { type: "string" }, - examples: { - missingParams: { value: "Missing fid,tid,uid or uname" }, - invalidData: { value: "Invalid data format" }, - }, - }, - }, - }, - 404: { - description: "ไม่พบไฟล์ข้อมูล JSON ใน TMPL_PATH", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Error" }, - example: { error: "Data not found: /path/to/TMPL_PATH/T12_b2_IN.json" }, - }, - }, - }, - 502: { - description: "เกิดข้อผิดพลาดในการเรียกไปยังบริการ Docedit /api/template/generate", - content: { - "text/plain": { - schema: { type: "string" }, - example: "Error from docedit generate: Expected docx stream but received text/html (Status 500)", - }, - }, - }, - 500: { - description: "เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์", - content: { - "text/plain": { - schema: { type: "string" }, - example: "Internal Server Error", - }, - }, - }, - }, - }, - }, "/api/note/external/documents": { post: { tags: ["external"], @@ -607,79 +225,31 @@ export const openApiSpec = { }, }, }, - "/api/documents/token": { - post: { - tags: ["documents"], - summary: "ขอ access_token ของกลุ่ม documents (docver)", - description: - "ออก access_token ใช้กับ endpoint อื่นของกลุ่ม documents เท่านั้น — token เป็น " + - "JWT HS256 ฝัง claims {userid, username, fid, template} ตามที่ส่งมา อายุตาม env " + - "DOCVER_TOKEN_TTL_SECONDS (default 3600 วินาที) · identity (userId/userName), " + - "templateId และ docId (= fid) ตอนทำงานแกะจาก claims ของ token นี้ — " + - "fid จึงบังคับ", - security: [{ ApiKeyAuth: [] }], - requestBody: { - required: true, - content: { - "application/json": { - schema: { $ref: "#/components/schemas/TokenRequest" }, - examples: { - "ครบทุก claim": { - value: { - userid: "123", - username: "Sorawit Bholsithi", - fid: "3", - template: "T12", - }, - }, - "ขั้นต่ำ (template เป็นทางเลือก)": { - value: { userid: "123", username: "Sorawit Bholsithi", fid: "3" }, - }, - }, - }, - }, - }, - responses: { - 200: { - description: "access_token พร้อมใช้ (แนบเป็น Authorization: Bearer ทุก request ถัดไป)", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/TokenResponse" }, - }, - }, - }, - 400: { $ref: "#/components/responses/BadRequest" }, - 401: { - description: "ไม่ได้ส่ง header x-api-key หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY)", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Error" }, - }, - }, - }, - 500: { $ref: "#/components/responses/ServerError" }, - }, - }, - }, - "/api/documents/versions": { + "/api/documents/{docId}/versions": { post: { tags: ["documents"], summary: "อัปโหลด version ใหม่ของเอกสาร (docx + pdf)", description: - "เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) — สร้างเอกสาร " + - "อัตโนมัติเมื่อ docId ยังไม่เคยมี แล้วเก็บไฟล์ .docx (+ .pdf ถ้าแนบมา) ลง MinIO " + - "พร้อมผู้อัปโหลด — versionNumber ที่ server จ่ายคือค่าจริงเสมอ (client ที่เลขไม่ตรง " + - "ต้องแก้ XML part ของตัวเอง) · pdf เป็น optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มี " + - "สำเนา PDF (ดู hasPdf ใน response) · ผู้อัปโหลด (userId/userName) และ templateId " + - "แกะจาก Bearer token ไม่รับใน form (ส่ง field เหล่านั้นมา = 400)", - security: [{ DocverBearer: [] }], + "สร้างเอกสารอัตโนมัติเมื่อ docId ยังไม่เคยมี (docId ห้ามซ้ำ) แล้วเก็บไฟล์ " + + ".docx (+ .pdf ถ้าแนบมา) ลง MinIO พร้อมผู้อัปโหลด — versionNumber ที่ server จ่าย " + + "คือค่าจริงเสมอ (client ที่เลขไม่ตรงต้องแก้ XML part ของตัวเอง) · pdf เป็น " + + "optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มีสำเนา PDF (ดู hasPdf ใน response)", + parameters: [ + { + name: "docId", + in: "path", + required: true, + schema: { type: "string" }, + description: "รหัสเอกสารจากผู้เรียก (add-in GUID / รหัส e-Court) — [A-Za-z0-9._:-]", + }, + ], requestBody: { required: true, content: { "multipart/form-data": { schema: { type: "object", - required: ["file"], + required: ["file", "userId", "userName"], properties: { file: { type: "string", @@ -692,6 +262,14 @@ export const openApiSpec = { description: "ไฟล์ .pdf ของ version นี้ (optional — ไม่แนบหรือไฟล์เปล่า = ไม่มี)", }, + userId: { + type: "string", + description: "รหัสผู้อัปโหลด (ระบบ stand-alone ไม่มี session)", + }, + userName: { + type: "string", + description: "ชื่อผู้อัปโหลด (แสดงใน version history)", + }, clientVersionId: { type: "string", description: "GUID ที่ client generate ก่อนเรียก (เก็บไว้สืบสวน ไม่ใช่ identity)", @@ -705,6 +283,10 @@ export const openApiSpec = { type: "string", description: "commit message ของ version (≤ 2048 ตัวอักษร)", }, + templateId: { + type: "string", + description: "รหัส template ที่ใช้สร้างเอกสาร (ถ้ามี)", + }, fileName: { type: "string", description: @@ -725,7 +307,6 @@ export const openApiSpec = { }, }, 400: { $ref: "#/components/responses/BadRequest" }, - 401: { $ref: "#/components/responses/DocverUnauthorized" }, 413: { description: "ไฟล์แนบใหญ่เกิน 100MB", content: { @@ -741,10 +322,17 @@ export const openApiSpec = { tags: ["documents"], summary: "รายการ version ทั้งหมดของเอกสาร (ใหม่ → เก่า)", description: - "เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) — version ทุกตัว " + - "ของเอกสารนั้น พร้อมผู้อัปโหลด (userId/userName), เวลา, commit message, ค่า fields " + - "และ hasPdf — ใช้เขียน UI version history ของ add-in", - security: [{ DocverBearer: [] }], + "version ทุกตัวของ docId นี้ พร้อมผู้อัปโหลด (userId/userName), เวลา, commit " + + "message, ค่า fields และ hasPdf — ใช้เขียน UI version history ของ add-in", + parameters: [ + { + name: "docId", + in: "path", + required: true, + schema: { type: "string" }, + description: "รหัสเอกสารจากผู้เรียก — [A-Za-z0-9._:-]", + }, + ], responses: { 200: { description: "รายการ version เรียงใหม่ → เก่า", @@ -758,7 +346,6 @@ export const openApiSpec = { }, }, 400: { $ref: "#/components/responses/BadRequest" }, - 401: { $ref: "#/components/responses/DocverUnauthorized" }, 404: { description: "ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยอัปโหลด version)", content: { @@ -771,17 +358,22 @@ export const openApiSpec = { }, }, }, - "/api/documents/versions/{versionId}/download-link": { + "/api/documents/{docId}/versions/{versionId}/download-link": { get: { tags: ["documents"], summary: "ขอ presigned URL ดาวน์โหลดไฟล์ของ version (docx/pdf)", description: - "เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) · คืน URL อายุสั้น " + - "(env DOCVER_LINK_TTL_SECONDS, default 900 วินาที) ให้เปิดในเบราว์เซอร์/Word ของ " + - "ผู้ใช้ — URL อ้าง endpoint MinIO ตรง ๆ จึงต้องเป็นโฮสต์ที่ผู้ใช้เข้าถึงได้ · ขอ pdf " + - "ใน version ที่ไม่มีสำเนา = 404", - security: [{ DocverBearer: [] }], + "คืน URL อายุสั้น (env DOCVER_LINK_TTL_SECONDS, default 900 วินาที) ให้เปิดใน " + + "เบราว์เซอร์/Word ของผู้ใช้ — URL อ้าง endpoint MinIO ตรง ๆ จึงต้องเป็นโฮสต์ที่ " + + "ผู้ใช้เข้าถึงได้ · ขอ pdf ใน version ที่ไม่มีสำเนา = 404", parameters: [ + { + name: "docId", + in: "path", + required: true, + schema: { type: "string" }, + description: "รหัสเอกสาร (ต้องตรงกับ version นี้ — โยน id ข้ามเอกสารไม่ได้)", + }, { name: "versionId", in: "path", @@ -807,7 +399,6 @@ export const openApiSpec = { }, }, 400: { $ref: "#/components/responses/BadRequest" }, - 401: { $ref: "#/components/responses/DocverUnauthorized" }, 404: { description: "ไม่พบ version นี้ของเอกสาร หรือขอ format=pdf ใน version ที่ไม่มีสำเนา PDF", @@ -829,17 +420,7 @@ export const openApiSpec = { in: "header", name: "x-api-key", description: - "x-api-key ของ N10 = env EXTERNAL_API_KEY · ของการขอ token กลุ่ม documents = env " + - "DOCVER_API_KEY (คนละค่ากับของ N10 — กดปุ่ม Authorize ใส่ครั้งเดียวแล้วใช้ได้ทุก " + - "request ของกลุ่มที่ key ตรง)", - }, - DocverBearer: { - type: "http", - scheme: "bearer", - bearerFormat: "JWT", - description: - "access_token จาก POST /api/documents/token — แนบเป็น Authorization: Bearer " + - "กับ endpoint อื่นของกลุ่ม documents ทุกตัว", + "ค่าเดียวกับ env EXTERNAL_API_KEY ของ server — กดปุ่ม Authorize ด้านบนเพื่อใส่ครั้งเดียวแล้วใช้ทุก request", }, }, responses: { @@ -853,15 +434,6 @@ export const openApiSpec = { }, }, }, - DocverUnauthorized: { - description: - "ไม่ได้แนบ Authorization: Bearer หรือ token ไม่ถูกต้อง/หมดอายุ (กลุ่ม documents — ข้อความเดียวกันทุกสาเหตุ)", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Error" }, - }, - }, - }, ServerError: { description: "เกิดข้อผิดพลาดของระบบ (เช่นไฟล์ .docx ไม่ถูกต้อง / DB ล้ม)", content: { @@ -875,82 +447,21 @@ export const openApiSpec = { Error: { type: "object", properties: { + success: { type: "boolean", example: false }, error: { type: "string" }, }, }, - TokenRequest: { - type: "object", - description: "claims ที่จะฝังใน access_token ของกลุ่ม documents — key อื่น = 400", - required: ["userid", "username", "fid"], - properties: { - userid: { type: "string", description: "รหัสผู้ใช้ (บังคับ, ≤ 100 ตัวอักษร)" }, - username: { type: "string", description: "ชื่อผู้ใช้ (บังคับ, ≤ 200 ตัวอักษร)" }, - fid: { - type: "string", - description: - "fileId ของ e-Court (บังคับ, ≤ 200 ตัวอักษร) — เป็น docId ของทุก operation ตรง ๆ", - }, - template: { - type: "string", - nullable: true, - description: - "รหัส template ที่ใช้สร้างเอกสาร — กลายเป็น templateId ตอนอัปโหลด version", - }, - }, - }, - TokenResponse: { - type: "object", - 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)", - }, - }, - }, ExtractResponse: { type: "object", properties: { + success: { type: "boolean", example: true }, data: { type: "object", description: "ชื่อแท็ก Content Control → ข้อความในนั้น (หลายบรรทัดคั่นด้วย \\n)", additionalProperties: { type: "string" }, example: { - "cc": { - "blackyear": "๙๐/๒๕๖๙", - "redyear": "xxx", - "curdate": "วันที่ เดือน สิงหาคม พุทธศักราช ๒๕๖๙ ", - "accuser": "นายธีรพล ร่มเริง", - "detail1": "ตุลาการเจ้าของสำนวนได้พิจารณาคำฟ้อง...", - }, - "bookmark": { - "signature": "" - }, - "properties": { - "standard": { - "creator": "mam", - "created": "2026-10-22T02:58:00.000Z", - "modified": "2026-10-22T07:08:00.000Z" - }, - "extended": { - "template": "TS_P2_IN.dotx", - "totalTime": 172, - "pages": 2, - "words": 124, - }, - "custom": { - "ticket_data": "{\"exp\":1793013726,\"fid\":\"T_no_warning\",\"tid\":\"T_X\",\"uid\":\"john\",\"uname\":\"John Doe\"}", - "HmacSha256": "1dc53fbd48410845c895ddb841a8fe4ad93da413126fae9c688202e57f2baaa9", - "Ed25519": "95ea0ca6d81df885c22a2334ed89b2fc3c75a1a309fb5843e0deb0a16c9d05a45b345fb28fd606f3b152285e70560ad43a6389534794aa0d6f06d13f1307b005" - } - }, - "verify": { - "exp": "26/10/2569 18:22:06", - "HmacSha256": true, - "Ed25519": true - } + blackno: "คดีหมายเลขดำที่", + court: "ศาลปกครองสุพรรณบุรี", }, }, }, diff --git a/middleware.ts b/middleware.ts index 712d46e..f05c44b 100644 --- a/middleware.ts +++ b/middleware.ts @@ -37,6 +37,6 @@ export function middleware(req: NextRequest) { // ยังถูกคุมอยู่ดีเพราะ root page ส่งต่อให้ /note ซึ่ง middleware จับตามปกติ export const config = { matcher: [ - "/((?!note/login|api/note|api/template|tmpl/|api/documents|api/swagger|tmpl|wordaddin|note/uploads|note/samples|note/swagger-ui|note/pdf\\.worker\\.min\\.mjs|_next|fonts|favicon\\.ico).+)", + "/((?!note/login|api/note|api/template|api/documents|api/swagger|wordaddin|note/uploads|note/samples|note/swagger-ui|note/pdf\\.worker\\.min\\.mjs|_next|fonts|favicon\\.ico).+)", ], }; diff --git a/package-lock.json b/package-lock.json index 466a9f7..748a8c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,8 @@ "dependencies": { "@pdf-lib/fontkit": "^1.1.1", "@xmldom/xmldom": "^0.9.12", - "docx-templates": "^4.15.0", "drizzle-orm": "^0.45.2", "lucide-react": "^1.42.0", - "minio": "^8.0.7", "next": "^14.2.35", "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.10.38", @@ -28,7 +26,7 @@ "xpath": "^0.0.34" }, "devDependencies": { - "@types/node": "^26.6.5", + "@types/node": "^26.5.0", "@types/office-js": "^1.0.612", "@types/pngjs": "^6.0.5", "@types/react": "^18.3.31", @@ -63,6 +61,7 @@ "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", @@ -81,6 +80,7 @@ "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -90,7 +90,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", @@ -98,6 +99,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", @@ -168,6 +170,7 @@ "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -181,6 +184,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", @@ -200,6 +204,7 @@ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.2.0" }, @@ -213,6 +218,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", @@ -232,6 +238,7 @@ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.2.0" }, @@ -245,6 +252,7 @@ "integrity": "sha512-J2jmTPv8ZraSHDTz9l2Bx8gNL3ktfDDWo2mxWfzarn64O9Fjhb+l85YWyubGy2xUdeGuZPKzvQLltGv8bSu8eQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", @@ -264,6 +272,7 @@ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.2.0" }, @@ -277,6 +286,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", @@ -296,6 +306,7 @@ "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", @@ -331,6 +342,7 @@ "integrity": "sha512-BoSmXPx2er1Ai+wKlDvj29jIQespCNBwEmKyZVHO2kEFsWbGjAjwMCGzug3DJM5/QYIV3vej0S1zcU5bq9fa8w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2" }, @@ -348,6 +360,7 @@ "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", @@ -364,6 +377,7 @@ "integrity": "sha512-7GEAoIsaoBr6KELNRb8nypowCqvk8dnCHFCYg4XD4lOQGY2GqjQg5IhkRjyBFRO18CGSMq05PaNqSOE9GQro3g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -377,6 +391,7 @@ "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=22.0.0" } @@ -407,6 +422,7 @@ "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -420,6 +436,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", @@ -435,6 +452,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" @@ -449,6 +467,7 @@ "integrity": "sha512-8utCLdTZn14CiAe0HW4JbtzUMsc0mdDwnjUvStFMrR4IxQP8S2eL9zVAdplpfORVJGYmidZElpeXRYjJQ4UyUw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", @@ -473,6 +492,7 @@ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -486,6 +506,7 @@ "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", @@ -507,6 +528,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" @@ -521,6 +543,7 @@ "integrity": "sha512-NDW2/bz2Pz71EJAlhSw+jg1kpRIWtrzj0gCzcHuG1u7iLvSOHpUc5lICkDA9x0bbeityxTneiunSBluvS+/y4w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/msal-common": "16.14.2" }, @@ -534,6 +557,7 @@ "integrity": "sha512-HOBR0Gfo39TMrkxNG8k6cfjRkrRjXZc3JQs3ebMWQc9S4cihuknMYra3sgmzbZB752oQBqBN9+WF7c826p2vIQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.0" } @@ -544,6 +568,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" @@ -559,6 +584,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@azure/msal-common": "15.17.0", "@azure/msal-node-runtime": "^0.20.0", @@ -574,6 +600,7 @@ "integrity": "sha512-VQ5/gTLFADkwue+FohVuCqlzFPUq4xSrX8jeZe+iwZuY6moliNC8xt86qPVNYdtbQfELDf2Nu6LI+demFPHGgw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.0" } @@ -583,7 +610,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", @@ -591,6 +619,7 @@ "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.0" } @@ -601,6 +630,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", @@ -628,6 +658,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", @@ -649,6 +680,7 @@ "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -673,6 +705,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.1.90" } @@ -765,7 +798,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -814,7 +846,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1727,7 +1758,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", @@ -1735,6 +1767,7 @@ "integrity": "sha512-WtcEoG/imdHRvC3vofGi/OcgH+cjHHhO0AfEeTlsnrKLjVKKBXV6aoIrB2nHZPpE7iW5sA7AZMR6bPD8ytxN+w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10" } @@ -1745,6 +1778,7 @@ "integrity": "sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20" }, @@ -1927,6 +1961,7 @@ "integrity": "sha512-w3PMZH5rahrukn8/I7P9Ihil+twgLTUHDZtJlJyBbUKyPaOSSQjLZkb0PpncVhin1gCaMgOFXy6iNPgcZUoo2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -1953,6 +1988,7 @@ "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1962,7 +1998,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", @@ -2665,6 +2702,7 @@ "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mute-stream": "^1.0.0" }, @@ -2678,6 +2716,7 @@ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "minipass": "^7.0.4" }, @@ -2712,6 +2751,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" @@ -2740,7 +2780,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", @@ -2764,6 +2805,7 @@ "integrity": "sha512-OayhehwI+CnO0Wr53e29ZJZWGsNA5yVG7r54qmZSLc5HxA5Cozk4hP7EbYDCXkxh4NbQoT1dhTzC8bkRo+wWXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer": "^5.2.1", "debug": "^4.1.1", @@ -2776,6 +2818,7 @@ "integrity": "sha512-wGuFEzvRiWZmDxQMGKEjOKhEIVnLiG6vRUuM9Hwqxpe/kbiyA2WiUyEVpniNPaaw8gDHTf9zJHnPNNj0JiL5mA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@microsoft/dev-tunnels-contracts": ">1.1.8", "axios": "^1.6.2", @@ -2790,6 +2833,7 @@ "integrity": "sha512-YlAI2jC0grK2p2fvxDV9Hin5EtRXL6yOMhLKqtjRVlH8Q0+hI9UWFl7RhLWSZjsbDyl+0LIlI2HjlcbsUhXGsQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "adm-zip": "^0.5.17", "original-fs": "^1.2.0", @@ -2803,6 +2847,7 @@ "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12.0" } @@ -2817,6 +2862,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "peer": true, "bin": { "uuid": "dist-node/bin/uuid" } @@ -2827,6 +2873,7 @@ "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=14.0.0" } @@ -2837,6 +2884,7 @@ "integrity": "sha512-mih5trnmvIKpjLgqzsoxqiWFvG5eMeETPCAdtFvDaXyferWzFt9UrdARfRoYboPhfFjYRYw8EoF3EA4yFVWCsQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@apidevtools/swagger-parser": "^10.1.1", "@microsoft/app-manifest": "1.1.3", @@ -2855,6 +2903,7 @@ "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2919,6 +2968,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", @@ -2934,6 +2984,7 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -2944,6 +2995,7 @@ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -2963,6 +3015,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", @@ -3033,6 +3086,7 @@ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -3046,6 +3100,7 @@ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -3056,6 +3111,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", @@ -3097,6 +3153,7 @@ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -3111,6 +3168,7 @@ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", @@ -3136,6 +3194,7 @@ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3150,6 +3209,7 @@ "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3164,6 +3224,7 @@ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.6.0" } @@ -3174,6 +3235,7 @@ "integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.3.0", @@ -3218,6 +3280,7 @@ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3232,6 +3295,7 @@ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -3254,6 +3318,7 @@ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -3264,6 +3329,7 @@ "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" }, @@ -3278,6 +3344,7 @@ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3291,6 +3358,7 @@ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -3301,6 +3369,7 @@ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -3318,6 +3387,7 @@ "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "content-type": "^2.1.0" }, @@ -3335,6 +3405,7 @@ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3349,6 +3420,7 @@ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", @@ -3376,6 +3448,7 @@ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -3396,6 +3469,7 @@ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", @@ -3415,6 +3489,7 @@ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -3827,13 +3902,15 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.1.0.tgz", "integrity": "sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==", + "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/nodable" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -3879,6 +3956,7 @@ "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/oxc-project" } @@ -3923,6 +4001,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -3940,6 +4019,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -3957,6 +4037,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -3974,6 +4055,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -3991,6 +4073,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4008,6 +4091,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4025,6 +4109,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4042,6 +4127,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4059,6 +4145,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4076,6 +4163,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4093,6 +4181,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4110,6 +4199,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4127,6 +4217,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4144,6 +4235,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4161,6 +4253,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4170,7 +4263,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", @@ -4236,7 +4330,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", @@ -4254,14 +4349,15 @@ "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "26.6.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", - "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "version": "26.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.0.tgz", + "integrity": "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==", "dev": true, "license": "MIT", "dependencies": { @@ -4298,7 +4394,6 @@ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4330,7 +4425,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", @@ -4338,6 +4434,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", @@ -4400,6 +4497,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" @@ -4414,6 +4512,7 @@ "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4427,6 +4526,7 @@ "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -4447,6 +4547,7 @@ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 14" } @@ -4507,6 +4608,7 @@ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "type-fest": "^0.21.3" }, @@ -4568,13 +4670,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/apache-arrow": { "version": "21.2.0", @@ -4582,6 +4686,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", @@ -4598,6 +4703,7 @@ "integrity": "sha512-b4e2xxj/yMeT2hNlD6x7zNFiQK2Dlmkb9CQ6v3/1G2RezKN4YS2lcEv8lzGm1mhs4f2lM4yNAMI/zUDBZBBt7w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -4607,7 +4713,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", @@ -4615,6 +4722,7 @@ "integrity": "sha512-ZLDA7mShh4mP2Z/HlFolmvhBPX1LfnbIWXrselyYVA7EKjHhri1fZzpu2EiWAmfbRxNBY6fRjoPJWbx5giKy4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cls-hooked": "^4.2.2", "continuation-local-storage": "^3.2.1", @@ -4634,21 +4742,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", @@ -4660,18 +4771,13 @@ "node": ">=12" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, "node_modules/async-hook-jl": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "stack-chain": "^1.3.7" }, @@ -4685,6 +4791,7 @@ "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "semver": "^5.3.0", "shimmer": "^1.1.0" @@ -4699,6 +4806,7 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver" } @@ -4709,6 +4817,7 @@ "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.1" } @@ -4718,7 +4827,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", @@ -4726,6 +4836,7 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -4773,6 +4884,7 @@ "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", @@ -4786,6 +4898,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" @@ -4797,6 +4910,7 @@ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "4" }, @@ -4810,6 +4924,7 @@ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -4824,6 +4939,7 @@ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "18 || 20 || >=22" } @@ -4847,7 +4963,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/baseline-browser-mapping": { "version": "2.11.21", @@ -4891,27 +5008,20 @@ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, - "node_modules/block-stream2": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", - "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", - "license": "MIT", - "dependencies": { - "readable-stream": "^3.4.0" - } - }, "node_modules/body-parser": { "version": "1.20.8", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", @@ -4937,6 +5047,7 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -4947,6 +5058,7 @@ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -4959,7 +5071,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", @@ -4967,6 +5080,7 @@ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", @@ -4983,6 +5097,7 @@ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -4997,6 +5112,7 @@ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^4.0.2" }, @@ -5017,12 +5133,6 @@ "node": ">=8" } }, - "node_modules/browser-or-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", - "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", - "license": "MIT" - }, "node_modules/browserslist": { "version": "4.28.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", @@ -5043,7 +5153,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", @@ -5078,26 +5187,19 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "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", @@ -5112,6 +5214,7 @@ "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "run-applescript": "^7.0.0" }, @@ -5139,6 +5242,7 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -5149,6 +5253,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" @@ -5163,6 +5268,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" @@ -5179,7 +5285,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", @@ -5227,6 +5334,7 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5251,6 +5359,7 @@ "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": "*" } @@ -5261,6 +5370,7 @@ "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "get-func-name": "^2.0.2" }, @@ -5312,6 +5422,7 @@ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "engines": { "node": ">=18" } @@ -5322,6 +5433,7 @@ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" }, @@ -5335,6 +5447,7 @@ "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "string-width": "^4.2.0" }, @@ -5367,6 +5480,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", @@ -5382,6 +5496,7 @@ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -5400,6 +5515,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", @@ -5415,6 +5531,7 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver" } @@ -5445,6 +5562,7 @@ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5468,6 +5586,7 @@ "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-timsort": "^1.0.3", "esprima": "^4.0.1" @@ -5482,6 +5601,7 @@ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -5495,6 +5615,7 @@ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -5505,6 +5626,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" @@ -5516,6 +5638,7 @@ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -5525,13 +5648,8 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cors": { "version": "2.8.6", @@ -5539,6 +5657,7 @@ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "object-assign": "^4", "vary": "^1" @@ -5557,6 +5676,7 @@ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5572,6 +5692,7 @@ "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": "*" } @@ -5581,7 +5702,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", @@ -5589,6 +5711,7 @@ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "boolbase": "^2.0.0", "css-what": "^8.0.0", @@ -5624,6 +5747,7 @@ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -5650,7 +5774,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", @@ -5689,6 +5814,7 @@ "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4.0" } @@ -5699,6 +5825,7 @@ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -5718,21 +5845,13 @@ "dev": true, "license": "MIT" }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -5749,7 +5868,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", @@ -5757,6 +5877,7 @@ "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "type-detect": "^4.0.0" }, @@ -5770,6 +5891,7 @@ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4.0.0" } @@ -5780,6 +5902,7 @@ "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" @@ -5797,6 +5920,7 @@ "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -5810,6 +5934,7 @@ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -5820,6 +5945,7 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.4.0" } @@ -5830,6 +5956,7 @@ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -5840,6 +5967,7 @@ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -5851,6 +5979,7 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8" } @@ -5861,6 +5990,7 @@ "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "address": "^1.0.1", "debug": "4" @@ -5879,6 +6009,7 @@ "integrity": "sha512-6eb9YRrimz8oTr5+JDzGmSYnXy5V7YnK5y/hd8AUDK1MssHjQKm9LlD6NSrHx4vMDF3+e/spI2hmWTviElgWZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "semver": "^5.3.0" } @@ -5889,6 +6020,7 @@ "integrity": "sha512-l126t01d2ZS9EreskvEtZPrcgstuvH3rbKy82oUhUrVmBaGx4hO9wECdl3cvZbKDYjMF3QJDB5z5dL9yWAjvZQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "diagnostic-channel": "*" } @@ -5899,6 +6031,7 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver" } @@ -5917,31 +6050,13 @@ "dev": true, "license": "MIT" }, - "node_modules/docx-templates": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/docx-templates/-/docx-templates-4.15.0.tgz", - "integrity": "sha512-H0JEDFVCF5j64Do9fqHVWwF86MOWUnHhQ2dfGWy7C2o9yPwtbCT1pXpotE6165Px07fia1Ted1Xh1fj1UZ9u1g==", - "license": "MIT", - "dependencies": { - "jszip": "^3.10.1", - "sax": "1.3.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/docx-templates/node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", - "license": "ISC" - }, "node_modules/dom-serializer": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", @@ -5967,6 +6082,7 @@ } ], "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -5977,6 +6093,7 @@ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "domelementtype": "^3.0.0" }, @@ -5994,6 +6111,7 @@ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", @@ -6013,6 +6131,7 @@ "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=10" } @@ -6648,6 +6767,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", @@ -6663,6 +6783,7 @@ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" } @@ -6672,7 +6793,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", @@ -6687,6 +6809,7 @@ "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "shimmer": "^1.2.0" } @@ -6704,6 +6827,7 @@ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -6714,6 +6838,7 @@ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "once": "^1.4.0" } @@ -6737,6 +6862,7 @@ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" } @@ -6764,6 +6890,7 @@ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -6777,6 +6904,7 @@ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -6792,7 +6920,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", @@ -6851,7 +6980,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", @@ -6869,6 +6999,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" @@ -6893,22 +7024,18 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -6919,6 +7046,7 @@ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "eventsource-parser": "^3.0.1" }, @@ -6932,6 +7060,7 @@ "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" } @@ -6942,6 +7071,7 @@ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, "license": "(MIT OR WTFPL)", + "peer": true, "engines": { "node": ">=6" } @@ -7010,6 +7140,7 @@ "integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" @@ -7030,6 +7161,7 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -7039,7 +7171,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", @@ -7083,7 +7216,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.8", @@ -7106,6 +7240,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", + "dev": true, "funding": [ { "type": "github", @@ -7113,6 +7248,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" @@ -7122,6 +7258,7 @@ "version": "5.11.2", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.2.tgz", "integrity": "sha512-R9iDuNrQYeQut46cn2r2wHKn4HYzVDvDm5J1wW+koZewykv0yuO2HChTYeZtrULyHIDM9cj9TUXloCsueCQUog==", + "dev": true, "funding": [ { "type": "github", @@ -7129,6 +7266,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@nodable/entities": "^3.0.1", "fast-xml-builder": "^1.2.0", @@ -7181,6 +7319,7 @@ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -7204,21 +7343,13 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -7238,6 +7369,7 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -7247,21 +7379,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.1", @@ -7275,6 +7410,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=4.0" }, @@ -7290,6 +7426,7 @@ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7320,6 +7457,7 @@ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -7344,6 +7482,7 @@ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -7353,7 +7492,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", @@ -7361,6 +7501,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", @@ -7402,6 +7543,7 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -7412,6 +7554,7 @@ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -7422,6 +7565,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", @@ -7447,6 +7591,7 @@ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -7473,7 +7618,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", @@ -7481,6 +7627,7 @@ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", @@ -7512,6 +7659,7 @@ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -7531,6 +7679,7 @@ "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.8", "neo-async": "^2.6.2", @@ -7553,6 +7702,7 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7563,6 +7713,7 @@ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -7576,6 +7727,7 @@ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -7628,7 +7780,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", @@ -7643,6 +7796,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -7656,6 +7810,7 @@ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -7671,6 +7826,7 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=0.12" }, @@ -7689,7 +7845,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", @@ -7697,6 +7854,7 @@ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -7713,6 +7871,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", @@ -7728,6 +7887,7 @@ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=0.12" }, @@ -7741,6 +7901,7 @@ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", @@ -7762,6 +7923,7 @@ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -7775,7 +7937,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", @@ -7783,6 +7946,7 @@ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -7827,7 +7991,8 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/ignore": { "version": "5.3.2", @@ -7835,28 +8000,26 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "dev": true, + "license": "ISC", + "peer": true }, "node_modules/ini": { "version": "1.3.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", @@ -7957,6 +8120,7 @@ "integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 12" } @@ -7967,6 +8131,7 @@ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.10" } @@ -7989,7 +8154,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", @@ -8013,6 +8179,7 @@ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "is-docker": "cli.js" }, @@ -8062,6 +8229,7 @@ "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20" }, @@ -8075,6 +8243,7 @@ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-docker": "^3.0.0" }, @@ -8094,6 +8263,7 @@ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "is-docker": "cli.js" }, @@ -8126,7 +8296,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", @@ -8134,6 +8305,7 @@ "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -8145,13 +8317,15 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", + "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/is-wsl": { "version": "2.2.0", @@ -8159,6 +8333,7 @@ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-docker": "^2.0.0" }, @@ -8166,18 +8341,13 @@ "node": ">=8" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "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", @@ -8185,7 +8355,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -8196,6 +8365,7 @@ "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -8205,7 +8375,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", @@ -8229,6 +8400,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -8242,7 +8414,6 @@ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", @@ -8297,14 +8468,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", @@ -8325,6 +8498,7 @@ "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -8335,6 +8509,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", @@ -8352,48 +8527,6 @@ "npm": ">=6" } }, - "node_modules/jszip": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", - "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/junk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", @@ -8410,6 +8543,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", @@ -8422,6 +8556,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" @@ -8434,6 +8569,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" @@ -8445,25 +8581,18 @@ "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.9" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -8501,6 +8630,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8522,6 +8652,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8543,6 +8674,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8564,6 +8696,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8585,6 +8718,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8606,6 +8740,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8627,6 +8762,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8648,6 +8784,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8669,6 +8806,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8690,6 +8828,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8711,6 +8850,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8745,6 +8885,7 @@ "integrity": "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "css-select": "^7.0.0", "cssom": "^0.5.0", @@ -8768,56 +8909,65 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/lodash.includes": { "version": "4.3.0", "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", @@ -8825,6 +8975,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", @@ -8854,6 +9005,7 @@ "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "get-func-name": "^2.0.1" } @@ -8893,6 +9045,7 @@ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" } @@ -8903,6 +9056,7 @@ "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -8922,6 +9076,7 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -8932,6 +9087,7 @@ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -8952,6 +9108,7 @@ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -8976,6 +9133,7 @@ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "mime": "cli.js" }, @@ -8987,7 +9145,9 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -8996,7 +9156,9 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -9010,6 +9172,7 @@ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -9023,6 +9186,7 @@ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "brace-expansion": "^5.0.8" }, @@ -9039,49 +9203,18 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minio": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.7.tgz", - "integrity": "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==", - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.4", - "block-stream2": "^2.1.0", - "browser-or-node": "^2.1.1", - "buffer-crc32": "^1.0.0", - "eventemitter3": "^5.0.1", - "fast-xml-parser": "^5.3.4", - "ipaddr.js": "^2.0.1", - "lodash": "^4.17.21", - "mime-types": "^2.1.35", - "query-string": "^7.1.3", - "stream-json": "^1.8.0", - "through2": "^4.0.2", - "xml2js": "^0.5.0 || ^0.6.2" - }, - "engines": { - "node": "^16 || ^18 || >=20" - } - }, - "node_modules/minio/node_modules/ipaddr.js": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", - "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -9092,6 +9225,7 @@ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minipass": "^7.1.2" }, @@ -9131,14 +9265,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", @@ -9146,6 +9282,7 @@ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "mustache": "bin/mustache" } @@ -9156,6 +9293,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" } @@ -9195,7 +9333,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", @@ -9203,6 +9342,7 @@ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -9212,14 +9352,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", @@ -9305,6 +9447,7 @@ "integrity": "sha512-lLdwkeg2XUVPTHqWjPltTxH81zbQNbUWC+AEb9ni50KjxkUzAAx1HGtMT21sbntzfUGfIphhcklvs3imJeqDXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "semver": "^7.3.5" }, @@ -9317,7 +9460,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", @@ -9365,6 +9509,7 @@ "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "http2-client": "^1.2.5" }, @@ -9387,7 +9532,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", @@ -9395,6 +9541,7 @@ "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es6-promise": "^3.2.1" } @@ -9432,6 +9579,7 @@ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "boolbase": "^2.0.0" }, @@ -9449,6 +9597,7 @@ "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "fast-safe-stringify": "^2.0.7" } @@ -9459,6 +9608,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", @@ -9474,6 +9624,7 @@ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 6" } @@ -9484,6 +9635,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", @@ -9504,6 +9656,7 @@ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 6" } @@ -9514,6 +9667,7 @@ "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "funding": { "url": "https://github.com/Mermade/oas-kit?sponsor=1" } @@ -9524,6 +9678,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", @@ -9544,6 +9699,7 @@ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 6" } @@ -9574,6 +9730,7 @@ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -10223,6 +10380,7 @@ "integrity": "sha512-kxV247eRDdGhBKOZYBhAvgzm/PveXdUXvZR5bi0mDYWFeY6wHoDuCSjRw00khRR///1ZvgixfvS4CqSfezyptQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@microsoft/app-manifest": "^1.0.2", "adm-zip": "0.5.16", @@ -10244,6 +10402,7 @@ "integrity": "sha512-2eOdCCYJ5bhCe2p9KKETdg1UNshsKaT0lDU/jNopAg3t7zC1WxwvofTSO/+4Log5L4Re+wUdV8MqrQikZBa7+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@xmldom/xmldom": "^0.8.5", "commander": "^9.0.0", @@ -10259,6 +10418,7 @@ "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" } @@ -10269,6 +10429,7 @@ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -10279,6 +10440,7 @@ "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12.0" } @@ -10289,6 +10451,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -10302,6 +10465,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", @@ -10317,6 +10481,7 @@ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-name": "1.1.3" } @@ -10326,7 +10491,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", @@ -10334,6 +10500,7 @@ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -10344,6 +10511,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -10359,6 +10527,7 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -10369,6 +10538,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -10379,6 +10549,7 @@ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -10400,6 +10571,7 @@ "integrity": "sha512-MUbcqOodtlOUNhLKjSdqeLBHFVd7Qh5pUODqmQe2VLBKk7PuRgl4dCk6bp2dU79vl6BcDyrRERpkyVnQg6zdFg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "commander": "^13.0.0" }, @@ -10413,6 +10585,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -10425,7 +10598,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", @@ -10433,6 +10607,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -10447,6 +10622,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "peer": true, "bin": { "uuid": "dist-node/bin/uuid" } @@ -10456,7 +10632,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", @@ -10464,6 +10641,7 @@ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -10547,6 +10725,7 @@ "integrity": "sha512-WjthzxXY5gl78+jh91LczM9zg26zU8tc3afLi2bUcrMaPc6LaRaKJhw6NsyRB73kVDx76DGTe6Ws6sxufghOdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "adm-zip": "0.5.12", "commander": "^13.0.0", @@ -10566,6 +10745,7 @@ "integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -10576,6 +10756,7 @@ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -10586,6 +10767,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -10601,6 +10783,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -10611,6 +10794,7 @@ "integrity": "sha512-MUbcqOodtlOUNhLKjSdqeLBHFVd7Qh5pUODqmQe2VLBKk7PuRgl4dCk6bp2dU79vl6BcDyrRERpkyVnQg6zdFg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "commander": "^13.0.0" }, @@ -10624,6 +10808,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -10657,6 +10842,7 @@ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -10670,6 +10856,7 @@ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "wrappy": "1" } @@ -10680,6 +10867,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", @@ -10705,7 +10893,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", @@ -10732,6 +10921,7 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -10740,6 +10930,7 @@ "version": "1.6.2", "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "dev": true, "funding": [ { "type": "github", @@ -10747,6 +10938,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10757,6 +10949,7 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -10774,6 +10967,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" @@ -10790,7 +10984,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", @@ -10798,6 +10993,7 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -10892,6 +11088,7 @@ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.20.0" } @@ -10925,7 +11122,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", @@ -11074,7 +11270,6 @@ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", "license": "Unlicense", - "peer": true, "engines": { "node": ">=12" }, @@ -11089,6 +11284,7 @@ "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20" }, @@ -11103,6 +11299,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", @@ -11124,18 +11321,13 @@ "node": ">=10" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", @@ -11147,7 +11339,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", @@ -11155,6 +11348,7 @@ "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -11173,6 +11367,7 @@ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -11183,6 +11378,7 @@ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -11204,6 +11400,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" @@ -11215,24 +11412,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11260,6 +11439,7 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -11270,6 +11450,7 @@ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", @@ -11286,6 +11467,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", @@ -11301,7 +11483,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" }, @@ -11314,7 +11495,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" @@ -11348,7 +11528,9 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -11376,7 +11558,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", @@ -11384,6 +11567,7 @@ "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "funding": { "url": "https://github.com/Mermade/oas-kit?sponsor=1" } @@ -11394,6 +11578,7 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11446,6 +11631,7 @@ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -11466,7 +11652,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.8", @@ -11474,6 +11661,7 @@ "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" @@ -11508,6 +11696,7 @@ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -11525,6 +11714,7 @@ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" @@ -11536,6 +11726,7 @@ "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -11549,6 +11740,7 @@ "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.12.0" } @@ -11591,6 +11783,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -11605,7 +11798,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -11618,6 +11812,7 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -11651,6 +11846,7 @@ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -11664,6 +11860,7 @@ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -11689,6 +11886,7 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -11698,7 +11896,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", @@ -11706,6 +11905,7 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "mime": "cli.js" }, @@ -11719,6 +11919,7 @@ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -11729,18 +11930,13 @@ "node": ">= 0.8.0" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, "node_modules/setprototypeof": { "version": "1.2.0", "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", @@ -11748,6 +11944,7 @@ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -11761,6 +11958,7 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -11771,6 +11969,7 @@ "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -11783,7 +11982,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", @@ -11791,6 +11991,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", @@ -11805,6 +12006,7 @@ "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "should-type": "^1.4.0" } @@ -11815,6 +12017,7 @@ "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "should-type": "^1.3.0", "should-type-adaptors": "^1.0.1" @@ -11825,7 +12028,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", @@ -11833,6 +12037,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" @@ -11843,7 +12048,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", @@ -11851,6 +12057,7 @@ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", @@ -11871,6 +12078,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" @@ -11888,6 +12096,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", @@ -11907,6 +12116,7 @@ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -11960,7 +12170,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/simple-get": { "version": "4.0.1", @@ -11982,6 +12193,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -12018,21 +12230,13 @@ "source-map": "^0.6.0" } }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/stack-chain": { "version": "1.3.7", "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/stackback": { "version": "0.0.2", @@ -12047,6 +12251,7 @@ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -12058,27 +12263,13 @@ "dev": true, "license": "MIT" }, - "node_modules/stream-chain": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", - "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", - "license": "BSD-3-Clause" - }, - "node_modules/stream-json": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", - "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", - "license": "BSD-3-Clause", - "dependencies": { - "stream-chain": "^2.2.5" - } - }, "node_modules/streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", @@ -12094,6 +12285,7 @@ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", @@ -12109,6 +12301,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -12119,6 +12312,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -12131,20 +12325,13 @@ "node": ">=10.0.0" } }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -12196,6 +12383,7 @@ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12204,6 +12392,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.5.1.tgz", "integrity": "sha512-UM4nTLWzQiDBseVxUGxN0wJzIk0UU89Qi7pKXz9qISo+lhviJikTuikBsK/oFRlpvYHvSdz0TVoLyQ+sEpYwxA==", + "dev": true, "funding": [ { "type": "github", @@ -12211,6 +12400,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "anynum": "^1.0.1" } @@ -12267,6 +12457,7 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -12302,6 +12493,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", @@ -12330,6 +12522,7 @@ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -12350,14 +12543,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", @@ -12365,6 +12560,7 @@ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -12376,6 +12572,7 @@ "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 6" } @@ -12431,6 +12628,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", @@ -12448,6 +12646,7 @@ "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -12460,7 +12659,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", @@ -12468,6 +12668,7 @@ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -12485,6 +12686,7 @@ "integrity": "sha512-UDdUF9kV2hYdlv+7AgqP2kXarVSUhjK7tg1BUflIRGEgND0/QoNpN64rcEuhEcM8AIbW65yrCopJWqRhLZ3m8w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "axios": "^1.6.1" } @@ -12495,6 +12697,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", @@ -12513,7 +12716,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", @@ -12538,15 +12742,6 @@ "node": ">=0.8" } }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "license": "MIT", - "dependencies": { - "readable-stream": "3" - } - }, "node_modules/tinybench": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", @@ -12608,7 +12803,6 @@ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12642,6 +12836,7 @@ "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=14.14" } @@ -12665,6 +12860,7 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.6" } @@ -12701,6 +12897,7 @@ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "tree-kill": "cli.js" } @@ -12743,6 +12940,7 @@ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -12756,6 +12954,7 @@ "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -12766,6 +12965,7 @@ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -12779,6 +12979,7 @@ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -12792,7 +12993,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", @@ -12815,6 +13017,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -12827,14 +13030,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.0", @@ -12869,6 +13074,7 @@ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -12917,6 +13123,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -12925,6 +13132,7 @@ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -12939,6 +13147,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "peer": true, "bin": { "uuid": "dist/esm/bin/uuid" } @@ -12949,6 +13158,7 @@ "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.10" } @@ -12959,6 +13169,7 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -12969,6 +13180,7 @@ "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", @@ -13047,6 +13259,7 @@ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13156,6 +13369,7 @@ "integrity": "sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8.0.0 || >=10.0.0" } @@ -13224,6 +13438,7 @@ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -13263,7 +13478,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", @@ -13285,7 +13501,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", @@ -13315,6 +13532,7 @@ "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.2.0" @@ -13332,6 +13550,7 @@ "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-inside-container": "^1.0.0" }, @@ -13356,6 +13575,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "dev": true, "funding": [ { "type": "github", @@ -13363,6 +13583,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=16.0.0" } @@ -13371,6 +13592,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, "license": "MIT", "dependencies": { "sax": ">=0.6.0", @@ -13384,6 +13606,7 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=4.0" @@ -13411,6 +13634,7 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=10" } @@ -13421,6 +13645,7 @@ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "engines": { "node": ">=18" } @@ -13431,6 +13656,7 @@ "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -13447,6 +13673,7 @@ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -13466,6 +13693,7 @@ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -13500,6 +13728,7 @@ "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "dev": true, "license": "ISC", + "peer": true, "peerDependencies": { "zod": "^3.25.28 || ^4" } diff --git a/package.json b/package.json index fabdcde..701ecae 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "dependencies": { "@pdf-lib/fontkit": "^1.1.1", "@xmldom/xmldom": "^0.9.12", - "docx-templates": "^4.15.0", "drizzle-orm": "^0.45.2", "lucide-react": "^1.42.0", "minio": "^8.0.7", @@ -32,7 +31,7 @@ "xpath": "^0.0.34" }, "devDependencies": { - "@types/node": "^26.6.5", + "@types/node": "^26.5.0", "@types/office-js": "^1.0.612", "@types/pngjs": "^6.0.5", "@types/react": "^18.3.31", diff --git a/public/tmpl/T12_FD155_3s-long.json b/public/tmpl/T12_FD155_3s-long.json deleted file mode 100644 index 294791e..0000000 --- a/public/tmpl/T12_FD155_3s-long.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "info": { - "ticket_data": "{\"fid\":\"T12_FD155_3s-long\",\"tid\":\"T12\",\"uid\":\"john\",\"uname\":\"John Doe\"}" - }, - "dc": { - "Interpleader": false, - "docabbr": "(ต. ๒๒)", - "blackno": "คดีหมายเลขดำที่", - "redno": "คดีหมายเลขแดงที่", - "court": "ศาลปกครองนครราชสีมา", - "panels": [ - { - "pname": "นายวิรุฬห์ จินตนะกุล", - "pmname": "ตุลาการศาลปกครองกลาง", - "pm": "ตุลาการเจ้าของสำนวน" - }, - { - "pname": "นายสัมฤทธิ์ อ่อนคำ", - "pmname": "ตุลาการศาลปกครองสูงสุด" - }, - { - "pname": "นางสุมาลี ลิมปโอวาท", - "pmname": "ตุลาการศาลปกครองสูงสุด" - } - ], - "pnameT":"ชื่อ pnameT", - "pmnameT":"ตำแหน่งของ pnameT", - "pmT":"ตุลาการผู้แถลงคดี", - "acc_name":"ชื่อ acc_name" - - }, - "cc": { - "blackyear": "๙๐/๒๕๖๙", - "redyear": "", - "curdate": "วันที่ เดือน สิงหาคม พุทธศักราช ๒๕๖๙", - "accuser": "นายธีรพล ร่มเริง", - "acc1": "ผู้ฟ้องคดี", - "accused": "รร", - "acc2": "ผู้ถูกฟ้องคดี", - "interpleader": "สมชาย ใจดี", - "acc3": "ผู้ร้องสอด", - "detail1": "ตำแหน่งปลัดอำเภอ (เจ้าพนักงานปกครองชำนาญการพิเศษ) ที่ทำการปกครองอำเภอแม่สาย จังหวัดเชียงราย กรมการปกครอง ได้รับความเดือดร้อนเสียหายจากการที่ผู้ถูกฟ้องคดีที่ ๒ มีคำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม ๒๕๖๓ ลงโทษไล่ผู้ฟ้องคดี ออกจากราชการ กรณีเมื่อครั้งผู้ฟ้องคดีดำรงตำแหน่งปลัดอำเภอ (เจ้าพนักงานปกครอง 5) ที่ทำการปกครองอำเภอพญาเม็งราย จังหวัดเชียงราย ถูกกล่าวหาว่ากระทำการทุจริตโครงการ จัดทำเวทีประชาคมหมู่บ้านเพื่อเสริมสร้างความเข้มแข็งให้ชุมชนอำเภอพญาเม็งราย จังหวัด เชียงราย และโครงการส่งเสริมการเกษตรแบบครบวงจร ซึ่งผู้ฟ้องคดีมีอำนาจหน้าที่เป็น คณะกรรมการตรวจรับพัสดุในโครงการดังกล่าว กระทำการจัดทำเอกสารหลักฐานการเบิกจ่ายเงิน อันเป็นเท็จและลงลายมือชื่อตรวจรับพัสดุอันเป็นเท็จ ซึ่งเป็นการไม่ปฏิบัติตามข้อ ๗๑ ของระเบียบสำนักนายกรัฐมนตรีว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ และที่แก้ไขเพิ่มเติม ผู้ถูกฟ้องคดีที่ ๑ ในการประชุมครั้งที่ ๑๐๕/๒๕๖๒ เมื่อวันที่ 4 ตุลาคม ๒๕๖๒ จึงมีมติชี้มูลความผิดอาญา ฐานเป็นเจ้าพนักงานปฏิบัติหรือละเว้นการปฏิบัติหน้าที่โดยมิชอบ เพื่อให้เกิดความเสียหาย แก่ผู้หนึ่งผู้ใด หรือปฏิบัติหรือละเว้นการปฏิบัติหน้าที่โดยทุจริต ตามมาตรา ๑๕๗ แห่งประมวล กฎหมายอาญา และความผิดทางวินัยอย่างร้ายแรง ฐานปฏิบัติหรือละเว้นการปฏิบัติหน้าที่ ราชการโดยมิชอบ เพื่อให้ตนเองหรือผู้อื่นได้ประโยชน์ที่มีควรได้ เป็นการทุจริตต่อหน้าที่ ราชการ ฐานปฏิบัติหน้าที่ราชการโดยจงใจไม่ปฏิบัติตามกฎหมาย ระเบียบของทางราชการ มติคณะรัฐมนตรี หรือนโยบายของรัฐบาล อันเป็นเหตุให้เสียหายแก่ราชการอย่างร้ายแรง และ ฐานกระทำการอื่นใดอันได้ชื่อว่าเป็นผู้ประพฤติชั่วอย่างร้ายแรง ตามมาตรา ๑๘๒ วรรคสาม มาตรา ๘๕ วรรคสอง และมาตรา ๔๘ วรรคสอง แห่งพระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๓๕ ประกอบมาตรา ๑๓๓ แห่งพระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๕๑ ผู้ถูกฟ้องคดีที่ ๑ ผู้ถูกฟ้องคดีที่ ๒ จึงได้ส่งรายงาน สำนวนการไต่สวน เอกสารหลักฐาน และคำวินิจฉัยไปยัง ซึ่งเป็นผู้บังคับบัญชาและผู้มีอำนาจบรรจุและแต่งตั้งผู้ฟ้องคดี ผู้ถูกฟ้องคดีที่ ๒ เสนอเรื่องต่อ อ.ก.พ. กระทรวงมหาดไทย ซึ่ง อ.ก.พ. กระทรวงมหาดไทย มีมติในการประชุม ครั้งที่ ๒/๒๕๖๓ เมื่อวันที่ ๑๘ มิถุนายน ๒๕๖๓ ให้ลงโทษไล่ผู้ฟ้องคดีออกจากราชการ ตามฐานความผิดที่ผู้ถูกฟ้องคดีที่ ๑ ชี้มูลความผิด ผู้ถูกฟ้องคดีที่ ๒ จึงมีคำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม ๒๕๖๓ ลงโทษไล่ผู้ฟ้องคดีออกจากราชการ ต่อมา ผู้ฟ้องคดีมีหนังสือลงวันที่ ๑๕ กันยายน ๒๕๖๓ อุทธรณ์คำสั่งดังกล่าวต่อคณะกรรมการพิทักษ์ ที่ ระบบคุณธรรม ต่อมาคณะกรรมการพิทักษ์ระบบคุณธรรมได้มีคำวินิจฉัย เรื่องดำที่ ๖๓๑๐๑๖๖ เรื่องแดงที่ ๐๐๔๘๑๖๕ ลงวันที่ ๕ กรกฎาคม ๒๕๖๕ ให้ยกอุทธรณ์ และมีหนังสือ นร ๑๐๑๐.๓.๑.๔/๒๕ ลงวันที่ 6 กรกฎาคม ๒๕๖๕ แจ้งคำวินิจฉัยดังกล่าวให้ผู้ฟ้องคดีทราบ ผู้ฟ้องคดีไม่เห็นพ้องด้วย เนื่องจากโครงการจัดทำเวทีประชาคมหมู่บ้านเพื่อเสริมสร้าง ความเข็มแข็งให้ชุมชนอำเภอพญาเม็งราย จังหวัดเชียงราย เป็นโครงการที่นายบุญยัง เรือนกูล นายอำเภอพญาเม็งราย ในขณะนั้น ได้ดำเนินการไปก่อนที่ผู้ฟ้องคดีจะได้รับการแต่งตั้ง ให้ดำรงตำแหน่งปลัดอำเภอพญาเม็งราย ครั้นเมื่อผู้ฟ้องคดีได้รับการแต่งตั้งให้ดำรงตำแหน่ง ดังกล่าว นายบุญยังได้ออกคำสั่งให้ผู้ฟ้องคดีปฏิบัติหน้าที่รับผิดชอบงานด้านความมั่นคง ตามคำสั่งอำเภอพญาเม็งราย ที่ ๓๗๒/๒๕๕๗ ลงวันที่ ๓ ธันวาคม ๒๕๔๗ ซึ่งรวมถึงโครงการ จัดทำเวทีประชาคมหมู่บ้านเพื่อเสริมสร้างความเข็มแข็งให้ชุมชนอำเภอพญาเม็งรายด้วย ผู้ฟ้องคดีในฐานะผู้ใต้บังคับบัญชาจึงต้องเชื่อฟังและปฏิบัติตามคำสั่งของนายบุญยัง แม้ผู้ฟ้องคดี จะเป็นผู้ลงนามเสนอและเปลี่ยนแปลงโครงการ แต่ผู้ฟ้องคดีไม่มีความเป็นอิสระในการบริหาร โดยอำนาจบริหารอยู่ภายใต้การสั่งการของนายบุญยัง ส่วนโครงการส่งเสริมการเกษต แบบครบวงจรเป็นการบริหารโครงการที่นายบุญยังเป็นผู้ดำเนินการควบคุมสั่งการทุกขั้น จนกระทั่งเสร็จสิ้นโครงการเช่นเดียวกัน เมื่อดำเนินการโครงการดังกล่าวเสร็จสิ้น นายบุญยัง และนางวัลลภาภรณ์ เรือนปาง เสมียนตราอำเภอพญาเม็งรายและกรรมการรับเงินอุดหนุนโครงการ จึงได้รวบรวมเอกสารที่เกี่ยวข้องเสนอผู้ฟ้องคดีให้ลงนามในฐานะคณะกรรมการตรวจรับพัสดุ โดยผู้ฟ้องคดีไม่มีโอกาสได้ปฏิบัติหน้าที่ตามระเบียบสำนักนายกรัฐมนตรีว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ เมื่อนางวัลลภาภรณ์ผู้ตรวจรับพัสดุยืนยันว่าได้ตรวจสอบพัสดุครบถ้วนทุกรายการ และส่งมอบให้ นายบุญยังแจกจ่ายครบถ้วนแล้ว ผู้ฟ้องคดีจึงลงนามตรวจรับพัสดุ จึงเห็นได้ว่า ผู้ฟ้องคดีไม่มี เจตนาทุจริต ข้อเท็จจริงไม่ปรากฏว่าผู้ฟ้องคดีมีทรัพย์สินเพิ่มมากขึ้นผิดปกติ หรือมีการ ได้ประโยชน์ในลักษณะทรัพย์สิน หรือประโยชน์อื่นใดจากการดำเนินโครงการ ผู้ฟ้องคดี ไม่ได้กระทำการทุจริตต่อหน้าที่ อีกทั้ง ผู้ถูกฟ้องคดีที่ ๑ ไม่มีพยานหลักฐานที่สามารถพิสูจน์ โดยปราศจากข้อสงสัยว่าผู้ฟ้องคดีกระทำการทุจริต การที่ผู้ถูกฟ้องคดีที่ ๑ ไต่สวนข้อเท็จจริง และมีมติชี้มูลความผิดจึงเป็นการไต่สวนและชี้มูลความผิดไม่ตรงกับข้อเท็จจริงและเป็นการ พยายามบิดเบือนว่าผู้ฟ้องคดีกระทำความผิดทางวินัยฐานทุจริต ทั้งที่ผู้ฟ้องคดีกระทำความผิด ทางวินัยฐานอื่น มติของผู้ถูกฟ้องคดีที่ ๑ ในการประชุมครั้งที่ ๑๐๕/๒๕๖๓ เมื่อวันที่ 4 ตุลาคม ๒๕๖๒ ที่ชี้มูลความผิดผู้ฟ้องคดีจึงเป็นมติที่ไม่ชอบด้วยกฎหมาย นอกจากนี้ ผู้ถูกฟ้องคดีที่ ๑ มีอำนาจไต่สวนและชี้มูลความผิดทางวินัยเฉพาะความผิดฐานทุจริตต่อหน้าที่ราชการเท่านั้น เมื่อผู้ฟ้องคดีไม่ได้กระทำความผิดฐานทุจริตต่อหน้าที่ ผู้ถูกฟ้องคดีที่ ๑ จึงไม่มีอำนาจไต่สวน และชี้มูลความผิดผู้ฟ้องคดี และไม่ผูกพันผู้ถูกฟ้องคดีที่ ๒ ให้ต้องถือรายงานการไต่สวนของ ผู้ถูกฟ้องคดีที่ ๑ เป็นรายงานการสอบสวน โดยผู้ถูกฟ้องคดีที่ ๒ ต้องดำเนินการแต่งตั้ง คณะกรรมการสอบสวนความผิดทางวินัยเพื่อสอบสวนข้อเท็จจริง เมื่อผู้ถูกฟ้องคดีที่ ๒ ไม่ได้ แต่งตั้งคณะกรรมการดังกล่าว การที่ผู้ถูกฟ้องคดีที่ ๒ มีคำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม ๒๕๖๓ ลงโทษไล่ผู้ฟ้องคดีออกจากราชการ จึงเป็นการออกคำสั่งโดยไม่ถูกต้อง ตามรูปแบบ ขั้นตอน หรือวิธีการ อันเป็นสาระสำคัญที่กำหนดไว้สำหรับการออกคำสั่งนั้น ผู้ฟ้องคดีจึงนำคดีมาฟ้องต่อศาลปกครองสูงสุดโดยวิธีส่งทางไปรษณีย์ลงทะเบียน เมื่อวันที่ ๓ ตุลาคม ๒๕๖\n\nขอให้ศาลมีคำพิพากษาหรือคำสั่ง ดังนี้ \n\n๑. เพิกถอนมติของผู้ถูกฟ้องคดีที่ ๑ ในการประชุมครั้งที่ ๑๐๕/๒๕๖๒ เมื่อวันที่ ๘ ตุลาคม ๒๕๖๒ ที่ชี้มูลความผิดของผู้ฟ้องคดี \n\n๒. เพิกถอนคำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม ๒๕๖๓ ที่ลงโทษไล่ผู้ฟ้องคดีออกจากราชการ \n\n๓. ให้แก่ผู้ฟ้องคดี คดี\n\nศาลได้พิจารณาคำฟ้องและคำขอของผู้ฟ้องคดีแล้ว สามารถแปลความได้ว่า ผู้ฟ้องคดีมีความประสงค์จะขอให้ศาลมีคำพิพากษาหรือคำสั่งเพิกถอนคำวินิจฉัยอุทธรณ์ ของคณะกรรมการพิทักษ์ระบบคุณธรรม เรื่องดำที่ ๖๓๑๐๑๖๖ เรื่องแดงที่ ๑๐๔๘๑๖๕ ลงวันที่ ๕ กรกฎาคม ๒๕๖๕ ที่ให้ยกอุทธรณ์ของผู้ฟ้องคดี ศาลจึงมีคำสั่งลงวันที่ ๒๘ กุมภาพันธ์ ๒๕๖๖ เรียกให้คณะกรรมการพิทักษ์ระบบคุณธรรมเข้ามาเป็นผู้ถูกฟ้องคดีที่ ๓ ตาม มาตรา ๔๔ แห่งพระราชบัญญัติจัดตั้งศาลปกครองและวิธีพิจารณาคดีปกครอง พ.ศ. ๒๕๔๒ ประกอบกับข้อ ๗๘ แห่งระเบียบของที่ประชุมใหญ่ตุลาการในศาลปกครองสูงสุด ว่าด้วย วิธีพิจารณาคดีปกครอง พ.ศ. ๒๕๔๓ และมาตรา ๕๗ และมาตรา ๕๘ แห่งประมวลกฎหมาย วิธีพิจารณาความแพ่ง\n\nผู้ถูกฟ้องคดีที่ ๑ ให้การว่า ขณะเกิดเหตุคดีนี้ ผู้ฟ้องคดีดำรงตำแหน่งปลัดอำเภอ (พนักงานปกครอง 5) ที่ทำการปกครองอำเภอพญาเม็งราย จังหวัดเชียงราย มีอำนาจหน้าที่ ช่วยเหลือนายอำเภอในการปฏิบัติราชการให้เป็นไปตามพระราชบัญญัติระเบียบบริหารราชการ แผ่นดิน พ.ศ. ๒๕๓๔ และที่แก้ไขเพิ่มเติม โดยได้รับการแต่งตั้งให้เป็นคณะกรรมการตรวจรับ พัสดุในโครงการส่งเสริมการเกษตรแบบครบวงจรของที่ทำการปกครองอำเภอพญาเม็งราย ปีงบประมาณ พ.ศ. ๒๕๕๗ และได้รับการแต่งตั้งให้เป็นคณะกรรมการตรวจรับงานจ้างในโครงการ จัดทำเวทีประชาคมหมู่บ้านเพื่อเสริมสร้างความเข้มแข็งให้ชุมชนในอำเภอพญาเม็งราย จังหวัดเชียงราย งบประมาณปี พ.ศ. ๒๕๕๘ ตามระเบียบสำนักนายกรัฐมนตรีว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ และที่แก้ไขเพิ่มเติม ข้อเท็จจริงปรากฏว่า โครงการจัดทำเวทีประชาคมไม่มีการจัดโครงการ ตามที่มีการจัดซื้อจัดจ้าง และการจัดซื้อวัสดุตามโครงการส่งเสริมการเกษตรไม่ได้รับพัสดุ ครบถ้วนตามที่ได้มีการจัดซื้อ อันเป็นการไม่ปฏิบัติตามข้อ ๗๑ ของระเบียบสำนักนายกรัฐมนตรี ว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ และที่แก้ไขเพิ่มเติม ทำให้เกิดความเสียหายต่อทางราชการ มีมูล ความผิดทางอาญา ตามประมวลกฎหมายอาญา มาตรา ๑๕๗ และมีมูลความผิดทางวินัย อย่างร้ายแรง ตามพระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๓๕ มาตรา ๑๒ วรรคสาม มาตรา ๘๕ วรรคสอง และมาตรา ๔๘ วรรคสอง เมื่อผู้ถูกฟ้องคดีที่ ๑ ได้รับเรื่องกล่าวหา จากสำนักงานการตรวจเงินแผ่นดิน และสำนักตรวจสอบพิเศษ ภาค ๘ (จังหวัดเชียงใหม่) เรื่อง การตรวจสอบการใช้จ่ายเงินของที่ทำการปกครองอำเภอพญาเม็งราย จังหวัดเชียงราย จึงมีมติ แต่งตั้งคณะกรรมการไต่สวนเพื่อดำเนินการไต่สวน ซึ่งคณะกรรมการไต่สวนได้ไต่สวน ข้อเท็จจริงและรวบรวมพยานหลักฐานแล้วเห็นว่าพยานหลักฐานเพียงพอพิสูจน์ได้ว่าผู้ฟ้องคดี กระทำความผิดตามข้อกล่าวหา จึงแจ้งข้อกล่าวหากับผู้ฟ้องคดี ผู้ฟ้องคดีรับทราบข้อกล่าวหา และมีหนังสือชี้แจงแก้ข้อกล่าวหาแล้ว ผู้ถูกฟ้องคดีที่ ๑ พิจารณาข้อเท็จจริงพร้อมพยานหลักฐาน\n\nตามสำนวนการไต่สวนของคณะกรรมการไต่สวนในการประชุมครั้งที่ ๑๐๕/๒๕๖๒ เมื่อวันที่ ๘ ตุลาคม ๒๕๖๒ แล้วมีมติว่า การกระทำของผู้ฟ้องคดีมีมูลความผิดทางอาญา ตามประมวลกฎหมายอาญา มาตรา ๑๕๗ และมีมูลความผิดทางวินัยอย่างร้ายแรง ฐานปฏิบัติหรือละเว้นการปฏิบัติหน้าที่ราชการ โดยมิชอบ เพื่อให้ตนเองหรือผู้อื่นได้ประโยชน์ที่มีควรได้ เป็นการทุจริตต่อหน้าที่ราชการ ฐานปฏิบัติหน้าที่ราชการโดยจงใจไม่ปฏิบัติตามกฎหมาย ระเบียบของทางราชการ มติคณะรัฐมนตรี หรือนโยบายของรัฐบาล อันเป็นเหตุให้เสียหายแก่ราชการอย่างร้ายแรง และฐานกระทำการอื่นใด อันได้ชื่อว่าเป็นผู้ประพฤติชั่วอย่างร้ายแรง ตามพระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๓๕ มาตรา ๒ วรรคสาม มาตรา ๕๕ วรรคสอง และมาตรา ๔๘ วรรคสอง เมื่อผู้ถูกฟ้องคดีที่ ๑ ไต่สวนแล้วมีมติว่าผู้ฟ้องคดีกระทำความผิดฐานทุจริตต่อหน้าที่ หรือกระทำความผิดต่อตำแหน่ง หน้าที่ราชการ อันเป็นความผิดมูลฐานตามที่รัฐธรรมนูญกำหนดไว้แล้ว ผู้ถูกฟ้องคดีที่ ๑ จึงมี อำนาจชี้มูลความผิดทางวินัยผู้ฟ้องคดีฐานอื่นซึ่งเป็นความผิดที่เกี่ยวข้องกันได้ ตามบทบัญญัติ มาตรา ๑๙ (๔) ประกอบมาตรา ๘๔ แห่งพระราชบัญญัติประกอบรัฐธรรมนูญว่าด้วยการป้องกัน และปราบปรามการทุจริต พ.ศ. ๒๕๔๒ และที่แก้ไขเพิ่มเติม ดังนั้น การไต่สวนข้อเท็จจริง มติชี้มูลความผิด และคำวินิจฉัยของผู้ถูกฟ้องคดีที่ ๑ ตลอดจนการที่ผู้ถูกฟ้องคดีที่ ๑ ชี้มูล ความผิดทางวินัยและส่งเรื่องให้ผู้ถูกฟ้องคดีที่ ๒ ซึ่งผู้บังคับบัญชาของผู้ฟ้องคดีดำเนินการ ทางวินัย จึงเป็นการดำเนินการตามขั้นตอนที่กฎหมายและระเบียบกำหนดไว้แล้ว และมีผล ผูกพันผู้ถูกฟ้องคดีที่ ๒ ซึ่งเป็นผู้บังคับบัญชาของผู้ฟ้องคดีในการพิจารณาโทษทางวินัยตามมติ ของผู้ถูกฟ้องคดีที่ ๑ การที่ผู้ถูกฟ้องคดีที่ ๒ มีคำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม ๒๕๖๓ ลงโทษไล่ผู้ฟ้องคดีออกจากราชการตามฐานความผิดที่ผู้ถูกฟ้องคดีที่ ๑ ได้มีมติจึงชอบด้วยกฎหมายแล้ว นอกจากนี้ อัยการสูงสุดได้ยื่นฟ้องผู้ฟ้องคดีเป็นจำเลยที่ ๒ ต่อศาลอาญาคดีทุจริตและประพฤติมิชอบภาค ๕ ในคดีหมายเลขดำที่ อท ๑๓๕, ๑๓๖/๒๕๖๒ คดีหมายเลขแดงที่ อท ๗๘, ๗๙/๒๕๖๓ ซึ่งศาลอาญาคดีทุจริตและประพฤติมิชอบภาค ๕ ได้วินิจฉัยว่า โครงการจัดทำเวทีประชาคมหมู่บ้านเพื่อเสริมสร้างความเข้มแข็งให้ชุมชน อำเภอ พญาเม็งราย จังหวัดเชียงราย ผู้ฟ้องคดีในฐานะประธานกรรมการตรวจการจ้างและตรวจรับพัสดุ ได้ลงนามรับรองเป็นหลักฐานว่า มีการส่งมอบการจ้างและพัสดุต่างๆ ถูกต้องครบถ้วน และยังสั่งการ ให้คณะกรรมการตรวจการจ้างและตรวจรับพัสดุคนอื่นที่เป็นสมาชิกอาสารักษาดินแดน ซึ่งเป็น ผู้ใต้บังคับบัญชาโดยตรงของผู้ฟ้องคดี ลงนามในเอกสารเพื่อใช้เป็นหลักฐานในการเบิกจ่ายเงิน ของทางราชการ ทั้งที่ข้อเท็จจริงไม่มีการจัดโครงการและไม่มีการตรวจการจ้างและตรวจรับพัสดุ ตามที่มีการเบิกจ่ายแต่อย่างใด และในส่วนของโครงการส่งเสริมการเกษตรแบบครบวงจร ผู้ฟ้องคดี มีเจตนาร่วมกับนายบุญยัง เรือนกูล ในการทำเอกสารหลักฐานการเบิกจ่ายอันเป็นเท็จ เนื่องจากไม่ปฏิบัติตามระเบียบสำนักนายกรัฐมนตรีว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ และที่แก้ไขเพิ่มเติม การกระทำของผู้ฟ้องคดีจึงเป็นความผิด ฐานเป็นเจ้าพนักงานปฏิบัติหรือละเว้นการปฏิบัติหน้าที่ โดยมิชอบ เพื่อให้เกิดความเสียหายแก่ผู้หนึ่งผู้ใด หรือปฏิบัติหรือละเว้นการปฏิบัติหน้าที่โดยทุจริต พิพากษาจำคุกผู้ฟ้องคดี 5 ปี\n\nผู้ถูกฟ้องคดีที่ ๒ ให้การในทำนองเดียวกับคำให้การของผู้ถูกฟ้องคดีที่ ๑ และ เพิ่มเติมว่า มูลเหตุคดีนี้เกิดจากอำเภอพญาเม็งรายได้จัดทำโครงการจัดทำเวทีประชาคม หมู่บ้านเพื่อเสริมสร้างความเข้มแข็งให้ชุมชนอำเภอพญาเม็งราย ปีงบประมาณ พ.ศ. ๒๕๕๘ เพื่อให้ ราษฎรมีส่วนร่วมในการป้องกันและเฝ้าระวังปัญหายาเสพติดในพื้นที่ ๕ ตำบล ๖๙ หมู่บ้าน ระยะเวลาดำเนินการตั้งแต่วันที่ ๑๕ ธันวาคม ๒๕๕๗ ถึงวันที่ ๓๑ มกราคม ๒๕๔๘ ได้รับการ สนับสนุนงบประมาณจากองค์การบริหารส่วนตำบล จำนวน ๕ แห่ง แห่งละ ๑๐,๐๐๐ บาท และเทศบาลตำบลพญาเม็งราย ๒๐,๐๐๐ บาท รวมเป็นเงิน ๗๐,๐๐๐ บาท โดยมีผู้ฟ้องคดี เป็นผู้เสนอโครงการ โดยผ่านการเห็นชอบจากนายประจญ ปรัชญ์สกุล ปลัดอำเภอหัวหน้าฝ่าย บริหารงานปกครอง และได้รับอนุมัติโครงการจากนายบุญยัง เรือนกูล นายอำเภอพญาเม็งราย และอำเภอพญาเม็งรายได้มีคำสั่ง ที่ ๓๙๖/๒๕๕๗ ลงวันที่ ๒๗ ธันวาคม ๒๕๕๗ แต่งตั้ง ดังกล่าว นางวัลลภาภรณ์ เรือนปาง เสมียนตราอำเภอพญาเม็งราย เป็นกรรมการรับเงินอุดหนุนโครงการ ต่อมา ผู้ฟ้องคดีได้เสนอขออนุมัติเปลี่ยนแปลงกำหนดการจัดทำโครงการการจัดทำเวที ประชาคมหมู่บ้าน โดยให้จัดประชุมให้แล้วเสร็จภายในวันที่ ๑๙ มกราคม ๒๕๕๘ ณ หอประชุม พญาเม็งราย ดังนี้ หมู่ที่ ๑ ๒๐ ตำบลแม่เปา วันที่ ๑๗ มกราคม ๒๕๔๗ หมู่ที่ ๑ - ๑๔ ตำบล เม็งราย วันที่ ๑๘ มกราคม ๒๕๕๗ หมู่ที่ ๑ ๑๑ ตำบลแม่ต่ำ วันที่ ๑๘ มกราคม ๒๕๕๗ หมู่ที่ ๑- ๑๘ ตำบลไม้ยา วันที่ ๑๙ มกราคม ๒๕๕๗ และหมู่ที่ ๑ ๘ ตำบลตาดควัน วันที่ ๑๙ มกราคม ๒๕๔๗ นายบุญยัง ได้ลงนามอนุมัติท้ายบันทึกดังกล่าว และมีผู้ฟ้องคดี นายพัฒน์ อินถา และนายทัศน์ ถาแก้ว เป็นคณะกรรมการตรวจรับพัสดุการดำเนินการจัดซื้อจัดจ้าง จำนวน ๓ รายการ ดังนี้ (๑) วัสดุแบบพิมพ์อบรมสร้างความเข้มแข็งให้ชุมชน และกระดาษถ่ายเอกสารจำนวน ๑๐ รีม รีมละ ๑๐๐ บาท เป็นเงิน ๑,๐๐๐ บาท จากการตรวจสอบข้อเท็จจริงพบว่า โครงการจัดทำเวที ประชาคมหมู่บ้านฯ ไม่มีการจัดเตรียมเอกสารหรือแจกจ่ายเอกสาร จึงไม่ได้มีการใช้วัสดุที่จัดซื้อ ให้เป็นไปตามวัตถุประสงค์ของโครงการ (๒) จ้างเหมาจัดทำอาหาร/อาหารว่าง เครื่องดื่ม ให้ผู้เข้าอบรมโครงการการจัดทำเวทีประชาคมหมู่บ้านฯ กับนายสว่าง วรรณสอน สมาชิก กองอาสารักษาดินแดน จากการตรวจสอบข้อเท็จจริงพบว่าไม่มีการจ้างนายสว่าง ผู้รับจ้าง ทำอาหารจริง และนายสว่างไม่มีอาชีพในการทำอาหาร ทั้งปรากฏหลักฐานการเบิกจ่ายซึ่งไม่มี รายละเอียดในการจัดหาอาหารในแต่ละวัน จำนวนอาหารในแต่ละวันมีความเหมาะสมกับราคาหรือไม่ เป็นการยากต่อการดำเนินการตรวจรับว่ามีการจัดจ้างตรงตามใบสั่งจ้างหรือไม่ (๓) จ้างเหมา จัดสถานที่และเช่าเครื่องเสียงจัดทำเวทีประชาคมฯ จำนวน ๓๐,๐๐๐ บาท กับนายสุวนันท์ ไชยานันตา สมาชิกกองอาสารักษาดินแดน จากการตรวจสอบข้อเท็จจริงพบว่า นายสุวนันท์ไม่มีอาชีพในการ จัดสถานที่หรือมีเครื่องเสียงให้เช่า ไม่มีการจ้างเหมาจัดสถานที่และเช่าเครื่องเสียง ไม่มีการ จ่ายเงินให้กับนายสุวนันท์จริง และเอกสารมีการดำเนินการไม่ถูกต้อง เช่น ใบส่งมอบงานระบุ วันที่ ๑๙ มกราคม ๒๕๕๙ และใบสำคัญรับเงินไม่ระบุวันที่ มกราคม ๒๕๕๐ เป็นต้น ส่วนที่ อำเภอพญาเม็งรายได้จัดทำโครงการจ่ายเงินอุดหนุนและการจัดซื้อวัสดุโครงการส่งเสริมเกษตร แบบครบวงจร จำนวน ๓๘๔,๐๐๐ บาท จากการตรวจสอบเอกสารปีงบประมาณ พ.ศ. ๒๕๔๗ ๒๕๔๘ พบว่า มีการจัดซื้อโดยแบ่งการจัดซื้อจำนวน 5 ครั้ง โดยแยกบันทึกเป็นแต่ละหมู่บ้านจำนวน 5 หมู่บ้าน ครั้งละ ๑๑ รายการ วงเงินจัดซื้อหมู่บ้านละ ๖๔,๐๐๐ บาท ตามใบสั่งซื้อเลขที่ ๗/๒๕๔๘ - ๑๒/๒๕๕๘ ลงวันที่ ๑๔ กุมภาพันธ์ ๒๕๕๘ รายการที่จัดซื้อประกอบด้วย (๑) เครื่องสกัดยาฆ่าแมลง (๒) เครื่องบดดิน (๓) ถังพ่นยา (๔) ถั่วฝักยาว (๕) ผักบุ้งจีน (๖) แตงกวา (๓) คะน้า (๘) กวางตุ้ง (๔) ผักชี (๑๐) ผักขึ้นฉ่าย ผักข (๑๑) บวบเหลี่ยม รวมเป็นเงินทั้งสิ้น ๓๘๔,๐๐๐ บาท เป็นรายการจัดซื้อวัสดุจากนายธนกร ไชยวรรณ์ โดยจัดซื้อในวิธีตกลงราคาให้กับ เกษตรกรในพื้นที่อำเภอพญาเม็งรายใน 5 หมู่บ้าน มีผู้ฟ้องคดี นายพัฒน์ อินถา และนายทัศน์ ถาแก้ว เป็นคณะกรรมการตรวจรับพัสดุ ซึ่งคณะกรรมการตรวจรับพัสดุได้ลงลายมือชื่อในใบตรวจรับ โดยเห็นว่ามีปริมาณและคุณภาพถูกต้อง พร้อมส่งมอบให้กับเจ้าหน้าที่พัสดุแล้ว แต่จากการ ตรวจสอบข้อเท็จจริงพบว่าโครงการส่งเสริมการเกษตรแบบครบวงจรไม่มีการจัดทำโครงการที่ระบุ รายละเอียดของวัตถุประสงค์ กลุ่มเป้าหมาย ผลที่ได้รับจากโครงการอันไม่มีจุดประสงค์ในการ ใช้งบประมาณ และในการขอซื้อวัสดุอุปกรณ์ในรายงานการขอซื้อ ไม่มีที่มาของราคาของวัสดุ และมีการจัดวัสดุชนิดเดียวกัน1 ๑๑ ๑๑ รายการ แยกเป็น 5 ครั้ง ในวันเดียวกัน อันทำให้อำนาจ ในการสั่งซื้อเปลี่ยนไปเป็นการแบ่งซื้อตามข้อ ๒๒ วรรคสอง ของระเบียบสำนักนายกรัฐมนตรี ว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ และที่แก้ไขเพิ่มเติม อีกทั้ง เมื่อตรวจสอบสำเนาเอกสารบัญชี รับพัสดุของที่ทำการปกครองอำเภอพญาเม็งราย ผู้ลงลายมือชื่อรับพัสดุแจ้งว่าลงลายมือชื่อตามที่ ที่ทำการปกครองอำเภอพญาเม็งรายให้ลงลายมือชื่อโดยไม่ได้ตรวจสอบ และบางรายยืนยันว่าไม่ใช่ ลายมือชื่อของตนและยืนยันว่าได้รับเฉพาะเครื่องสกัดยาฆ่าแมลงและเครื่องบดดิน ส่วนรายการอื่นๆ ไม่ได้รับ \n\nเมื่อผู้ถูกฟ้องคดีที่ ๑ ได้รับเรื่องกล่าวหา พิจารณาพฤติการณ์แห่งการกระทำ ความผิด และมีมติชี้มูลความผิดทางวินัยผู้ฟ้องคดีแล้ว จึงส่งรายงานสำนวนการสอบสวน เอกสารหลักฐาน พร้อมทั้งคำวินิจฉัยมายังผู้ถูกฟ้องคดีที่ ๒ ซึ่งเป็นผู้บังคับบัญชาของผู้ฟ้องคดี\n\nเพื่อพิจารณาโทษทางวินัยตามฐานความผิดที่ผู้ถูกฟ้องคดีที่ ๑ ได้มีมติ โดยไม่ต้องแต่งตั้ง คณะกรรมการสอบสวนวินัยอีก และให้ถือว่าสำนวนการไต่สวนของผู้ถูกฟ้องคดีที่ ๑ เป็นสำนวน การสอบสวนทางวินัยของคณะกรรมการสอบสวนวินัย ตามมาตรา ๔๘ แห่งพระราชบัญญัติ ขั้นตอน ประกอบรัฐธรรมนูญว่าด้วยการป้องกันและปราบปรามการทุจริต พ.ศ. ๒๕๖๑ การที่ผู้ถูกฟ้องคดีที่ ๒ มีคำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม ๒๕๖๓ ลงโทษไล่ผู้ฟ้องคดี ออกจากราชการตามฐานความผิดที่ผู้ถูกฟ้องคดีที่ ๑ มีมติ เป็นการลงโทษตามแนวการลงโทษ ตามมติคณะรัฐมนตรี และระดับโทษเหมาะสมแก่กรณีกระทำผิดแล้ว โดยที่การพิจารณาโทษ ทางวินัยกับผู้ฟ้องคดีนั้น ผู้ถูกฟ้องคดีที่ ๒ ได้รับฟังข้อเท็จจริงถูกต้องตามหลักเกณฑ์ รูปแบบ หรือวิธีการอันเป็นสาระสำคัญเกี่ยวกับการลงโทษทางวินัยอย่างร้ายแรงตามที่กฎหมาย กำหนด แต่ผู้ถูกฟ้องคดีที่ ๒ ไม่สามารถวินิจฉัยกระบวนการไต่สวนหรือมติ มูลของผู้ถูกฟ้องคดีที่ ๑ เป็นอย่างอื่นได้ เนื่องจากสำนวนการไต่สวนของผู้ถูกฟ้องคดีที่ ๑ มีผลผูกพันผู้ถูกฟ้องคดีที่ ๒ ที่จะต้องพิจารณาโทษทางวินัยตามฐานความผิดที่ผู้ถูกฟ้องคดีที่ ๑ มีมติ ซึ่งผู้ถูกฟ้องคดีที่ ๒ ได้ดำเนินการตามหลักเกณฑ์ รูปแบบ ขั้นตอน หรือวิธีการอันเป็นสาระสำคัญถูกต้องตามที่ กฎหมายกำหนดแล้ว ดังนั้น คำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม ๒๕๖๓ ลงโทษไล่ผู้ฟ้องคดีออกจากราชการ จึงเป็นคำสั่งที่ชอบด้วยกฎหมาย ไม่มีเหตุที่จะให้ ผู้ฟ้องคดีกลับเข้ารับราชการและคืนสิทธิประโยชน์ให้แก่ผู้ฟ้องคดีแต่อย่างใด\n\nผู้ถูกฟ้องคดีที่ ๓ ให้การในทำนองเดียวกับคำให้การของผู้ถูกฟ้องคดีที่ ๑ และ ผู้ถูกฟ้องคดีที่ ๓ ขอยืนยันตามวินิจฉัยของผู้ถูกฟ้องคดีที่ ๓ เรื่องคำที่ ผู้ถูกฟ้องคดีที่ ๒ และเพิ่มเติมว่า ๖๓๑๐๑๖๖ เรื่องแดงที่ ๑๐๔๘๑๖๕ ลงวันที่ ๕ กรกฎาคม ๒๕๖๕ ซึ่งผู้ถูกฟ้องคดีที่ ๓ ได้วินิจฉัยอุทธรณ์ของผู้ฟ้องคดีตามอำนาจหน้าที่ที่กฎหมายกำหนด และได้พิจารณาข้อเท็จจริง ตามพยานหลักฐานที่ปรากฏอย่างละเอียดรอบคอบ ปราศจากอคติ และให้ความเป็นธรรม แก่ผู้ฟ้องคดี ตามหลักเกณฑ์และวิธีการที่กฎ ก.พ.ค. ว่าด้วยการอุทธรณ์และการพิจารณา วินิจฉัยอุทธรณ์ พ.ศ. ๒๕๕๑ แล้ว ดังนั้น การที่ผู้ถูกฟ้องคดีที่ ๓ มีคำวินิจฉัยยกอุทธรณ์ของผู้ฟ้องคดี จึงเป็นการกระทำที่ถูกต้องและชอบด้วยกฎหมายแล้ว\n\nศาลปกครองสูงสุดออกนั่งพิจารณาคดีครั้งแรกเมื่อวันที่ 4 มีนาคม ๒๕๖๙ โดยได้รับฟังสรุปข้อเท็จจริงของตุลาการเจ้าของสำนวน พิจารณาคำชี้แจงด้วยวาจาประกอบ คำแถลงการณ์เป็นหนังสือของตุลาการผู้แถลงคดี\n\nศาลปกครองสูงสุดได้ตรวจพิจารณาพยานหลักฐานในคำฟ้อง คำให้การ คำคัดค้านคำให้การ คำให้การเพิ่มเติม แล้ว\n\nข้อเท็จจริงรับฟังได้ว่า เดิมผู้ฟ้องคดีรับราชการเป็นข้าราชการพลเรือนสามัญ ตำแหน่งปลัดอำเภอ (เจ้าพนักงานปกครองชำนาญการพิเศษ) ที่ทำการปกครองอำเภอแม่สาย จังหวัดเชียงราย กรมการปกครอง ได้รับความเดือดร้อนเสียหายจากการที่ผู้ถูกฟ้องคดีที่ ๒ มีคำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม ๒๕๖๓ ลงโทษไล่ผู้ฟ้องคดี ออกจากราชการ ตั้งแต่วันที่ ๒๓ พฤษภาคม ๒๕๖๓ เป็นต้นไป กรณีเมื่อครั้งผู้ฟ้องคดีดำรงตำแหน่ง ปลัดอำเภอ (เจ้าพนักงานปกครอง 5) ที่ทำการปกครองอำเภอพญาเม็งราย จังหวัดเชียงราย ถูกกล่าวหาว่ากระทำการทุจริตโครงการส่งเสริมการเกษตรแบบครบวงจร และโครงการจัดทำ เวทีประชาคมหมู่บ้านเพื่อเสริมสร้างความเข้มแข็งให้ชุมชนอำเภอพญาเม็งราย จังหวัดเชียงราย ซึ่งผู้ฟ้องคดีมีอำนาจหน้าที่เป็นคณะกรรมการตรวจรับพัสดุในโครงการดังกล่าว กระทำการ จัดทำเอกสารหลักฐานการเบิกจ่ายเงินอันเป็นเท็จและลงลายมือชื่อตรวจรับพัสดุอันเป็นเท็จ ซึ่งเป็นการไม่ปฏิบัติตามข้อ ๗๑ ของระเบียบสำนักนายกรัฐมนตรีว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ และที่แก้ไขเพิ่มเติม ผู้ถูกฟ้องคดีที่ ๑ ในการประชุมครั้งที่ ๑๐๕/๒๕๖๒ เมื่อวันที่4 ตุลาคม ๒๕๖๒ จึงมีมติชี้มูลความผิดอาญา ฐานเป็นเจ้าพนักงานปฏิบัติหรือละเว้นการปฏิบัติหน้าที่ โดยมิชอบ เพื่อให้เกิดความเสียหายแก่ผู้หนึ่งผู้ใด หรือปฏิบัติหรือละเว้นการปฏิบัติหน้าที่ โดยทุจริต ตามมาตรา ๑๕๗ แห่งประมวลกฎหมายอาญา และความผิดทางวินัยอย่างร้ายแรง ฐานปฏิบัติหรือละเว้นการปฏิบัติหน้าที่ราชการโดยมิชอบ เพื่อให้ตนเองหรือผู้อื่นได้ประโยชน์ ที่มีควรได้ เป็นการทุจริตต่อหน้าที่ราชการ ฐานปฏิบัติหน้าที่ราชการโดยจงใจไม่ปฏิบัติตาม กฎหมาย ระเบียบของทางราชการ มติคณะรัฐมนตรี หรือนโยบายของรัฐบาล อันเป็นเหตุให้ เสียหายแก่ราชการอย่างร้ายแรง และฐานกระทำการอื่นใดอันได้ชื่อว่าเป็นผู้ประพฤติชั่วอย่างร้ายแรง ตามมาตรา ๘๒ วรรคสาม มาตรา ๘๕ วรรคสอง และมาตรา ๙๘ วรรคสอง แห่งพระราชบัญญัติ ระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๓๕ ประกอบมาตรา ๑๓๓ แห่งพระราชบัญญัติระเบียบ ข้าราชการพลเรือน พ.ศ. ๒๕๕๑ ผู้ถูกฟ้องคดีที่ ๑ จึงมีหนังสือสำนักงาน ป.ป.ช. ที่ ปช ๐๑.๐๑ (ชร) ๑๔๔๒ ลงวันที่ 6 พฤศจิกายน ๒๕๖๒ ส่งรายงาน สำนวนการไต่สวน เอกสารหลักฐาน และคำวินิจฉัย ซึ่งเป็นผู้บังคับบัญชาและผู้มีอำนาจบรรจุและแต่งตั้งผู้ฟ้องคดี ไปยังผู้ถูกฟ้องคดีที่ ๒ ตามมาตรา ๕๗ (๔) แห่งพระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๕๑ ผู้ถูกฟ้องคดีที่ ๒ เสนอเรื่องให้ อ.ก.พ. กระทรวงมหาดไทย พิจารณา ซึ่ง อ.ก.พ. กระทรวงมหาดไทย มีมติในการประชุม ครั้งที่ ๒/๒๕๖๓ เมื่อวันที่ ๑๘ มิถุนายน ๒๕๖๓ ให้ลงโทษไล่ผู้ฟ้องคดีออกจากราชการตามฐาน ความผิดที่ผู้ถูกฟ้องคดีที่ ๑ ชี้มูลความผิด และตามมติคณะรัฐมนตรี เมื่อวันที่ ๒๑ ธันวาคม ๒๕๓๖ แจ้งตามหนังสือสำนักเลขาธิการคณะรัฐมนตรี ที่ นร ๐๒๐๕/ว ๒๓๔ ลงวันที่ ๒๔ ธันวาคม ๒๕๓๖ ผู้ถูกฟ้องคดีที่ ๒ จึงมีคำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม\n\n๒๕๖๓ ลงโทษไล่ผู้ฟ้องคดีออกจากราชการ ตั้งแต่วันที่ ๒๓ พฤษภาคม ๒๕๖๓ เป็นต้นไป ซึ่งเป็นวันที่ผู้ฟ้องคดีถูกพักราชการไว้ก่อน ต่อมา ผู้ฟ้องคดีมีหนังสือลงวันที่ ๑๕ กันยายน ๒๕๖๓ อุทธรณ์คำสั่งดังกล่าวต่อผู้ถูกฟ้องคดีที่ ๓ ผู้ถูกฟ้องคดีที่ ๓ พิจารณาแล้วจึงมีคำวินิจฉัย เรื่องดำที่ ผู้ฟ้องคดี ๖๓๑๐๑๖๖ เรื่องแดงที่ ๐๐๔๘๑๖๕ ลงวันที่ ๕ กรกฎาคม ๒๕๖๕ ให้ยกอุทธรณ์ และมีหนังสือ ที่ นร ๑๐๑๐.๓.๑.๔/๒๕ ลงวันที่ 4 กรกฎาคม ๒๕๖๕ แจ้งคำวินิจฉัยดังกล่าว ให้ผู้ฟ้องคดีทราบ ผู้ฟ้องคดีไม่เห็นพ้องด้วย เนื่องจากผู้ฟ้องคดีปฏิบัติตามคำสั่งของผู้บังคับบัญชา ไม่มีเจตนาทุจริต ไม่มีพยานหลักฐานว่าผู้ฟ้องคดีทุจริต ผู้ถูกฟ้องคดีที่ ๑ จึงไม่มีอำนาจชี้มูล จึงนำคดีมาฟ้องต่อศาลปกครองสูงสุด\n\nศาลปกครองสูงสุดได้ตรวจพิจารณากฎหมาย ระเบียบ และข้อบังคับที่เกี่ยวข้อง ประกอบด้วยแล้ว\n\nคดีมีประเด็นที่จะต้องวินิจฉัยว่า คำวินิจฉัยเรื่องดำที่ ๖๓๑๐๑๖๖ เรื่องแดง ที่ ๑๐๔๘๑๖๕ ลงวันที่ ๕ กรกฎาคม ๒๕๖๕ ให้ยกอุทธรณ์ของผู้ฟ้องคดี เป็นการกระทำการ โดยชอบด้วยกฎหมายหรือไม่ โดยคดีมีปัญหาต้องวินิจฉัยก่อนว่า คำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม ๒๕๖๓ ที่ลงโทษไล่ผู้ฟ้องคดีออกจากราชการ ตามมติ ของผู้ถูกฟ้องคดีที่ ๑ ในการประชุมครั้งที่ ๑๐๕/๒๕๖๒ เมื่อวันที่ 6 ตุลาคม ๒๕๖๒ เป็นการ กระทำโดยถูกต้องตามรูปแบบขั้นตอน หรือวิธีการอันเป็นสาระสำคัญที่กำหนดไว้สำหรับ การกระทำนั้นหรือไม\n\nพิเคราะห์แล้วเห็นว่า พระราชบัญญัติประกอบรัฐธรรมนูญว่าด้วยการป้องกัน และปราบปรามการทุจริต พ.ศ. ๒๕๖๑ มาตรา ๔ บัญญัติว่า “เจ้าหน้าที่ของรัฐ” หมายความว่า ข้าราชการหรือพนักงานส่วนท้องถิ่นซึ่งมีตำแหน่งหรือเงินเดือนประจำ ผู้ปฏิบัติงานในหน่วยงาน ของรัฐหรือในรัฐวิสาหกิจ ผู้บริหารท้องถิ่น รองผู้บริหารท้องถิ่น ผู้ช่วยผู้บริหารท้องถิ่น และ สมาชิกสภาท้องถิ่นขององค์กรปกครองส่วนท้องถิ่น เจ้าพนักงานตามกฎหมายว่าด้วยลักษณะ ปกครองท้องที่ หรือเจ้าพนักงานอื่นตามที่กฎหมายบัญญัติ และให้หมายความรวมถึงกรรมการ อนุกรรมการ ลูกจ้างของส่วนราชการ หน่วยงานของรัฐ หรือรัฐวิสาหกิจ และบุคคลหรือคณะบุคคล บรรดาซึ่งมีกฎหมายกำหนดให้ใช้อำนาจหรือได้รับมอบให้ใช้อำนาจทางปกครองที่จัดตั้งขึ้น ในระบบราชการ รัฐวิสาหกิจ หรือกิจการอื่นของรัฐด้วย แต่ไม่รวมถึงผู้ดำรงตำแหน่งทางการเมือง ตุลาการศาลรัฐธรรมนูญ ผู้ดำรงตำแหน่งในองค์กรอิสระ และคณะกรรมการ ป.ป.ช. มาตรา ๔๘ วรรคหนึ่ง บัญญัติว่า เมื่อความปรากฏต่อคณะกรรมการ ป.ป.ช. ไม่ว่าจะมีการกล่าวหาหรือไม่ ว่ามีการกระทำความผิดที่อยู่ในหน้าที่และอำนาจของคณะกรรมการ ป.ป.ช. ให้คณะกรรมการ ป.ป.ช. ดำเนินการตามหน้าที่และอำนาจโดยพลัน โดยในกรณีที่จำเป็นต้องมีการไต่สวน ต้องไต่สวน\n\nและมีความเห็นหรือวินิจฉัยให้แล้วเสร็จภายในกำหนดเวลาที่คณะกรรมการ ป.ป.ช. กำหนด ซึ่งต้องไม่เกินสองปีนับแต่วันเริ่มดำเนินการไต่สวน มาตรา ๔๑ บัญญัติว่า เมื่อคณะกรรมการ ป.ป.ช. ไต่สวนแล้วมีมติวินิจฉัยว่าเจ้าหน้าที่ของรัฐกระทำความผิดฐานทุจริตต่อหน้าที่ หรือ กระทำความผิดต่อตำแหน่งหน้าที่ราชการ หรือความผิดต่อตำแหน่งหน้าที่ในการยุติธรรม หรือ ความผิดที่เกี่ยวข้องกันให้ดำเนินการดังต่อไปนี้.. (๒) ถ้ามีมูลความผิดทางวินัย ให้คณะกรรมการ ป.ป.ช. ส่งรายงาน สำนวนการไต่สวน เอกสารหลักฐาน และคำวินิจฉัยไปยังผู้บังคับบัญชาหรือผู้มีอำนาจ แต่งตั้งถอดถอนภายในสามสิบวันเพื่อให้ดำเนินการทางวินัยต่อไป มาตรา ๔๘ วรรคหนึ่ง บัญญัติว่า เมื่อผู้บังคับบัญชาหรือผู้มีอำนาจแต่งตั้งถอดถอนผู้ถูกกล่าวหาได้รับสำนวนการไต่สวน ของคณะกรรมการ ป.ป.ช. ตามมาตรา ๑ แล้ว ให้ผู้บังคับบัญชาหรือผู้มีอำนาจแต่งตั้งถอดถอน ผู้ถูกกล่าวหาผู้นั้นพิจารณาโทษทางวินัยตามฐานความผิดที่คณะกรรมการ ป.ป.ช. ได้มีมติโดยไม่ต้อง แต่งตั้งคณะกรรมการสอบสวนวินัยอีก โดยในการพิจารณาโทษทางวินัยแก่ผู้ถูกกล่าวหา ให้ถือว่า สำนวนการไต่สวนของคณะกรรมการ ป.ป.ช. เป็นสำนวนการสอบสวนทางวินัยของคณะกรรมการ สอบสวนวินัยตามกฎหมาย ระเบียบ หรือข้อบังคับว่าด้วยการบริหารงานบุคคลของผู้ถูกกล่าวหานั้น แล้วแต่กรณี\n\nจากบทบัญญัติแห่งกฎหมายดังกล่าว การที่ผู้ถูกฟ้องคดีที่ ๑ จะใช้อำนาจในการ ไต่สวนและวินิจฉัยชี้มูลความผิดที่เกี่ยวข้องกันตามกฎหมายว่าด้วยการป้องกันและปราบปราม การทุจริตได้จะต้องเป็นกรณีที่เจ้าหน้าที่ของรัฐกระทำความผิดฐานทุจริตต่อหน้าที่ หรือกระทำ ความผิดต่อตำแหน่งหน้าที่ราชการ หรือความผิดต่อตำแหน่งหน้าที่ในการยุติธรรมเสียก่อน หากมีความผิดอื่นที่เกี่ยวข้องกับความผิดมูลฐานดังกล่าว ผู้ถูกฟ้องคดีที่ ๑ จึงจะมีอำนาจชี้มูล ความผิดทางวินัยอันเป็นความผิดที่เกี่ยวข้องกันได้ ข้อเท็จจริงปรากฏว่า เมื่อสำนักงานการ ดังกล่าว ตรวจเงินแผ่นดิน โดยสำนักตรวจสอบพิเศษ ภาค ๘ มีหนังสือ ที่ ตผ ๐๐๕๐ ชม/๐๐๑ ลงวันที่ ๙ มกราคม ๒๕๖๐ ถึงผู้ถูกฟ้องคดีที่ ๑ เรื่อง การตรวจสอบการใช้จ่ายเงินที่ได้รับอุดหนุน ของที่ทำการปกครองอำเภอพญาเม็งราย จังหวัดเชียงราย ผู้ถูกฟ้องคดีที่ ๑ ได้รับเรื่องกล่าวหา และมีมติในการประชุมครั้งที่ ๑๐๐๙ - ๘๐/๒๕๖๑ เมื่อวันที่ 4 กันยายน ๒๕๖๑ แต่งตั้งคณะกรรมการไต่สวนเพื่อดำเนินการไต่สวน ตามมาตรา ๕๑ แห่งพระราชบัญญัติ ประกอบรัฐธรรมนูญว่าด้วยการป้องกันและปราบปรามการทุจริต พ.ศ. ๒๕๖๑ คณะกรรมการไต่สวน ได้แจ้งข้อกล่าวหาให้ผู้ฟ้องคดีทราบ ผู้ฟ้องคดีรับทราบข้อกล่าวหาและเข้าใจข้อกล่าวหา ตลอดจน ได้ชี้แจงแก้ข้อกล่าวหา ซึ่งคณะกรรมการไต่สวนได้รวบรวมข้อเท็จจริงและพยานหลักฐาน ทั้งหมดแล้วเห็นว่ามีพยานหลักฐานเพียงพอที่จะสนับสนุนข้อกล่าวหา จึงได้จัดทำรายงานและ สำนวนไต่สวนพร้อมทั้งทำความเห็นเสนอต่อผู้ถูกฟ้องคดีที่ ๑ ผู้ถูกฟ้องคดีที่ ๑ พิจารณาแล้ว\n\nมีมติในการประชุมครั้งที่ ๑๐๕/๒๕๖๒ เมื่อวันที่ ๘ ตุลาคม ๒๕๖๒ ว่า การกระทำของผู้ฟ้องคดี มีมูลความผิดทางอาญา ตามมาตรา ๑๕๗ แห่งประมวลกฎหมายอาญา และความผิดทางวินัย อย่างร้ายแรง ฐานปฏิบัติหรือละเว้นการปฏิบัติหน้าที่ราชการโดยมิชอบ เพื่อให้ตนเองหรือผู้อื่น ได้ประโยชน์ที่มีควรได้ เป็นการทุจริตต่อหน้าที่ราชการ ฐานปฏิบัติหน้าที่ราชการโดยจงใจ ไม่ปฏิบัติตามกฎหมาย ระเบียบของทางราชการ มติคณะรัฐมนตรี หรือนโยบายของรัฐบาล อันเป็นเหตุให้เสียหายแก่ราชการอย่างร้ายแรง และฐานกระทำการอื่นใดอันได้ชื่อว่าเป็น ผู้ประพฤติชั่วอย่างร้ายแรง ตามมาตรา ๘๒ วรรคสาม มาตรา ๘๕ วรรคสอง และมาตรา ๔๘ วรรคสอง แห่งพระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๓๕ ประกอบมาตรา ๑๓๓ แห่งพระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๕๑ จึงเห็นได้ว่า ผู้ถูกฟ้องคดีที่ ๑ ไต่สวน ข้อเท็จจริงและมีมติชี้มูลความผิดทางวินัยผู้ฟ้องคดีในการกระทำความผิดฐานทุจริตต่อหน้าที่แล้ว ผู้ถูกฟ้องคดีที่ ๑ จึงมีอำนาจชี้มูลความผิดทางวินัยผู้ฟ้องคดีในฐานอื่นที่เกี่ยวข้องกันและส่งเรื่องไปยัง ผู้ถูกฟ้องคดีที่ 6 ซึ่งเป็นผู้บังคับบัญชาหรือผู้มีอำนาจแต่งตั้งถอดถอนผู้ฟ้องคดีเพื่อให้ดำเนินการ ทางวินัยได้ โดยไม่ต้องแต่งตั้งคณะกรรมการสอบสวนวินัยอีก และให้ถือว่าสำนวนการไต่สวนของ ผู้ถูกฟ้องคดีที่ ๑ เป็นสำนวนการสอบสวนทางวินัยของคณะกรรมการสอบสวนวินัย ตามมาตรา ๙๑ (๒) และมาตรา ๔๘ วรรคหนึ่ง แห่งพระราชบัญญัติประกอบรัฐธรรมนูญว่าด้วยการป้องกันและ ปราบปรามการทุจริต พ.ศ. ๒๕๖๑ ที่ผู้ฟ้องคดีอ้างว่า ผู้ถูกฟ้องคดีที่ ๑ มีอำนาจไต่สวนและชี้มูล ความผิดทางวินัยเฉพาะความผิดฐานทุจริตต่อหน้าที่ราชการเท่านั้น เมื่อผู้ฟ้องคดีไม่ได้กระทำ ความผิดฐานทุจริตต่อหน้าที่ ผู้ถูกฟ้องคดีที่ ๑ จึงไม่มีอำนาจไต่สวนและชี้มูลความผิดผู้ฟ้องคดี และไม่ผูกพันผู้ถูกฟ้องคดีที่ ๒ ให้ต้องถือรายงานการไต่สวนของผู้ถูกฟ้องคดีที่ ๑ เป็นรายงาน การสอบสวน โดยผู้ถูกฟ้องคดีที่ ๑ ต้องดำเนินการแต่งตั้งคณะกรรมการสอบสวนความผิดทางวินัย เพื่อสอบสวนข้อเท็จจริง นั้น จึงฟังไม่ขึ้น นอกจากนี้ เมื่อพิจารณามาตรา ๑๙๒ แห่งพระราชบัญญัติ ประกอบรัฐธรรมนูญดังกล่าว ซึ่งบัญญัติว่า การดำเนินการตรวจรับคำกล่าวหา แสวงหาข้อเท็จจริง และรวบรวมพยานหลักฐาน ไต่สวนและมีความเห็นหรือวินิจฉัย ตามพระราชบัญญัติประกอบ รัฐธรรมนูญว่าด้วยการป้องกันและปราบปรามการทุจริต พ.ศ. ๒๕๔๒ และที่แก้ไขเพิ่มเติม ในวันก่อนวันที่พระราชบัญญัติประกอบรัฐธรรมนูญนี้ใช้บังคับให้เป็นอันใช้ได้และให้ดำเนินการ ต่อไป โดยการดำเนินการดังกล่าวต้องดำเนินการให้แล้วเสร็จตามมาตรา ๔๘ กล่าวคือ ต้องไม่เกิน ๒๕๖๑ สองปีนับแต่วันเริ่มดำเนินการไต่สวน การที่ผู้ถูกฟ้องคดีที่ ๑ มีคำสั่งให้ไต่สวนเมื่อวันที่ 4 กันยายน จนถึงผู้ถูกฟ้องคดีที่ ๑ มีมติชี้มูลความผิดของผู้ฟ้องคดีในการประชุมครั้งที่ ๑๐๕/๒๕๖๒ เมื่อวันที่ ๘ ตุลาคม ๒๕๖๒ จึงยังไม่พ้นกำหนดระยะเวลาสองปี ตามมาตรา ๔๘ ประกอบมาตรา ๑๙๒ แห่งพระราชบัญญัติประกอบรัฐธรรมนูญว่าด้วยการป้องกันและปราบปรามการทุจริต พ.ศ. ๒๕๖๑\n\nที่ผู้ฟ้องคดีอ้างว่าผู้ถูกฟ้องคดีที่ ๑ ใช้เวลาในการดำเนินการไต่สวนประมาณสามปีเศษ ล่วงเลย ต่อมา เมื่อผู้ถูกฟ้องคดีที่ ระยะเวลาตามที่กฎหมายกำหนดไว้โดยไม่มีเหตุจำเป็น และผู้ถูกฟ้องคดีที่ ๑ ไม่ได้ขอขยาย ระยะเวลา อันเป็นการไม่ปฏิบัติตามพระราชบัญญัติประกอบรัฐธรรมนูญว่าด้วยการป้องกัน และปราบปรามการทุจริต พ.ศ. ๒๕๖๑ มาตรา ๔๘ ทำให้มติการชี้มูลความผิดของผู้ถูกฟ้องคดีที่ ๑ ไม่มีผลผูกพันต่อผู้ถูกฟ้องคดีที่ ๒ ที่จะดำเนินการลงโทษทางวินัยแก่ผู้ฟ้องคดี นั้น จึงฟังไม่ขึ้น ๒ ซึ่งเป็นผู้บังคับบัญชามีอำนาจบรรจุและแต่งตั้งผู้ฟ้องคดี ตามมาตรา ๕๗ (๔) แห่งพระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๕๑ ได้รับหนังสือสำนักงาน ป.ป.ช. ที่ ปช ๐๑.๐๑ (ชร/๑๔๔๒ ลงวันที่ 4 พฤศจิกายน ๒๕๖๒ ส่งรายงาน สำนวนการไต่สวน เอกสารหลักฐาน และคำวินิจฉัยแล้ว จึงส่งเรื่องให้ อ.ก.พ. กระทรวงมหาดไทย พิจารณาโทษทางวินัยผู้ฟ้องคดี ตามมาตรา ๔๗ วรรคสอง แห่งพระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๕๑ ซึ่ง อ.ก.พ. กระทรวงมหาดไทย ได้มีมติในการประชุมครั้งที่ ๒/๒๕๖๓ เมื่อวันที่ ๑๘ มิถุนายน ๒๕๖๓ ว่า พฤติการณ์ของผู้ฟ้องคดีเป็นความผิดวินัยอย่างร้ายแรงตามฐานความผิดที่ผู้ถูกฟ้องคดีที่ ๑ มีมติ ชี้มูลความผิด และตามมติคณะรัฐมนตรี เมื่อวันที่ ๒๑ ธันวาคม ๒๕๓๖ แจ้งตามหนังสือสำนัก เลขาธิการคณะรัฐมนตรี ที่ นร ๐๒๐๕/ว ๒๓๔ ลงวันที่ ๒๔ ธันวาคม ๒๕๓๖ ที่กำหนดให้ลงโทษ ผู้กระทำผิดวินัยฐานทุจริตต่อหน้าที่ราชการเป็นความผิดวินัยอย่างร้ายแรงควรลงโทษไล่ออก ที่ จากราชการ ให้ลงโทษไล่ผู้ฟ้องคดีออกจากราชการ ผู้ถูกฟ้องคดีที่ ๒ จึงมีคำสั่งกรมการปกครอง ๑๔๐๐/๒๕๖๓ ลงวันที่ 4 สิงหาคม ๒๕๖๓ ลงโทษไล่ผู้ฟ้องคดีออกจากราชการ ตั้งแต่วันที่ ๒๓ พฤษภาคม ๒๕๖๓ เป็นต้นไป ซึ่งเป็นวันที่ผู้ฟ้องคดีถูกพักราชการไว้ก่อน เห็นได้ว่า กระบวนการออกคำสั่งลงโทษไล่ผู้ฟ้องคดีออกจากราชการดังกล่าวเป็นการกระทำโดยถูกต้อง ตามรูปแบบขั้นตอน หรือวิธีการอันเป็นสาระสำคัญที่กำหนดไว้สำหรับการกระทำนั้นแล้ว\n\nคดีมีปัญหาต้องวินิจฉัยต่อไปว่า ผู้ฟ้องคดีกระทำผิดวินัยอย่างร้ายแรง ตามที่ ผู้ถูกฟ้องคดีที่๑ ชี้มูลหรือไม่\n\nพิเคราะห์แล้วเห็นว่า พระราชบัญญัติระเบียบบริหารราชการแผ่นดิน พ.ศ. ๒๕๓๔ มาตรา ๒๓ บัญญัติว่า ในอำเภอหนึ่ง นอกจากจะมีนายอำเภอเป็นผู้ปกครองบังคับบัญชาและ รับผิดชอบดังกล่าวในมาตรา ๒๒ ให้มีปลัดอำเภอและหัวหน้าส่วนราชการประจำอำเภอ บัญญัติว่า ซึ่งกระทรวง ทบวง กรมต่าง ๆ ส่งมาประจำให้ปฏิบัติหน้าที่เป็นผู้ช่วยเหลือนายอำเภอ และ มีอำนาจบังคับบัญชาข้าราชการฝ่ายบริหารส่วนภูมิภาคซึ่งสังกัดกระทรวง ทบวง กรมนั้น ในอำเภอนั้น พระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๓๕ มาตรา ๑๒ วรรคสาม การปฏิบัติหรือละเว้นการปฏิบัติหน้าที่ราชการโดยมิชอบ เพื่อให้ตนเองหรือผู้อื่น ได้ประโยชน์ที่มีควรได้ เป็นการทุจริตต่อหน้าที่ราชการและเป็นความผิดวินัยอย่างร้ายแรง\n\nมาตรา ๘๕ วรรคสอง บัญญัติว่า การปฏิบัติหน้าที่ราชการโดยจงใจไม่ปฏิบัติตามกฎหมาย ระเบียบของทางราชการ มติคณะรัฐมนตรี หรือนโยบายของรัฐบาล อันเป็นเหตุให้เสียหายแก่ ราชการอย่างร้ายแรง เป็นความผิดวินัยอย่างร้ายแรง และมาตรา ๔๘ วรรคสอง บัญญัติว่า การกระทำความผิดอาญาจนได้รับโทษจำคุก หรือโทษที่หนักกว่าจำคุกโดยคำพิพากษาถึงที่สุด ให้จำคุก หรือให้รับโทษที่หนักกว่าจำคุก เว้นแต่เป็นโทษสำหรับความผิดที่ได้กระทำโดยประมาท หรือความผิดลหุโทษ หรือกระทำการอื่นใดอันได้ชื่อว่าเป็นผู้ประพฤติชั่วอย่างร้ายแรงเป็นความผิด วินัยอย่างร้ายแรง พระราชบัญญัติระเบียบข้าราชการพลเรือน พ.ศ. ๒๕๕๑ มาตรา ๑๓๓ บัญญัติว่า ข้าราชการพลเรือนผู้ใดมีกรณีกระทำผิดวินัยหรือกรณีที่สมควรให้ออกจากราชการ อยู่ก่อนวันที่บทบัญญัติในลักษณะ ๔ ข้าราชการพลเรือนสามัญ และลักษณะ ๕ ข้าราชการ พลเรือนในพระองค์ แห่งพระราชบัญญัตินี้ใช้บังคับ ให้ผู้บังคับบัญชาตามพระราชบัญญัตินี้มีอำนาจ สั่งลงโทษผู้นั้นหรือสั่งให้ผู้นั้นออกจากราชการตามกฎหมายว่าด้วยระเบียบข้าราชการพลเรือน ที่ใช้อยู่ในขณะนั้น ส่วนการสอบสวน การพิจารณาและการดำเนินการเพื่อลงโทษหรือให้ออก จากราชการ ให้ดำเนินการตามพระราชบัญญัตินี้... ระเบียบสำนักนายกรัฐมนตรีว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ ข้อ ๗๑ กำหนดว่า คณะกรรมการตรวจรับพัสดุ มีหน้าที่ดังนี้... (๒) ตรวจรับพัสดุ ให้ถูกต้องครบถ้วนตามหลักฐานที่ตกลงกันไว้... ... ในกรณีที่เห็นว่าพัสดุที่ส่งมอบ มีรายละเอียด ไม่เป็นไปตามข้อกำหนดในสัญญาหรือข้อตกลง ให้รายงานหัวหน้าส่วนราชการผ่านหัวหน้าเจ้าหน้าที่ พัสดุ เพื่อทราบหรือสั่งการ แล้วแต่กรณี\n\nข้อเท็จจริงปรากฏว่า ขณะผู้ฟ้องคดีดำรงตำแหน่งปลัดอำเภอ (เจ้าพนักงาน ปกครอง 5) ที่ทำการปกครองอำเภอพญาเม็งราย จังหวัดเชียงราย มีหน้าที่เป็นผู้ช่วยเหลือ นายอำเภอ และมีอำนาจบังคับบัญชาข้าราชการฝ่ายบริหารส่วนภูมิภาคซึ่งสังกัดกระทรวง ทบวง กรมนั้น ในอำเภอนั้น ตามมาตรา ๖๓ แห่งพระราชบัญญัติระเบียบบริหารราชการแผ่นดิน พ.ศ. ๒๕๓๔ ที่ทำการปกครองอำเภอพญาเม็งรายได้รับการสนับสนุนเงินงบประมาณประจำปี พ.ศ. ๒๕๔๘ โดยนายอำเภอพญาเม็งรายได้แต่งตั้งให้ผู้ฟ้องคดีเป็นประธานกรรมการตรวจรับพัสดุ จำนวน ๒ โครงการ คือ โครงการจัดทำเวทีประชาคมหมู่บ้านเพื่อเสริมสร้างความเข้มแข็ง ให้ชุมชนอำเภอพญาเม็งราย จังหวัดเชียงราย และโครงการส่งเสริมการเกษตรแบบครบวงจร สำหรับโครงการจัดทำเวทีประชาคมหมู่บ้านเพื่อเสริมสร้างความเข้มแข็งให้ชุมชนอำเภอพญาเม็งราย จังหวัดเชียงราย ได้รับเงินงบประมาณและเงินอุดหนุนจากองค์การบริหารส่วนตำบลในอำเภอพญา เม็งราย ๕ อบต. เป็นเงิน ๙๐,๐๐๐ บาท โดยมีผู้ฟ้องคดีเป็นผู้เสนอโครงการซึ่งมีวัตถุประสงค์เพื่อให้ราษฎร มีส่วนร่วมในการป้องกันและเฝ้าระวังปัญหายาเสพติดในพื้นที่ ๕ ตำบล ๖๙ หมู่บ้าน ระหว่างวันที่ ๑๕ ธันวาคม ๒๕๔๗ ถึงวันที่ ๓๑ มกราคม ๒๕๔๘ ต่อมา ผู้ฟ้องคดีได้มีหนังสือ ไม่ระบุวันที่ เดือนมกราคม ๒๕๕๘ ขออนุมัติเปลี่ยนแปลงกำหนดการจัดทำเวทีประชาคมหมู่บ้านฯ โดยจัดประชุม เวทีประชาคมหมู่บ้านในวันที่ ๑๗ - ๑๙ มกราคม ๒๕๕๘ ณ หอประชุมพญาเม็งราย โครงการดังกล่าว มีการจัดซื้อจัดจ้างรวม ๓ รายการ คือ (๑) วัสดุแบบพิมพ์อบรมสร้างความเข้มแข็งให้ชุมชน และกระดาษถ่ายเอกสาร จำนวน ๑๐ รีม รีมละ ๑๐๐ บาท เป็นเงิน ๑,๐๐๐ บาท จากนางเจรุมาศ อนุสถิตวัฒนา ผู้ขาย (๒) จ้างเหมาจัดทำอาหาร อาหารว่าง และเครื่องดื่ม สำหรับผู้เข้าอบรม ในวันที่ ๑๗ ๑๙ มกราคม ๒๕๔๘ จำนวน ๕๙ หมู่บ้าน หมู่บ้านละ ๒๐ คน คนละ ๕๐ บาท เป็นเงิน ๕๔,๐๐๐ บาท จากนายสว่าง วรรณสอน ผู้รับจ้าง (๓) จ้างเหมาจัดสถานที่และเช่าเครื่องเสีย จัดทำเวทีประชาคม ในวันที่ ๑๗ ๑๙ มกราคม ๒๕๕๘ เป็นเงิน ๔,๐๐๐ บาท และจัดหาโต๊ะ เก้าอี้ สำหรับวิทยากรและผู้เข้าอบรม จำนวน ๕๐๐ ตัว พร้อมโต๊ะหมู่บูชา แอมป์พาวเวอร์ และ อุปกรณ์อื่นๆ เป็นเงิน ๒๑,๐๐๐ บาท รวมเป็นเงิน ๓๐,๐๐๐ บาท จากนายสุวนันท์ ไชยานันตา ผู้รับจ้าง รวมเป็นเงิน ๙๐,๐๐๐ บาท แต่จากรายงานการไต่สวนของผู้ถูกฟ้องคดีที่ ๑ ปรากฏว่า พยานบุคคลซึ่งเป็นผู้ปกครองท้องที่อำเภอพญาเม็งรายหลายรายต่างให้ถ้อยคำสอดคล้องกันว่า อำเภอพญาเม็งรายดำเนินการจัดประชุมประชาคม ณ หอประชุมหมู่บ้าน เพียงครึ่งวัน โดยไม่มี การจัดเลี้ยงอาหารกลางวันแก่ผู้เข้าอบรม และในวันที่ ๑๗ - ๑๙ มกราคม ๒๕๕๘ ไม่มีการจัด เวทีประชาคมหมู่บ้าน ณ หอประชุมอำเภอพญาเม็งราย แต่อย่างใด มีเพียงการประชุมประชาคม ของหมู่ที่ 5 ตำบลตาดควัน ในวันที่ ๑๗ ธันวาคม ๒๕๔๗ และการประชุมประชาคมของหมู่ที่ ๒ ตำบลแม่ต่ำ ในวันที่ ๓๐ ธันวาคม ๒๕๕๗ ตามบันทึกรายงานการตรวจ ลงวันที่ ๑๗ ธันวาคม ๒๕๕๗ และวันที่ ๓๐ ธันวาคม ๒๕๔๗ เท่านั้น ประกอบกับนายสุวนันท์ ไชยานันตา ผู้รับจ้างเหมา จัดสถานที่และเช่าเครื่องเสียงจัดทำเวทีประชาคม ให้ถ้อยคำว่า นายสุวนันท์เป็นอาสาสมัครรักษา ดินแดนอำเภอพญาเม็งรายอยู่ภายใต้การบังคับบัญชาโดยตรงของผู้ฟ้องคดี ผู้ฟ้องคดีได้สั่งการให้ นายสุวนันท์ลงลายมือชื่อเป็นผู้รับจ้างทั้งที่ทราบอยู่แล้วว่านายสุวนันท์ไม่มีอาชีพในการจัดสถานที่ หรือมีเครื่องเสียงให้เช่า แม้นายสุวนันท์จะได้จัดหาโต๊ะ เก้าอี้ ลำโพงขนาดใหญ่ ลำโพงขนาดเล็ก และไมโครโฟน เพื่อใช้จัดสถานที่ในวันที่ ๑๖ มกราคม ๒๕๕๘ จริง แต่นายสุวนันท์ไม่ทราบว่า ในวันที่ ๑๗ ๑๙ มกราคม ๒๕๕๘ มีการจัดประชุมหรือไม่ และไม่ทราบว่ามีการจัดหาเครื่องเสียง ชุดใหญ่พร้อมแอมป์พาวเวอร์หรือไม่ โดยนายสุวนันท์ไม่ได้รับค่าจ้างแต่อย่างใด อีกทั้ง ในการ เบิกจ่ายค่าจ้างเหมาจัดทำอาหาร อาหารว่าง และเครื่องดื่ม ก็ไม่ปรากฏรายละเอียดในหลักฐาน การเบิกจ่าย และนายสว่าง วรรณสอน ผู้รับจ้าง ก็ไม่มีอาชีพในการจัดทำอาหาร แต่เป็นอาสาสมัคร รักษาดินแดนอำเภอพญาเม็งรายซึ่งอยู่ภายใต้การบังคับบัญชาโดยตรงของผู้ฟ้องคดีเช่นเดียวกับ นายสุวนันท์ เห็นได้ว่า ผู้ฟ้องคดีซึ่งได้รับแต่งตั้งให้เป็นประธานกรรมการตรวจรับพัสดุ มีหน้าที่ ตรวจรับพัสดุให้ถูกต้องครบถ้วนตามหลักฐานที่ตกลงกันไว้ กรณีที่เห็นว่าพัสดุที่ส่งมอบมีรายละเอียดไม่เป็นไปตามข้อกำหนดในสัญญาหรือข้อตกลง ให้รายงานหัวหน้าส่วนราชการ ผ่านหัวหน้าเจ้าหน้าที่พัสดุเพื่อทราบหรือสั่งการ แล้วแต่กรณี ตามข้อ ๗๑ ของระเบียบสำนัก นายกรัฐมนตรีว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ แต่ผู้ฟ้องคดีกลับไม่ตรวจรับพัสดุตามโครงการจัดทำ เวทีประชาคมหมู่บ้านฯ ทั้งยังลงนามรับรองเป็นหลักฐานว่ามีการดำเนินการจัดซื้อจัดจ้างถูกต้อง ครบถ้วน เพื่อเสนอให้มีการเบิกจ่ายเงิน อันเป็นการไม่ปฏิบัติตามข้อ ๗๑ ของระเบียบสำนัก นายกรัฐมนตรีดังกล่าว\n\nส่วนโครงการส่งเสริมการเกษตรแบบครบวงจร ได้รับจัดสรรเงินงบประมาณ จำนวน ๓๘๔,๐๐๐ บาท มีการจัดซื้อวัสดุอุปกรณ์ส่งเสริมเกษตรธรรมชาติแบบครบวงจร จากนายธนกร ไชยวรรณ์ จำนวน 5 ครั้ง สำหรับหมู่ที่ ๓ ตำบลเม็งราย หมู่ที่ ๙ ตำบลไม้ยา หมู่ที่ ๙ ตำบลแม่ต่ำ หมู่ที่ ๑๓ ตำบลตาดควัน หมู่ที่ ๑๔ ตำบลไม้ยา และหมู่ที่ ๑๙ ตำบลแม่เปา ครั้งละ ๑๑ รายการ ประกอบด้วยเครื่องสกัดยาฆ่าแมลง เครื่องบดดิน ถังพ่นยา ถั่วฝักยาว ผักบุ้งจีน แตงกวา คะน้า กวางตุ้งใบ ผักชี ผักขึ้นฉ่าย และบวบเหลี่ยม ตามใบสั่งซื้อเลขที่ ๗/๒๕๔๘ ๑๒/๒๕๔๘ ๑๔ กุมภาพันธ์ ๒๕๕๘ แต่ปรากฏว่าชาวบ้านหมู่ที่ ๓ ตำบลเม็งราย ได้รับวัสดุไม่ครบ ทั้ง ลงวันที่ ๑๑ รายการ หมู่ที่ ๓ ตำบลตาดควัน ได้รับวัสดุไม่ครบทั้ง ๑๑ รายการ หมู่ที่ ๙ ตำบลไม้ยา ได้รับเฉพาะเครื่องสกัดยาฆ่าแมลงและเครื่องบดดิน หมู่ที่ 4 ตำบลแม่ต่ำ ได้รับเฉพาะเครื่องสกัด ยาฆ่าแมลงและเครื่องบดดิน หมู่ที่ ๑๔ ตำบลไม้ยา ได้รับเฉพาะเครื่องสกัดยาฆ่าแมลงและเครื่องบดดิน และหมู่ที่ ๑๙ ตำบลแม่เปา ได้รับเฉพาะเครื่องสกัดยาฆ่าแมลงและเครื่องบดดิน ชาวบ้านต่างได้รับ พัสดุไม่ตรงตามที่ขออนุมัติและได้รับพัสดุไม่ครบถ้วน แม้ผู้ฟ้องคดีจะไม่มีอำนาจหน้าที่ในการ แจกจ่ายพัสดุให้กับชาวบ้าน แต่ผู้ฟ้องคดีซึ่งเป็นประธานกรรมการตรวจรับพัสดุ มีหน้าที่ตรวจ รับพัสดุให้ถูกต้องครบถ้วนตามหลักฐานที่ตกลงกันไว้ กรณีที่เห็นว่าพัสดุที่ส่งมอบมีรายละเอียด ไม่เป็นไปตามข้อกำหนดในสัญญาหรือข้อตกลง ให้รายงานหัวหน้าส่วนราชการผ่านหัวหน้า เจ้าหน้าที่พัสดุเพื่อทราบหรือสั่งการ แล้วแต่กรณี ตามข้อ ๗๑ ของระเบียบสำนักนายกรัฐมนตรี ว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ กลับลงนามยืนยันว่ามีการตรวจรับพัสดุครบถ้วน เพื่อเสนออนุมัติ เบิกจ่ายเงิน ทั้งที่ผู้ฟ้องคดีไม่ได้ตรวจรับพัสดุตามโครงการส่งเสริมการเกษตรแบบครบวงจรจริง อันเป็นการไม่ปฏิบัติตามข้อ ๗๑ ของระเบียบสำนักนายกรัฐมนตรีดังกล่าว นอกจากนี้ ผู้ฟ้องคดี รับว่าทั้งโครงการจัดทำเวทีประชาคมหมู่บ้านฯ และโครงการส่งเสริมการเกษตรแบบครบวงจร นายบุญยัง เรือนกูล นายอำเภอพญาเม็งราย และนางวัลลภาภรณ์ เรือนปาง เสมียนตราอำเภอ พญาเม็งราย ได้รวบรวมเอกสารที่เกี่ยวข้องเสนอผู้ฟ้องคดีให้ลงนามในฐานะประธานกรรมการ ตรวจรับพัสดุ ผู้ฟ้องคดีได้ลงนามรับรองเป็นหลักฐานว่ามีการดำเนินการจัดซื้อจัดจ้างถูกต้อง ครบถ้วน เพื่อเสนอให้มีการเบิกจ่ายเงินงบประมาณจำนวน ๙๐,๐๐๐ บาท และจำนวน ๓๘๔,๐๐๐ บาท อันเป็นการเอื้อให้บุคคลอื่นได้รับผลประโยชน์จากงบประมาณดังกล่าว\n\nที่ผู้ฟ้องคดีอ้างว่า ผู้ฟ้องคดีไม่มีความเป็นอิสระในการบริหาร ต้องตกอยู่ภายใต้การสั่งการของ นายบุญยัง ผู้ฟ้องคดีไม่มีเจตนาทุจริต ไม่ปรากฏว่าผู้ฟ้องคดีมีทรัพย์สินเพิ่มมากขึ้นผิดปกติ หรือมีการได้ประโยชน์ในลักษณะทรัพย์สิน หรือประโยชน์อื่นใดจากการดำเนินโครงการ ผู้ฟ้องคดี ไม่ได้กระทำการทุจริตต่อหน้าที่ นั้น เห็นว่า เมื่อพิจารณาประวัติการรับราชการของผู้ฟ้องคดี ในช่วงก่อนที่ผู้ฟ้องคดีจะได้รับการแต่งตั้งให้ดำรงตำแหน่งปลัดอำเภอ ที่ทำการปกครองพญาเม็งราย ผู้ฟ้องคดีดำรงตำแหน่งปลัดอำเภอ (เจ้าพนักงานปกครอง ๓) ตั้งแต่วันที่ ๒๖ ธันวาคม ๒๕๓๗ และรับราชการตำแหน่งดังกล่าวมาแล้วในหลายท้องที่ของจังหวัดเชียงราย ผู้ฟ้องคดีย่อมเป็น ข้าราชการผู้มีความรู้และประสบการณ์ในการปฏิบัติหน้าที่ในตำแหน่งนั้นเป็นอย่างดี เมื่อผู้ฟ้องคดี ได้รับแต่งตั้งให้เป็นประธานกรรมการตรวจรับพัสดุ ผู้ฟ้องคดีจึงมีหน้าที่ต้องตรวจรับพัสดุให้ถูกต้อง ครบถ้วนตามข้อ ๗๑ ของระเบียบสำนักนายกรัฐมนตรีว่าด้วยการพัสดุ พ.ศ. ๒๕๓๕ แต่ผู้ฟ้องคดี กลับยอมลงชื่อในเอกสารการตรวจรับพัสดุและการจ้างตามที่ นายบุญยัง นำมาให้โดยมิได้ทักท้วง หรือเสนอความเห็นให้มีการทบทวนการกระทำที่ไม่ถูกต้องแต่อย่างใด แม้ไม่ปรากฏว่าผู้ฟ้องคดี ได้จะได้รับเงินจากการเบิกจ่ายงบประมาณดังกล่าวโดยตรง แต่การกระทำของผู้ฟ้องคดียังเป็น การเอื้อประโยชน์ให้กับบุคคลอื่นได้รับเงินหรือประโยชน์ที่มีควรได้ ข้อกล่าวอ้างของผู้ฟ้องคดี ฟังไม่ขึ้น จึงเห็นได้ว่า พฤติการณ์ของผู้ฟ้องคดีเป็นการปฏิบัติหรือละเว้นการปฏิบัติหน้าที่ราชการ โดยมิชอบ เพื่อให้ตนเองหรือผู้อื่นได้ประโยชน์ที่มีควรได้ เป็นการทุจริตต่อหน้าที่ราชการ ปฏิบัติหน้าที่ราชการโดยจงใจไม่ปฏิบัติตามกฎหมาย ระเบียบของทางราชการ มติคณะรัฐมนตรี หรือนโยบายของรัฐบาล อันเป็นเหตุให้เสียหายแก่ราชการอย่างร้ายแรง และกระทำการอื่นใด อันได้ชื่อว่าเป็นผู้ประพฤติชั่วอย่างร้ายแรง ซึ่งเป็นความผิดวินัยอย่างร้ายแรง ตามมาตรา ๘๒ เมื่อวันที่ ๘ วรรคสาม มาตรา ๘๕ วรรคสอง และมาตรา ๙๘ วรรคสอง แห่งพระราชบัญญัติระเบียบ ข้าราชการพลเรือน พ.ศ. ๒๕๓๕ ประกอบมาตรา ๑๓๓ แห่งพระราชบัญญัติระเบียบข้าราชการ พลเรือน พ.ศ. ๒๕๕๑ ตามที่ผู้ถูกฟ้องคดีที่ ๑ ชี้มูลความผิด ดังนั้น การที่ผู้ถูกฟ้องคดีที่ ๒ มีคำสั่งกรมการปกครอง ที่ ๑๔๐๐/๒๕๖๓ ลงวันที่ ๕ สิงหาคม ๒๕๖๓ ลงโทษไล่ผู้ฟ้องคดี ออกจากราชการ จึงชอบด้วยกฎหมายแล้ว และการที่ผู้ถูกฟ้องคดีที่ ๓ วินิจฉัยอุทธรณ์ของผู้ฟ้องคดี โดยอาศัยข้อเท็จจริงและข้อกฎหมายเดียวกันแล้วมีคำวินิจฉัยเรื่องดำที่ ๖๓๑๐๑๖๖ เรื่องแดง ที่ ๑๐๔๘๑๖๕ ลงวันที่ ๕ กรกฎาคม ๒๕๖๕ ให้ยกอุทธรณ์ของผู้ฟ้องคดี จึงชอบด้วยกฎหมายเช่นกัน\n\nในส่วนของคำขอท้ายคำฟ้องที่ผู้ฟ้องคดีมีคำขอให้เพิกถอนมติของผู้ถูกฟ้องคดีที่ ๑ ในการประชุมครั้งที่ ๕/๒๕๖๓ เมื่อวันที่ 4 มกราคม ๒๕๖๒ ที่ชี้มูลความผิดทางวินัย อย่างร้ายแรงผู้ฟ้องคดี นั้น เห็นว่า แม้มติของผู้ถูกฟ้องคดีที่ ๑ ในการประชุมครั้งที่ ๕/๒๕๖๓ 4 มกราคม ๒๕๖๓ ที่ชี้มูลความผิดทางวินัยอย่างร้ายแรงผู้ฟ้องคดีฐานทุจริตต่อหน้าที่ ราชการ จะมีผลผูกพันให้ผู้ถูกฟ้องคดีที่ ๒ ต้องมีคำสั่งตามมติดังกล่าว แต่ก็เป็นเพียงส่วนหนึ่งของการพิจารณาทางปกครองเพื่อออกคำสั่งทางปกครองเท่านั้น ยังไม่มีผลเป็นการสร้าง นิติสัมพันธ์ขึ้นระหว่างบุคคลในอันที่จะก่อ เปลี่ยนแปลง โอน สงวน ระงับ หรือมีผลกระทบ ต่อสถานภาพของสิทธิหรือหน้าที่ของบุคคล อันจะถือได้ว่าเป็นคำสั่งทางปกครองตามมาตรา ๕ แห่งพระราชบัญญัติวิธีปฏิบัติราชการทางปกครอง พ.ศ. ๒๕๓๙ แต่อย่างใด ผู้ฟ้องคดีจึงมิใช่ ผู้ได้รับความเดือดร้อนหรือเสียหายจากมติดังกล่าวที่จะมีสิทธิฟ้องขอให้ศาลมีคำพิพากษาหรือ คำสั่งเพิกถอนมติของผู้ถูกฟ้องคดีที่ ๑ ในการประชุมครั้งที่ ๕/๒๕๖๓ เมื่อวันที่ 4 มกราคม ๒๕๖๒ ที่ชี้มูลความผิดทางวินัยอย่างร้ายแรงผู้ฟ้องคดีได้\n\nกรณีที่ผู้ฟ้องคดีอ้างว่า มติคณะรัฐมนตรีแจ้งเวียนตามหนังสือสำนักเลขาธิการ คณะรัฐมนตรีที่ นร ๐๒๐๕/ว ๒๓๔ ลงวันที่ ๒๔ ธันวาคม ๒๕๓๖ ที่ระบุว่า การให้ลงโทษ ผู้กระทำผิดวินัยฐานทุจริตต่อหน้าที่ราชการควรลงโทษเป็นไล่ออกจากราชการการ เป็นเพียง การแนะนำเท่านั้น การลงโทษผู้ฟ้องคดีจึงเป็นดุลพินิจของผู้ถูกฟ้องคดีที่ ๒ และผู้ถูกฟ้องคดีที่ ๓ ย่อมมีอำนาจในการวินิจฉัยฐานความผิดทางวินัยแก่ผู้ฟ้องคดีเสียใหม่ได้ นั้น เห็นว่า แม้มติ คณะรัฐมนตรีดังกล่าวมิได้มีสถานะเป็นกฎหมายหรือกฎอันจะมีผลผูกพันให้ผู้ถูกฟ้องคดีที่ ๒ ต้องปฏิบัติตาม แต่มติดังกล่าวเป็นการกำหนดนโยบายหรือแนวปฏิบัติในการใช้ดุลพินิจ สั่งลงโทษผู้กระทำผิดในเรื่องที่กำหนดไว้ เพื่อให้การลงโทษเป็นไปด้วยความเหมาะสมกับกรณี ความผิด มีมาตรฐานเดียวกัน และเป็นไปในทางเดียวกัน ดังนั้น ในการพิจารณาสั่งลงโทษทางวินัย อย่างร้ายแรงฐานทุจริตต่อหน้าที่ราชการ ผู้บังคับบัญชาต้องนำมติดังกล่าวมาประกอบการพิจารณา ใช้ดุลพินิจด้วย ส่วนกระบวนการพิจารณาอุทธรณ์ของผู้ถูกฟ้องคดีที่ ๓ มิใช่การพิจารณาเฉพาะ รายงานการสอบสวนทางวินัยเท่านั้น แต่ยังให้โอกาสผู้ถูกกล่าวหาพิสูจน์หลักฐานอื่น รวมทั้ง มีอำนาจสอบสวนเพิ่มเติมได้ ผู้ถูกฟ้องคดีที่ ๓ จึงมีอำนาจพิจารณาอุทธรณ์ตามหลักเกณฑ์และ วิธีการที่กำหนดไว้ตามอำนาจหน้าที่และอาจพิจารณาแตกต่างจากคำสั่งลงโทษของผู้ถูกฟ้องคดีที่ ๒ ได้ ข้ออ้างของผู้ฟ้องคดีในประเด็นนี้จึงฟังไม่ขึ้น\n\nกรณีที่ผู้ฟ้องคดีอ้างว่า คดีหมายเลขดำที่ อท ๑๓๕, ที่ อท ๗๘, ๑๓๖/๒๕๖๒ และคดีหมายเลขแดง ๗๗/๒๕๖๓ ของศาลอาญาคดีทุจริตและประพฤติมิชอบภาค ๕ นั้น ผู้ฟ้องคดียังใช้สิทธิ ทางศาลในชั้นอุทธรณ์และฎีกาอยู่ คดีจึงยังไม่ถึงที่สุด และที่ผู้ถูกฟ้องคดีที่ ๑ ให้การเพิ่มเติมว่า พยานหลักฐานตามที่ได้นำสืบในชั้นศาลและในชั้นไต่สวนของผู้ถูกฟ้องคดีที่ ๑ เห็นได้ชัดว่า ผู้ฟ้องคดีได้กระทำความผิดตามที่ถูกกล่าวหา และการกระทำของผู้ฟ้องคดีจึงเป็นความผิดตาม คำพิพากษาของศาลอาญาคดีทุจริตและประพฤติมิชอบภาค ๕ นั้น เห็นว่า ในการพิจารณา พิพากษาคดีปกครองเกี่ยวกับการลงโทษทางวินัย ไม่มีกฎหมายบัญญัติบังคับไว้ให้ศาลปกครอง จำต้องถือข้อเท็จจริงตามที่ปรากฏในคำพิพากษาคดีส่วนอาญา ดังนั้น การพิจารณาคดีปกครอง\n\nเกี่ยวกับการลงโทษทางวินัยในกรณีสืบเนื่องจากผู้ถูกฟ้องคดีที่ ที่ ๑ ๑ ชี้มูลว่าผู้ฟ้องคดีกระทำการ อันมีมูลความผิดฐานทุจริตต่อหน้าที่ ซึ่งเป็นความผิดทางวินัยและทางอาญานั้น แม้การดำเนินการ ทางวินัยและการดำเนินคดีส่วนอาญาจะได้อาศัยสำนวนการสอบสวนของผู้ถูกฟ้องคดีที่ ๑ เป็นหลัก และคดีส่วนอาญาได้มีคำพิพากษาถึงที่สุดแล้วหรือไม่ก็ตาม ศาลก็หาจำต้องถือข้อเท็จจริง ตามที่ปรากฏในคำพิพากษาคดีส่วนอาญาแต่อย่างใดไม่ ข้ออ้างของผู้ฟ้องคดีในประเด็นนี้ จึงฟังไม่ขึ้นเช่นกัน\n\nพิพากษายกฟ้อง" - } -} \ No newline at end of file diff --git a/public/tmpl/T12_b2_IN.json b/public/tmpl/T12_b2_IN.json deleted file mode 100644 index 4bd98a5..0000000 --- a/public/tmpl/T12_b2_IN.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "info": { - "ticket_data": "{\"fid\":\"T12_b2_IN\",\"tid\":\"T12\",\"uid\":\"john\",\"uname\":\"John Doe\"}" - }, - "dc": { - "Interpleader": true, - "docname": "", - "docabbr": "(ต. ๑๒)", - "blackno": "คดีหมายเลขดำที่", - "redno": "คดีหมายเลขแดงที่", - "court": "ศาลปกครองนครราชสีมา", - "panels": [ - { - "pname": "นายวิรุฬห์ จินตนะกุล", - "pmname": "ตุลาการศาลปกครองกลาง", - "pm": "ตุลาการเจ้าของสำนวน" - }, - { - "pname": "นายสัมฤทธิ์ อ่อนคำ", - "pmname": "ตุลาการศาลปกครองสูงสุด" - } - ], - "pnameT":"", - "pmnameT":"", - "pmT":"", - "acc_name":"" - - }, - "cc": { - "blackyear": "๙๐/๒๕๖๙", - "redyear": "", - "curdate": "วันที่ เดือน สิงหาคม พุทธศักราช ๒๕๖๙", - "accuser": "นายธีรพล ร่มเริง", - "acc1": "ผู้ฟ้องคดี", - "accused": "รร", - "acc2": "ผู้ถูกฟ้องคดี", - "interpleader": "สมชาย ใจดี", - "acc3": "ผู้ร้องสอด", - "detail1": "ตุลาการเจ้าของสำนวนได้พิจารณาคำฟ้องและเอกสารประกอบคำฟ้องแล้วเห็นว่าคดีนี้เป็นคดีพิพาทซึ่งไม่อยู่ในอำนาจศาลปกครองสูงสุด ผู้ฟ้องเป็นผู้มีสิทธิฟ้องคดี คดีนี้มีกฎหมายกำหนดให้ต้องดำเนินการแก้ไขความเดือดร้อนหรือเสียหายก่อนฟ้องคดี และผู้ฟ้องคดีได้ดำเนินการครบถ้วน โดยเป็นการยื่นฟ้องคดีภายในระยะเวลาการฟ้องคดีตาม พรบ. จัดตั้งศาลปกครองและวิธีพิจารณาคดีปกครองฯ คดีนี้มีค่าธรรมเนียมที่ต้องชำระ\nโดยผู้ฟ้องคดีได้ชำระค่าธรรมเนียมครบถ้วนแล้ว รวมทั้งคดีนี้ไม่เป็นการฟ้องซ้อน การดำเนินกระบวนพิจารณาซ้ำ และการฟ้องซ้ำ\n\nจึงมีคำสั่งรับคำฟ้องไว้พิจารณาและสั่งให้ส่งสำเนาคำฟ้องพร้อมทั้งสำเนาพยานหลักฐานไปยังผู้ถูกฟ้องคดีเพื่อให้ผู้ถูกฟ้องคดีทำคำให้การยื่นต่อศาลภายใน ๓๐ วัน นับแต่วันที่ได้รับคำสั่ง" - } -} \ No newline at end of file diff --git a/public/tmpl/TF_no_warning_P2.json b/public/tmpl/TF_no_warning_P2.json deleted file mode 100755 index 152736c..0000000 --- a/public/tmpl/TF_no_warning_P2.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "info": { - "ticket_data": "{\"fid\":\"TF_no_warning_P2\",\"tid\":\"T_X\",\"uid\":\"john\",\"uname\":\"John Doe\"}" - }, - "dc": { - "Interpleader": false, - "CommandTo": true, - "SendTo": false, - "AdditionCommand" : true, - "Warning":false, - - "docname":"คำสั่งศาล", - "doctype":"(ต. ๔๗.๑)", - "blackno": "คดีหมายเลขดำที่", - "redno": "คดีหมายเลขแดงที่", - - "court": "ศาลปกครองนครราชสีมา", - "doctitle":"คำสั่งศาลถึง", - "pname":"สมหญิง", - - "courtname":"ศาลปกครองสุพรรณบุรี", - "courtno":"๓๓๓ หมู่ ๖ ตำบลสนามชัย อำเภอเมืองสุพรรณบุรี", - "courttam":"จังหวัดสุพรรณบุรี ๗๒๐๐๐", - "courttel":"กลุ่มออกหมาย โทรศัพท์ ๐๓๕ ๔๓๒ ๓๓๓ ต่อ ๑๐๐๑,๒๒๐๓", - "courttel2":"สอบถามติดตามคดี ขอคำปรึกษาแนะนำ โทรสายด่วน ๑๓๕๕", - "pname2":"ชื่อของ pname2" - }, - "cc": { - "blackyear": "๙๐/๒๕๖๙", - "redyear": "xxx", - "curdate": "วันที่ เดือน สิงหาคม พุทธศักราช ๒๕๖๙ ", - "commto" : "นายสมชาย จรดปลายเท้า", - "addrno" : "3/77 xxx yyy ทุ่งสองห้อง,หลักสี่, จังหวัดกรุงเทพฯ ", - "groupname":"ผู้อำนวยการโรงเรียน", - - "accuser": "นายธีรพล ร่มเริง", - "acc1": "ผู้ฟ้องคดี", - "accused": "รร", - "acc2": "ผู้ถูกฟ้องคดี", - "interpleader": "สมชาย ใจดี", - "acc3": "ผู้ร้องสอด", - "detail1": "ตุลาการเจ้าของสำนวนได้พิจารณาคำฟ้อง และเอกสารประกอบคำฟ้องแล้ว เห็นว่าคดีนี้เป็นคดีพิพาทซึ่งไม่อยู่ในอำนาจศาลปกครองสูงสุด\n\nผู้ฟ้องเป็นผู้มีสิทธิฟ้องคดี คดีนี้มีกฎหมายกำหนดให้ต้องดำเนินการแก้ไข ความเดือดร้อนหรือเสียหายก่อนฟ้องคดี ", - "sendto" : "นายสมศักดิ์ ฟักมี", - - "warning": "หากผู้ถูกฟ้องคดีไม่ปฏิบัติตามคำสั่งศาลภายในระยะเวลาที่กำหนด ศาลจะถือว่า ผู้ถูกฟ้องคดียอมรับข้อเท็จจริงตามข้อหาของผู้ฟ้องคดี", - "detail2":"คำสั่งเพิ่มเติมคือ bla bla", - "remarkhead":"หัวของ Remark", - "remark":"This is remark Text bla bla " - } -} \ No newline at end of file diff --git a/public/tmpl/TS_P2_IN.json b/public/tmpl/TS_P2_IN.json deleted file mode 100755 index 479478a..0000000 --- a/public/tmpl/TS_P2_IN.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "info": { - "ticket_data": "{\"fid\":\"TS_P2_IN\",\"tid\":\"T_X\",\"uid\":\"john\",\"uname\":\"John Doe\"}" - }, - "dc": { - "Interpleader": true, - "CommandTo": true, - "SendTo": false, - "AdditionCommand" : true, - "Warning":true, - - "docname":"คำสั่งเรียกพยานบุคคล", - "doctype":"(ต. ๖)", - "blackno": "คดีหมายเลขดำที่", - "redno": "คดีหมายเลขแดงที่", - - "court": "ศาลปกครองนครราชสีมา", - "doctitle":"คำสั่งถึง", - "pname":"สมหญิง", - - "courtname":"ศาลปกครองสุพรรณบุรี", - "courtno":"๓๓๓ หมู่ ๖ ตำบลสนามชัย อำเภอเมืองสุพรรณบุรี", - "courttam":"จังหวัดสุพรรณบุรี ๗๒๐๐๐", - "courttel":"กลุ่มออกหมาย โทรศัพท์ ๐๓๕ ๔๓๒ ๓๓๓ ต่อ ๑๐๐๑,๒๒๐๓", - "courttel2":"สอบถามติดตามคดี ขอคำปรึกษาแนะนำ โทรสายด่วน ๑๓๕๕", - - "pname2":"ชื่อของ pname2", - "remarkhead":"หัวของ Remark" - }, - "cc": { - "blackyear": "๙๐/๒๕๖๙", - "redyear": "xxx", - "curdate": "วันที่ เดือน สิงหาคม พุทธศักราช ๒๕๖๙ ", - "commto" : "นายสมชาย จรดปลายเท้า", - "addrno" : "3/77 xxx yyy ทุ่งสองห้อง,หลักสี่, จังหวัดกรุงเทพฯ ", - "groupname":"ผู้อำนวยการโรงเรียน", - - "accuser": "นายธีรพล ร่มเริง", - "acc1": "ผู้ฟ้องคดี", - "accused": "รร", - "acc2": "ผู้ถูกฟ้องคดี", - "interpleader": "สมชาย ใจดี", - "acc3": "ผู้ร้องสอด", - "detail1": "ตุลาการเจ้าของสำนวนได้พิจารณาคำฟ้อง และเอกสารประกอบคำฟ้องแล้ว เห็นว่าคดีนี้เป็นคดีพิพาทซึ่งไม่อยู่ในอำนาจศาลปกครองสูงสุด\n\nผู้ฟ้องเป็นผู้มีสิทธิฟ้องคดี คดีนี้มีกฎหมายกำหนดให้ต้องดำเนินการแก้ไข ความเดือดร้อนหรือเสียหายก่อนฟ้องคดี ", - "sendto" : "นายสมศักดิ์ ฟักมี", - - "warning": "หากผู้ถูกฟ้องคดีไม่ปฏิบัติตามคำสั่งศาลภายในระยะเวลาที่กำหนด ศาลจะถือว่า ผู้ถูกฟ้องคดียอมรับข้อเท็จจริงตามข้อหาของผู้ฟ้องคดี", - "detail2":"คำสั่งเพิ่มเติมคือ bla bla", - - "remark":"This is remark Text bla bla " - } -} \ No newline at end of file diff --git a/public/tmpl/T_no_warning.json b/public/tmpl/T_no_warning.json deleted file mode 100755 index 7697123..0000000 --- a/public/tmpl/T_no_warning.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "info": { - "ticket_data": "{\"fid\":\"T_no_warning\",\"tid\":\"T_X\",\"uid\":\"john\",\"uname\":\"John Doe\"}" - }, - "dc": { - "Interpleader": false, - "CommandTo": true, - "SendTo": true, - "AdditionCommand" : false, - "Warning":false, - - "docname":"คำสั่งเรียก\nให้ทำคำให้การเพิ่มเติม", - "doctype":"(ต. ๓)", - "blackno": "คดีหมายเลขดำที่", - "redno": "คดีหมายเลขแดงที่", - - "court": "ศาลปกครองนครราชสีมา", - "doctitle":"คำสั่งถึง", - "pname":"สมหญิง", - - "courtname":"ศาลปกครองสุพรรณบุรี", - "courtno":"๓๓๓ หมู่ ๖ ตำบลสนามชัย อำเภอเมืองสุพรรณบุรี", - "courttam":"จังหวัดสุพรรณบุรี ๗๒๐๐๐", - "courttel":"กลุ่มออกหมาย โทรศัพท์ ๐๓๕ ๔๓๒ ๓๓๓ ต่อ ๑๐๐๑,๒๒๐๓", - "courttel2":"สอบถามติดตามคดี ขอคำปรึกษาแนะนำ โทรสายด่วน ๑๓๕๕", - "pname2":"ชื่อของ pname2" - }, - "cc": { - "docname":"คำสั่งเรียกพยานบุคคล", - "blackyear": "๙๐/๒๕๖๙", - "redyear": "xxx", - "curdate": "วันที่ เดือน สิงหาคม พุทธศักราช ๒๕๖๙ ", - "commto" : "นายสมชาย จรดปลายเท้า", - "addrno" : "3/77 xxx yyy ทุ่งสองห้อง,หลักสี่, จังหวัดกรุงเทพฯ ", - "groupname":"ผู้อำนวยการโรงเรียน", - - "accuser": "นายธีรพล ร่มเริง", - "acc1": "ผู้ฟ้องคดี", - "accused": "รร", - "acc2": "ผู้ถูกฟ้องคดี", - "interpleader": "สมชาย ใจดี", - "acc3": "ผู้ร้องสอด", - "detail1": "ตุลาการเจ้าของสำนวนได้พิจารณาคำฟ้อง และเอกสารประกอบคำฟ้องแล้ว \nเห็นว่าคดีนี้เป็นคดีพิพาทซึ่งไม่อยู่ในอำนาจศาลปกครองสูงสุด\n\nผู้ฟ้องเป็นผู้มีสิทธิฟ้องคดี คดีนี้มีกฎหมายกำหนดให้ต้องดำเนินการแก้ไข ความเดือดร้อนหรือเสียหายก่อนฟ้องคดี ", - "sendto" : "นายสมศักดิ์ ฟักมี", - - "warning": "หากผู้ถูกฟ้องคดีไม่ปฏิบัติตามคำสั่งศาลภายในระยะเวลาที่กำหนด ศาลจะถือว่า ผู้ถูกฟ้องคดียอมรับข้อเท็จจริงตามข้อหาของผู้ฟ้องคดี", - "detail2":"คำสั่งเพิ่มเติมคือ bla bla", - "remarkhead":"หัวของ Remark", - "remark":"This is remark Text bla bla " - } -} \ No newline at end of file diff --git a/tests/lib/document-versioning.test.ts b/tests/lib/document-versioning.test.ts index da18b93..635fb84 100644 --- a/tests/lib/document-versioning.test.ts +++ b/tests/lib/document-versioning.test.ts @@ -6,6 +6,7 @@ import { docverError, isDocxBytes, isPdfBytes, + parseDocId, parseDownloadFormat, parseFieldsJson, parseVersionIdParam, @@ -22,6 +23,28 @@ function ok(result: { ok: boolean } & T): asserts result is { ok: true } & T expect(result.ok).toBe(true); } +describe("parseDocId", () => { + it("รับ GUID / รหัส e-Court / charset ที่อนุญาตทั้งหมด และ trim รอบ ๆ", () => { + for (const raw of [ + "123e4567-e89b-12d3-a456-426614174000", + "ecourt:123", + "DOC_2026.10", + " doc1 ", + "a".repeat(200), + ]) { + const result = parseDocId(raw); + ok(result); + expect(result.docId).toBe(raw.trim()); + } + }); + + it("ตกเมื่อว่าง / เกิน 200 / มีช่องว่างกลาง / ไทย / slash / '..'", () => { + for (const raw of ["", " ", "a".repeat(201), "doc 1", "เอกสาร1", "a/b", "..", "a\\b"]) { + expect(parseDocId(raw).ok).toBe(false); + } + }); +}); + describe("parseVersionIdParam", () => { it("รับ UUID ทั้งพิมพ์ใหญ่/เล็ก (normalize ลงตัวพิมพ์เล็ก) + trim", () => { const result = parseVersionIdParam(" 6F9619FF-8B86-D011-B42D-00C04FC964FF "); @@ -89,11 +112,11 @@ describe("parseFieldsJson", () => { }); describe("parseVersionUploadForm", () => { - // identity (userId/userName) กับ templateId ถอดออกจาก form แล้ว — route แกะจาก - // Bearer token ผ่าน lib/docver-token.ts (เคสของ token อยู่ docver-token.test.ts) function baseForm(): FormData { const form = new FormData(); form.append("file", new File([Buffer.from("PK\x03\x04")], "draft.docx")); + form.append("userId", "u0001"); + form.append("userName", "สมชาย ใจดี"); return form; } @@ -101,13 +124,17 @@ describe("parseVersionUploadForm", () => { const form = baseForm(); form.append("clientVersionId", " 11111111-1111-4111-8111-111111111111 "); form.append("message", " "); + form.append("templateId", "tmpl-main"); form.append("fields", '{"blackno":"1"}'); form.append("fileName", "C:\\Users\\judge\\คำสั่งเรียก.docx"); const result = parseVersionUploadForm(form); ok(result); expect(result.form).toEqual({ + userId: "u0001", + userName: "สมชาย ใจดี", clientVersionId: "11111111-1111-4111-8111-111111111111", message: null, + templateId: "tmpl-main", fileName: "คำสั่งเรียก.docx", fields: { blackno: "1" }, }); @@ -121,22 +148,22 @@ describe("parseVersionUploadForm", () => { if (!result.ok) expect(result.error).toContain("oops"); }); - it("ส่ง userId/userName/templateId ใน form = 400 ไม่รู้จัก field (identity มาจาก token)", () => { - for (const legacy of ["userId", "userName", "templateId"]) { - const form = baseForm(); - form.append(legacy, "x"); - const result = parseVersionUploadForm(form); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toContain(legacy); - } + it("ตกเมื่อขาด userId หรือ userName (stand-alone บอกผู้อัปโหลดเอง)", () => { + const noUser = baseForm(); + noUser.delete("userId"); + expect(parseVersionUploadForm(noUser).ok).toBe(false); + const noName = baseForm(); + noName.delete("userName"); + expect(parseVersionUploadForm(noName).ok).toBe(false); }); it("ตกเมื่อ text field ถูกส่งมาเป็นไฟล์", () => { const form = baseForm(); - form.append("message", new File(["x"], "m.txt")); + form.delete("userId"); + form.append("userId", new File(["x"], "u.txt")); const result = parseVersionUploadForm(form); expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toContain("message"); + if (!result.ok) expect(result.error).toContain("userId"); }); it(`message ยาว ${DOCVER_MESSAGE_MAX} ผ่าน แต่เกิน = ตก`, () => { diff --git a/tests/lib/docver-token.test.ts b/tests/lib/docver-token.test.ts deleted file mode 100644 index 6b2a5ab..0000000 --- a/tests/lib/docver-token.test.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { createHmac } from "node:crypto"; -import { afterEach, describe, expect, it } from "vitest"; - -import { - issueDocverToken, - parseTokenRequestBody, - requireDocverApiKey, - requireDocverBearer, - verifyDocverToken, - type DocverTokenClaims, -} from "@/lib/docver-token"; - -// docver token (PLAN-DOCVER.md §auth) — validation/auth ล้วนของ /api/documents/*: -// ไฟล์นี้ตั้งใจไม่แตะ module ใดที่ลาก @/db หรือ minio มาด้วย (pattern เดียวกับ -// external-api-auth.test.ts — vitest ไม่โหลด .env จึงต้องรันได้โดยไม่ต้องตั้ง -// DATABASE_URL/MINIO_*) - -const SECRET = "test-secret-docver"; -const CLAIMS: DocverTokenClaims = { - userid: "123", - username: "Sorawit Bholsithi", - fid: "3", - template: "T12", -}; - -function ok(result: { ok: boolean } & T): asserts result is { ok: true } & T { - expect(result.ok).toBe(true); -} - -// --- env save/restore — require* อ่าน env ตอนเรียก ไม่ใช่ตอน module load -const HAD_API_KEY = Object.prototype.hasOwnProperty.call(process.env, "DOCVER_API_KEY"); -const ORIG_API_KEY = process.env.DOCVER_API_KEY; -const HAD_SECRET = Object.prototype.hasOwnProperty.call(process.env, "DOCVER_TOKEN_SECRET"); -const ORIG_SECRET = process.env.DOCVER_TOKEN_SECRET; - -afterEach(() => { - if (HAD_API_KEY) process.env.DOCVER_API_KEY = ORIG_API_KEY; - else delete process.env.DOCVER_API_KEY; - if (HAD_SECRET) process.env.DOCVER_TOKEN_SECRET = ORIG_SECRET; - else delete process.env.DOCVER_TOKEN_SECRET; -}); - -function reqWithKey(key?: string): Request { - return new Request("http://localhost/docedit/api/documents/token", { - headers: key === undefined ? {} : { "x-api-key": key }, - }); -} - -function reqWithBearer(authorization: string | null): Request { - return new Request("http://localhost/docedit/api/documents/doc1/versions", { - headers: authorization === null ? {} : { authorization }, - }); -} - -async function bodyOf(res: Response): Promise<{ error: string }> { - expect(res.headers.get("content-type")).toBe("application/json; charset=utf-8"); - return (await res.json()) as { error: string }; -} - -/** เซ็น payload ใด ๆ ด้วย SECRET ผ่านมือ — ใช้ประกอบเคส claims ผิดรูปที่ issueDocverToken สร้างเองไม่ได้ */ -function signPayload(payload: Record): string { - const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" }), "utf8").toString( - "base64url", - ); - const body = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url"); - const signature = createHmac("sha256", SECRET) - .update(`${header}.${body}`) - .digest("base64url"); - return `${header}.${body}.${signature}`; -} - -describe("issueDocverToken / verifyDocverToken", () => { - it("issue → verify ได้ claims ครบทุกตัว (userid/username/fid/template)", () => { - const verified = verifyDocverToken(issueDocverToken(CLAIMS, SECRET, 60), SECRET); - ok(verified); - expect(verified.claims).toEqual(CLAIMS); - }); - - it("template เป็นทางเลือก (null ได้) แต่ fid บังคับ — token ไม่มี fid = ไม่ผ่าน", () => { - const verified = verifyDocverToken( - issueDocverToken({ userid: "u1", username: "ชื่อ", fid: "7", template: null }, SECRET, 60), - SECRET, - ); - ok(verified); - expect(verified.claims).toEqual({ userid: "u1", username: "ชื่อ", fid: "7", template: null }); - const noFid = signPayload({ - userid: "u1", - username: "n", - template: "T1", - iat: 0, - exp: Math.floor(Date.now() / 1000) + 60, - }); - expect(verifyDocverToken(noFid, SECRET).ok).toBe(false); - }); - - it("แก้ payload แล้วใช้ signature เดิม (tampered) หรือ secret ผิด = ไม่ผ่าน", () => { - const token = issueDocverToken(CLAIMS, SECRET, 60); - const [header, , signature] = token.split("."); - const tamperedPayload = Buffer.from( - JSON.stringify({ ...CLAIMS, userid: "999" }), - "utf8", - ).toString("base64url"); - expect(verifyDocverToken(`${header}.${tamperedPayload}.${signature}`, SECRET).ok).toBe(false); - expect(verifyDocverToken(token, "wrong-secret").ok).toBe(false); - }); - - it("header alg อื่น (none / HS512) = ไม่ผ่าน แม้จะเซ็นด้วย secret ถูก (กัน alg-confusion)", () => { - const body = Buffer.from( - JSON.stringify({ ...CLAIMS, iat: 0, exp: Math.floor(Date.now() / 1000) + 60 }), - "utf8", - ).toString("base64url"); - for (const alg of ["none", "HS512"]) { - const header = Buffer.from(JSON.stringify({ alg, typ: "JWT" }), "utf8").toString("base64url"); - const signature = createHmac("sha256", SECRET) - .update(`${header}.${body}`) - .digest("base64url"); - expect(verifyDocverToken(`${header}.${body}.${signature}`, SECRET).ok).toBe(false); - } - }); - - it("token หมดอายุ (ttl ติดลบ) หรือไม่มี exp = ไม่ผ่าน", () => { - expect(verifyDocverToken(issueDocverToken(CLAIMS, SECRET, -10), SECRET).ok).toBe(false); - const noExp = signPayload({ ...CLAIMS, iat: Math.floor(Date.now() / 1000) }); - expect(verifyDocverToken(noExp, SECRET).ok).toBe(false); - }); - - it("claims ไม่ครบรูปร่าง (แม้เซ็นถูก) = ไม่ผ่าน — กัน token เซ็นเองหลุดไปใช้", () => { - const now = Math.floor(Date.now() / 1000) + 60; - for (const bad of [ - { username: "n", fid: "3", template: null, iat: 0, exp: now }, // ไม่มี userid - { userid: "u", fid: "3", template: null, iat: 0, exp: now }, // ไม่มี username - { userid: "", username: "n", fid: "3", template: null, iat: 0, exp: now }, // userid ว่าง - { userid: "u", username: "n", fid: 3, template: null, iat: 0, exp: now }, // fid เป็น number - { userid: "u", username: "n", fid: "", template: null, iat: 0, exp: now }, // fid ว่าง - { userid: "u", username: "n", template: null, iat: 0, exp: now }, // ไม่มี fid (บังคับแล้ว) - { userid: "u", username: "n", fid: null, template: null, iat: 0, exp: now }, // fid null - ]) { - expect(verifyDocverToken(signPayload(bad), SECRET).ok).toBe(false); - } - }); - - it("รูป token ผิด (ไม่ใช่ 3 segment / segment เป็น base64 ปกติไม่ใช่ base64url) = ไม่ผ่าน", () => { - expect(verifyDocverToken("not-a-jwt", SECRET).ok).toBe(false); - expect(verifyDocverToken("a.b.c.d", SECRET).ok).toBe(false); - // base64 ปกติมี + / ซึ่ง base64url decode แล้วได้ byte ผิด → header/payload พัง - expect(verifyDocverToken("???.???.???", SECRET).ok).toBe(false); - }); -}); - -describe("requireDocverApiKey", () => { - it("env ไม่ตั้ง / ว่าง / ช่องว่างล้วน = 401 fail closed ทุกกรณี", async () => { - for (const value of [undefined, "", " "]) { - if (value === undefined) delete process.env.DOCVER_API_KEY; - else process.env.DOCVER_API_KEY = value; - const res = requireDocverApiKey(reqWithKey("test-key")); - expect(res?.status).toBe(401); - expect(await bodyOf(res!)).toEqual({ error: "ระบบยังไม่ได้ตั้งค่า API key" }); - } - }); - - it("ไม่ใส่ header กับ key ผิด ต้องได้ 401 ข้อความเดียวกันเป๊ะ (กัน enumerate)", async () => { - process.env.DOCVER_API_KEY = "test-key"; - const missing = requireDocverApiKey(reqWithKey()); - const wrong = requireDocverApiKey(reqWithKey("not-the-key")); - expect(missing?.status).toBe(401); - expect(wrong?.status).toBe(401); - expect(await bodyOf(missing!)).toEqual(await bodyOf(wrong!)); - }); - - it("key ตรง = ผ่าน (คืน null) · env มี whitespace รอบ ๆ ก็ trim ให้เทียบได้", () => { - process.env.DOCVER_API_KEY = "test-key"; - expect(requireDocverApiKey(reqWithKey("test-key"))).toBeNull(); - process.env.DOCVER_API_KEY = " test-key "; - expect(requireDocverApiKey(reqWithKey("test-key"))).toBeNull(); - }); -}); - -describe("requireDocverBearer", () => { - it("ทุกกรณีพ้นสภาพ (ไม่มี header / ไม่ใช่ Bearer / token เน่า / หมดอายุ / secret ไม่ตั้ง) = 401 ข้อความเดียวกัน", async () => { - const deniedBodies = new Set(); - const cases: { label: string; secret: string | undefined; authorization: string | null }[] = [ - { label: "ไม่มี Authorization", secret: SECRET, authorization: null }, - { label: "scheme อื่น (Basic)", secret: SECRET, authorization: "Basic dXNlcjpwYXNz" }, - { label: "Bearer ว่างเปล่า", secret: SECRET, authorization: "Bearer " }, - { label: "token เน่า", secret: SECRET, authorization: "Bearer garbage.token.here" }, - { - label: "token หมดอายุ", - secret: SECRET, - authorization: `Bearer ${issueDocverToken(CLAIMS, SECRET, -10)}`, - }, - { - label: "เซ็นด้วย secret ผิด", - secret: SECRET, - authorization: `Bearer ${issueDocverToken(CLAIMS, "other-secret", 60)}`, - }, - { label: "secret ไม่ตั้ง (fail closed)", secret: undefined, authorization: null }, - { - label: "secret ไม่ตั้งแม้จะมี token ถูก", - secret: undefined, - authorization: `Bearer ${issueDocverToken(CLAIMS, SECRET, 60)}`, - }, - ]; - for (const testCase of cases) { - if (testCase.secret === undefined) delete process.env.DOCVER_TOKEN_SECRET; - else process.env.DOCVER_TOKEN_SECRET = testCase.secret; - const auth = requireDocverBearer(reqWithBearer(testCase.authorization)); - expect(auth.ok, testCase.label).toBe(false); - if (!auth.ok) { - expect(auth.response.status, testCase.label).toBe(401); - deniedBodies.add(JSON.stringify(await bodyOf(auth.response))); - } - } - expect(deniedBodies.size).toBe(1); // ข้อความ 401 เดียวกันหมด — ไม่ใช้ endpoint เป็น oracle - }); - - it("Bearer ที่ verify ผ่าน = ได้ claims · คำว่า bearer พิมพ์เล็กก็รับ (case-insensitive)", () => { - process.env.DOCVER_TOKEN_SECRET = SECRET; - const token = issueDocverToken(CLAIMS, SECRET, 60); - const auth = requireDocverBearer(reqWithBearer(`Bearer ${token}`)); - ok(auth); - expect(auth.claims).toEqual(CLAIMS); - const lower = requireDocverBearer(reqWithBearer(`bearer ${token}`)); - ok(lower); - expect(lower.claims).toEqual(CLAIMS); - }); - - it("env มี whitespace รอบ secret ก็ trim แล้ว verify ได้", () => { - process.env.DOCVER_TOKEN_SECRET = ` ${SECRET} `; - const auth = requireDocverBearer( - reqWithBearer(`Bearer ${issueDocverToken(CLAIMS, SECRET, 60)}`), - ); - ok(auth); - expect(auth.claims).toEqual(CLAIMS); - }); -}); - -describe("parseTokenRequestBody", () => { - it("ครบทุก field = ได้ claims (trim ค่า) · template ไม่ส่ง = null (fid บังคับ)", () => { - const full = parseTokenRequestBody({ - userid: " 123 ", - username: " Sorawit Bholsithi ", - fid: " 3 ", - template: " T12 ", - }); - ok(full); - expect(full.claims).toEqual({ userid: "123", username: "Sorawit Bholsithi", fid: "3", template: "T12" }); - - const minimal = parseTokenRequestBody({ userid: "123", username: "Sorawit", fid: "3" }); - ok(minimal); - expect(minimal.claims).toEqual({ userid: "123", username: "Sorawit", fid: "3", template: null }); - }); - - it("template ส่งมาเป็นช่องว่าง = null (ถือว่าไม่ส่ง)", () => { - const result = parseTokenRequestBody({ userid: "123", username: "n", fid: "3", template: " " }); - ok(result); - expect(result.claims.template).toBeNull(); - }); - - it("ไม่ใช่ object (array / string / null) = 400", () => { - for (const raw of [[], "text", null]) { - const result = parseTokenRequestBody(raw); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("body ต้องเป็น JSON object"); - } - }); - - it("key อื่นนอก allowlist = 400 ไม่รู้จัก field (กัน typo เงียบ ๆ)", () => { - const result = parseTokenRequestBody({ userid: "1", username: "n", oops: 1 }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toContain("oops"); - }); - - it("ขาด userid หรือ username / ส่งมาไม่ใช่ string = 400 ข้อความเฉพาะ field", () => { - for (const raw of [ - { username: "n" }, - { userid: " ", username: "n" }, - { userid: 123, username: "n" }, - { userid: "1" }, - { userid: "1", username: "" }, - ]) { - const result = parseTokenRequestBody(raw); - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toMatch(/ต้องระบุ (userid|username)/); - } - } - }); - - it("fid ขาด/null/ว่าง/ไม่ใช่ string (number) หรือ template ไม่ใช่ string = 400", () => { - const missing = parseTokenRequestBody({ userid: "1", username: "n" }); - expect(missing.ok).toBe(false); - if (!missing.ok) expect(missing.error).toContain("fid"); - for (const raw of [ - { userid: "1", username: "n", fid: null }, - { userid: "1", username: "n", fid: 3 }, - { userid: "1", username: "n", fid: "" }, - { userid: "1", username: "n", fid: " " }, - { userid: "1", username: "n", template: 12 }, - ]) { - expect(parseTokenRequestBody(raw).ok).toBe(false); - } - }); - - it("ความยาวเกิน cap: userid > 100 / username > 200 / fid > 200 / template > 100 = 400", () => { - expect(parseTokenRequestBody({ userid: "a".repeat(101), username: "n" }).ok).toBe(false); - expect( - parseTokenRequestBody({ userid: "1", username: "a".repeat(201) }).ok, - ).toBe(false); - expect( - parseTokenRequestBody({ userid: "1", username: "n", fid: "a".repeat(201) }).ok, - ).toBe(false); - expect( - parseTokenRequestBody({ userid: "1", username: "n", template: "a".repeat(101) }).ok, - ).toBe(false); - }); -}); diff --git a/wordaddin/services/versioningService.ts b/wordaddin/services/versioningService.ts index 506130a..54dd519 100644 --- a/wordaddin/services/versioningService.ts +++ b/wordaddin/services/versioningService.ts @@ -5,11 +5,6 @@ * - คงชื่อ/ลายเซ็น uploadVersion/getVersions/getDownloadLink/newClientVersionId ไว้เป๊ะ — * วันปลด mock: ก้อบ body ของต้นฉบับกลับมาได้เลย * - ต้นฉบับมีอีก 2 ตัวที่ไม่มี caller ใน UI จึงไม่พอร์ต: buildDownloadUrl, downloadVersion - * - **วันปลด mock ห้ามส่ง userId/userName/templateId ใน FormData** — contract ของ - * POST /api/documents/versions ถอด 3 field นี้ออกแล้ว (ส่งมา = 400) และไม่มี - * segment {docId} ใน URL อีกต่อไป: server แกะ identity/template และ docId - * (= fid — fileId ของ e-Court, string) จาก Bearer token (ออกที่ POST - * /api/documents/token ด้วย x-api-key, fid บังคับ) ที่ apiClient.ts แนบให้อยู่แล้ว * - **metadata ของ version ถูก persist ลง localStorage** (record เล็ก ๆ ต่อ documentId) — * ไม่งั้นปิดเอกสาร (taskpane ปิดตาม) แล้วเปิดใหม่ history ว่างเปล่า ทั้งที่ metadata * การ track อยู่ในไฟล์จริง ทำให้ทดสอบวง FR-18 ปิด→เปิดใหม่ไม่ได้; blob ของไฟล์จริง