ec-sri-invoice-signer
Version:
Ecuador SRI invoice signer.
158 lines (157 loc) • 6.65 kB
JavaScript
;
/**
* 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;