// lib/lib-utils.ts import { createHmac, createPrivateKey, createPublicKey, timingSafeEqual, verify, sign } from "node:crypto"; function generateHmacSha256(data, secretKey) { const payload = typeof data === "string" ? data : JSON.stringify(sortObjectKeys(data)); return createHmac("sha256", secretKey).update(payload, "utf8").digest("hex"); } function signEd25519(message, privateKeyHex) { const rawKeyBuffer = Buffer.from(privateKeyHex, "hex"); const seed = rawKeyBuffer.length === 64 ? rawKeyBuffer.subarray(0, 32) : rawKeyBuffer; 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"); } function sortObjectKeys(obj) { return Object.keys(obj).sort().reduce((result, key) => { result[key] = obj[key]; return result; }, {}); } // lib/lib-tmpl.ts import PizzaZip from "pizzip"; import { DOMParser, XMLSerializer } from "@xmldom/xmldom"; import xpath from "xpath"; var getCreateReport = async () => { const mod = await import("docx-templates"); const createReportFn = typeof mod.createReport === "function" ? mod.createReport : typeof mod.default === "function" ? mod.default : void 0; if (!createReportFn) { throw new Error("docx-templates createReport is not available"); } return createReportFn; }; var WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; var asXpathNode = (node) => node; function createSelector() { return xpath.useNamespaces({ w: WORD_NS }); } function detectSdtControlType(sdt, select) { const sdtPr = select( "./w:sdtPr", asXpathNode(sdt), true ); if (!sdtPr) { return { type: "other", multiLine: false }; } const textEl = select( "./w:text", asXpathNode(sdtPr), true ); if (textEl) { const multiLineAttr = select( "./@w:multiLine", asXpathNode(textEl), true ); return { type: "plain", multiLine: multiLineAttr?.value === "1" || multiLineAttr?.value === "true" }; } const richTextEl = select( "./w:richText", asXpathNode(sdtPr), true ); 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 ); if (found) { return { type: "other", multiLine: false }; } } return { type: "rich", multiLine: true }; } function detectSdtStructure(sdtContent, select) { const directP = select( "./w:p", asXpathNode(sdtContent) ); return directP.length > 0 ? "block" : "inline"; } function buildRunChildren(doc, text) { const nodes = []; const tokens = text.split(/(\t|\n)/); tokens.forEach((token) => { if (token === "") return; if (token === " ") { 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, text, rPr) { 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, sdtContent, select, text, allowLineBreak) { const safeText = allowLineBreak ? text : text.replace(/\n/g, " "); const existingRuns = select( "./w:r", asXpathNode(sdtContent) ); const firstRPr = existingRuns.length > 0 ? select( "./w:rPr", asXpathNode(existingRuns[0]), true ) : void 0; existingRuns.forEach((r) => { if (r.parentNode) r.parentNode.removeChild(r); }); sdtContent.appendChild( buildRun(doc, safeText, firstRPr) ); } function insertBlockContent(doc, sdtContent, select, text, allowMultiParagraph) { const allParagraphs = select( "./w:p", asXpathNode(sdtContent) ); 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 ); const firstRun = select( "./w:r", asXpathNode(firstP), true ); const firstRPr = firstRun ? select( "./w:rPr", asXpathNode(firstRun), true ) : void 0; allParagraphs.forEach((p) => { if (p !== firstP && p.parentNode) { p.parentNode.removeChild(p); } }); const existingRuns = select( "./w:r", asXpathNode(firstP) ); existingRuns.forEach((r) => { if (r.parentNode) r.parentNode.removeChild(r); }); if (!allowMultiParagraph) { firstP.appendChild(buildRun(doc, text.replace(/\n+/g, "\n"), firstRPr)); return; } const paragraphs = text.split("\n\n"); paragraphs.forEach((pText, index) => { let targetP; 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)); } buildRunChildren(doc, pText).forEach( (node) => run.appendChild(node) ); targetP.appendChild(run); }); } function insertFallbackContent(sdtContent, select, text) { const textNodes = select( ".//w:t", asXpathNode(sdtContent) ); if (textNodes.length === 0) return; textNodes[0].textContent = text; for (let i = 1; i < textNodes.length; i++) { textNodes[i].textContent = ""; } } function normalizeValue(value) { if (value === null || value === void 0) { return ""; } if (typeof value === "string") { return value; } if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { return String(value); } return JSON.stringify(value); } function templateContentControls(templateBuffer, data) { 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) ); sdtNodes.forEach((sdt) => { const tagAttr = select( "./w:sdtPr/w:tag/@w:val", asXpathNode(sdt), true ); 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 ); 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" }); } var CUSTOM_NS = "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"; var VT_NS = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"; var REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"; var CONTENT_TYPES_NS = "http://schemas.openxmlformats.org/package/2006/content-types"; var XMLNS_NS = "http://www.w3.org/2000/xmlns/"; var CUSTOM_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"; var CUSTOM_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.custom-properties+xml"; var CUSTOM_FMTID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"; var CUSTOM_PATH = "docProps/custom.xml"; function parseXml(xml, fileName) { const errors = []; const parser = new DOMParser(); const doc = parser.parseFromString( xml, "application/xml" ); if (errors.length > 0 || !doc.documentElement) { throw new Error( `Invalid ${fileName}: ${errors.join("; ")}` ); } return doc; } function assertRoot(doc, namespace, localName, fileName) { 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() { const xml = [ '', `` ].join(""); return parseXml(xml, CUSTOM_PATH); } function createValueElement(doc, value) { let tagName; let text; if (value == null) { 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) { 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") ); 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) { 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" ) ); 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) ); } function setCustomDocProps(docxBuffer, data) { const zip = new PizzaZip(docxBuffer); const customFile = zip.file(CUSTOM_PATH); const doc = customFile ? parseXml(customFile.asText(), CUSTOM_PATH) : createCustomXml(); const root = assertRoot( doc, CUSTOM_NS, "Properties", CUSTOM_PATH ); root.setAttributeNS(XMLNS_NS, "xmlns:vt", VT_NS); const properties = Array.from( root.getElementsByTagNameNS(CUSTOM_NS, "property") ); const propertyMap = /* @__PURE__ */ new Map(); const usedPids = /* @__PURE__ */ 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); } const valueElement = createValueElement(doc, value); while (property.firstChild) { property.removeChild(property.firstChild); } property.appendChild(valueElement); } zip.file( CUSTOM_PATH, new XMLSerializer().serializeToString(doc) ); ensureCustomRelationship(zip); ensureCustomContentType(zip); return Buffer.from( zip.generate({ type: "nodebuffer", compression: "DEFLATE" }) ); } function p_cc(title, tag, text = "", isMultiLine = true, lock = "sdtLocked") { const lockTag = lock == "none" ? "" : ``; const multiLineTag = isMultiLine ? '' : ""; const content = escapeXml(text); const xmlString = `${lockTag}${multiLineTag}${content}`; return `||${xmlString}||`; } function br(n) { let text = ""; for (let i = 0; i < n; i++) text += ""; return `||${text}||`; } function bm(id, name) { return `||||`; } function escapeXml(unsafe) { return unsafe.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); } var templateDynamicContents = async (template, data) => { const createReport = await getCreateReport(); return await createReport({ template, data, cmdDelimiter: ["{{", "}}"], additionalJsContext: { bm, br, p_cc } }); }; async function applyTemplate(templateBuffer, templateData, secret, private_key, ttl) { const originalTicket_data = templateData?.info?.ticket_data; if (originalTicket_data && typeof originalTicket_data === "string") { try { const originalTicketObj = JSON.parse(originalTicket_data); if (originalTicketObj && typeof originalTicketObj === "object") { console.log("Found valid ticket data: hash it"); const expireTimestamp = Math.floor(Date.now() / 1e3) + ttl; const fullObj = { ...originalTicketObj, exp: expireTimestamp }; const sortedObj = sortObjectKeys(fullObj); const ticket_data = JSON.stringify(sortedObj); const HmacSha256 = generateHmacSha256(ticket_data, secret); const Ed25519 = signEd25519(ticket_data, private_key); templateBuffer = setCustomDocProps(templateBuffer, { ticket_data, HmacSha256, Ed25519 }); } } catch (error) { console.error("Set custom property fail", error); } } if (templateData?.dc && typeof templateData?.dc === "object") { templateBuffer = Buffer.from(await templateDynamicContents(templateBuffer, templateData.dc)); } if (templateData?.cc && typeof templateData?.cc === "object") { templateBuffer = templateContentControls(templateBuffer, templateData.cc); } return templateBuffer; } // workers/docx.worker.ts async function generateDocx(input) { const templateBuffer = Buffer.from(input.templateBuffer); const output = await applyTemplate( templateBuffer, input.templateData, input.secret, input.privateKeyHex, input.ticketSec ); return Buffer.from(output); } export { generateDocx as default };