UNPKG

ec-sri-invoice-signer

Version:
158 lines (157 loc) 6.65 kB
"use strict"; /** * This doesn't implement the http://www.w3.org/TR/2001/REC-xml-c14n-20010315 specification entirely. Currently it's just * good enough to work with xml invoices that align with below requirements (which should cover most of the cases). * At first, won't implement the complete specification considering that the SRI software doesn't need many XML features * that the specification supports. In the future, if really needed, a complete canonicalization may be implemented. * * The requirements for the input invoice XML are (none of these are needed to interchange XML data with the): * - The invoice to sign should consist of the 'factura' node and its children (e.g. <?xml version="1.0" encoding="UTF-8"?><factura Id="comprobante">...</factura>). * The document declaration (i.e. <?xml version="1.0" encoding="UTF-8"?>) is optional. * - The invoice should be utf-8 encoded. * - No namespaces. * - No DOCTYPE entities. * - No Document type definition (DTD) tags. * - No xml-prefixed attributes (xml:<attr_name>). * * Canonicalization based on: * - https://www.w3.org/TR/xml-c14n * - https://www.di-mgt.com.au/xmldsig-c14n.html */ Object.defineProperty(exports, "__esModule", { value: true }); exports.c14nCanonicalize = void 0; // TODO: /** * - Remove any unnecesary code. * - Make sure tests and coverage are passing. OK */ const xml_1 = require("../utils/xml"); const CommentNodeIdentifier = '#comment'; const attributeCompare = (a, b) => { if (!a.namespaceURI && b.namespaceURI) { return -1; } if (!b.namespaceURI && a.namespaceURI) { return 1; } const left = a.namespaceURI + a.name; const right = b.namespaceURI + b.name; if (left === right) { return 0; } return left < right ? -1 : 1; }; const namespaceCompare = (a, b) => { if (!a.prefix) { return -1; } if (!b.prefix) { return 1; } if (a.prefix === b.prefix) { return 0; } return a.prefix < b.prefix ? -1 : 1; }; const parseAttributesAndNamespaces = (data) => { var _a; const attributes = []; const attributesWithPendingNamespace = []; const namespacesByPrefix = {}; for (const rawKey of Object.keys(data)) { const key = rawKey.substring(2); // without the @_ prefix const splittedKey = key.split(':'); const isNamespace = splittedKey[0] === 'xmlns'; const isAttribute = !isNamespace; const isAttributeWithNamespace = isAttribute && splittedKey.length === 2; if (isNamespace) { const prefix = splittedKey[1]; namespacesByPrefix[prefix] = { prefix, uri: data[rawKey] }; } if (isAttribute) { if (isAttributeWithNamespace) { const prefix = splittedKey[0]; const namespaceURI = (_a = namespacesByPrefix[prefix]) === null || _a === void 0 ? void 0 : _a.uri; if (namespaceURI === undefined) { attributesWithPendingNamespace.push({ name: splittedKey[isAttributeWithNamespace ? 1 : 0], namespaceURI, namespacePrefix: prefix, value: data[rawKey] }); } else { attributes.push({ name: splittedKey[isAttributeWithNamespace ? 1 : 0], namespacePrefix: prefix, namespaceURI, value: data[rawKey] }); } } else { attributes.push({ name: splittedKey[isAttributeWithNamespace ? 1 : 0], namespacePrefix: undefined, namespaceURI: undefined, value: data[rawKey] }); } } } const solvedAttributesWithPendingNamespace = attributesWithPendingNamespace.map((attr) => { var _a; return (Object.assign(Object.assign({}, attr), { namespaceURI: namespacesByPrefix[(_a = attr.namespacePrefix) !== null && _a !== void 0 ? _a : ''].uri, value: attr.value })); }); return { attributes: [...attributes, ...solvedAttributesWithPendingNamespace], namespaces: Object.values(namespacesByPrefix) }; }; const sortNamespaces = (namespaces) => { namespaces.sort(namespaceCompare); }; const sortAttributes = (attributes) => { attributes.sort(attributeCompare); }; const removeNode = (obj, currentPosition) => { obj.splice(currentPosition, 1); }; const insertAttributesAndNamespaces = (node, attributes, namespaces) => { const toInsert = {}; namespaces.forEach((namespace) => { toInsert[`@_xmlns${namespace.prefix ? `:${namespace.prefix}` : ''}`] = namespace.uri; }); attributes.forEach((attr) => { toInsert[`@_${attr.namespacePrefix ? `${attr.namespacePrefix}:${attr.name}` : attr.name}`] = attr.value; }); node[':@'] = toInsert; }; const mergeLocalAndInheritedNamespaces = (local, inherited) => { // A local should override an inherited with the same prefix const acceptedInherited = inherited.filter((inheritedNamespace) => !local.some((localNamespace) => localNamespace.prefix === inheritedNamespace.prefix)); return [...acceptedInherited, ...local]; }; const processNode = (node, alreadyDeclaredNamespaces, inheritedNamespaces) => { var _a, _b; const reservedKeywords = new Set([':@', '#text', CommentNodeIdentifier]); let { attributes, namespaces } = parseAttributesAndNamespaces((_a = node[':@']) !== null && _a !== void 0 ? _a : {}); if (inheritedNamespaces) { namespaces = mergeLocalAndInheritedNamespaces(namespaces, inheritedNamespaces); } sortNamespaces(namespaces); sortAttributes(attributes); const tagName = Object.keys(node).find((key) => !reservedKeywords.has(key)); const children = ((_b = node[tagName]) !== null && _b !== void 0 ? _b : []); let i = 0; insertAttributesAndNamespaces(node, attributes, namespaces); while (i < children.length) { const child = children[i]; if (child[CommentNodeIdentifier]) { removeNode(children, i); continue; } processNode(child, namespaces); i++; } return { namespaces: [...alreadyDeclaredNamespaces, ...namespaces] }; }; const processObj = (obj, inheritedNamespaces) => { let i = 0; while (i < obj.length) { const node = obj[i]; if (node[CommentNodeIdentifier]) { removeNode(obj, i); continue; } processNode(node, [], inheritedNamespaces); i++; } }; const c14nCanonicalize = (xml, options) => { const obj = (0, xml_1.parseXml)(xml); processObj(obj, options === null || options === void 0 ? void 0 : options.inheritedNamespaces); return (0, xml_1.buildXml)(obj); }; exports.c14nCanonicalize = c14nCanonicalize;