@zaptic-external/saml
Advanced tools
Comparing version 1.1.0 to 2.0.0
@@ -5,2 +5,9 @@ # Changelog | ||
## [2.0.0] - 2020/06/29 | ||
### Breaking change | ||
- Support multiple certificates by allowing an array to be passed to the SP. | ||
This means that we now have to pass an array and an extra attribute to tell us when the cert expires | ||
## [1.1.0] - 2019/06/21 | ||
@@ -7,0 +14,0 @@ |
@@ -1,6 +0,3 @@ | ||
export declare function signXML(xmlToSign: string, options: { | ||
certificate: string; | ||
key: string; | ||
algorithm: 'sha256' | 'sha512'; | ||
}): string; | ||
import { Certificate } from './helpers/certificate'; | ||
export declare function signXML(xmlToSign: string, options: Certificate): string; | ||
export declare type CheckSignatureOptions = { | ||
@@ -7,0 +4,0 @@ algorithm: 'sha256' | 'sha512' | 'sha1'; |
export declare function toX059(certificate: string): string; | ||
export declare function toPEM(certificate: string): string; | ||
export interface Certificate { | ||
certificate: string; | ||
key: string; | ||
notAfter: Date; | ||
algorithm: 'sha256' | 'sha512'; | ||
} | ||
export declare function getNonExpired(certificates: Certificate[]): Certificate; |
@@ -23,2 +23,9 @@ "use strict"; | ||
exports.toPEM = toPEM; | ||
function getNonExpired(certificates) { | ||
const cert = certificates.filter(certificate => certificate.notAfter.getTime() > Date.now())[0]; | ||
if (!cert) | ||
throw new Error('No valid certificates found'); | ||
return cert; | ||
} | ||
exports.getNonExpired = getNonExpired; | ||
//# sourceMappingURL=certificate.js.map |
@@ -6,3 +6,3 @@ export declare type AlgorithmAttribute = { | ||
}; | ||
export declare type Signature = { | ||
export interface Signature { | ||
SignedInfo: { | ||
@@ -30,4 +30,4 @@ CanonicalizationMethod: AlgorithmAttribute[]; | ||
KeyInfo: KeyInfo[]; | ||
}; | ||
export declare type KeyInfo = { | ||
} | ||
export interface KeyInfo { | ||
X509Data: { | ||
@@ -38,2 +38,2 @@ X509Certificate: { | ||
}[]; | ||
}; | ||
} |
import { Signature } from './helpers/saml-types'; | ||
export declare namespace SAMLLoginResponse { | ||
type Root = { | ||
interface Root { | ||
$: { | ||
@@ -22,4 +22,4 @@ Destination: string; | ||
Assertion: Assertion[]; | ||
}; | ||
type Assertion = { | ||
} | ||
interface Assertion { | ||
$: { | ||
@@ -45,4 +45,4 @@ ID: string; | ||
}[]; | ||
}; | ||
type Attribute = { | ||
} | ||
interface Attribute { | ||
$: { | ||
@@ -54,4 +54,4 @@ Name: string; | ||
}]; | ||
}; | ||
type Conditions = { | ||
} | ||
interface Conditions { | ||
$: { | ||
@@ -66,4 +66,4 @@ NotBefore: string; | ||
}[]; | ||
}; | ||
type Subject = { | ||
} | ||
interface Subject { | ||
NameID: { | ||
@@ -84,5 +84,5 @@ _: string; | ||
}[]; | ||
}; | ||
} | ||
} | ||
declare type LoginResponse<T> = { | ||
export interface LoginResponse<T> { | ||
id: string; | ||
@@ -105,8 +105,8 @@ inResponseTo: string; | ||
}[]; | ||
}; | ||
declare type CheckOptions = { | ||
} | ||
interface CheckOptions { | ||
issuer: string; | ||
audience: string; | ||
strictTimeCheck: boolean; | ||
}; | ||
} | ||
export declare function extract<T extends { | ||
@@ -113,0 +113,0 @@ [key: string]: string; |
import { Signature } from './helpers/saml-types'; | ||
export declare namespace SAMLLogoutRequest { | ||
type Root = { | ||
interface Root { | ||
$: { | ||
@@ -19,5 +19,5 @@ ID: string; | ||
Signature: Signature[]; | ||
}; | ||
} | ||
} | ||
declare type LogoutRequest = { | ||
interface LogoutRequest { | ||
id: string; | ||
@@ -28,4 +28,4 @@ issueInstant: string; | ||
names: string[]; | ||
}; | ||
} | ||
export declare function extract(response: string): Promise<LogoutRequest>; | ||
export {}; |
import { KeyInfo, Signature } from './helpers/saml-types'; | ||
export declare namespace SAMLMetadata { | ||
type Root = { | ||
interface Root { | ||
$: { | ||
@@ -22,4 +22,4 @@ ID: string; | ||
}[]; | ||
}; | ||
type Service = { | ||
} | ||
interface Service { | ||
$: { | ||
@@ -29,5 +29,5 @@ Binding: string; | ||
}; | ||
}; | ||
} | ||
} | ||
declare type Metadata = { | ||
interface Metadata { | ||
identityProvider: { | ||
@@ -41,4 +41,4 @@ id: string; | ||
}; | ||
}; | ||
} | ||
export declare function extract(xml: string): Promise<Metadata>; | ||
export {}; |
import { Validator } from './helpers/xml'; | ||
export declare type IDPOptions = { | ||
import * as LoginResponse from './login-response'; | ||
import { Certificate } from './helpers/certificate'; | ||
export interface IDPOptions { | ||
id: string; | ||
@@ -9,19 +11,11 @@ loginUrl: string; | ||
}; | ||
}; | ||
export declare type SPOptions = { | ||
} | ||
export interface SPOptions { | ||
id: string; | ||
singleLogoutUrl: string; | ||
assertionUrl: string; | ||
signature: { | ||
certificate: string; | ||
key: string; | ||
algorithm: 'sha256' | 'sha512'; | ||
}; | ||
encryption?: { | ||
certificate: string; | ||
key: string; | ||
algorithm: 'sha256' | 'sha512'; | ||
}; | ||
}; | ||
export declare type Preferences = { | ||
signature: Certificate[]; | ||
encryption?: Certificate[]; | ||
} | ||
export interface Preferences { | ||
signLoginRequests: boolean; | ||
@@ -35,4 +29,4 @@ strictTimeCheck: boolean; | ||
forceAuthenticationByDefault: boolean; | ||
}; | ||
export declare type OptionsWithoutMetadata = { | ||
} | ||
export interface OptionsWithoutMetadata { | ||
preferences?: Partial<Preferences>; | ||
@@ -42,4 +36,4 @@ getUUID: () => string | Promise<string>; | ||
sp: SPOptions; | ||
}; | ||
export declare type OptionsWithMetadata = { | ||
} | ||
export interface OptionsWithMetadata { | ||
preferences?: Partial<Preferences>; | ||
@@ -49,4 +43,4 @@ getUUID: () => string | Promise<string>; | ||
sp: SPOptions; | ||
}; | ||
declare type SAMLProviderOptions = { | ||
} | ||
interface SAMLProviderOptions { | ||
XSDs: { | ||
@@ -60,3 +54,3 @@ protocol: Validator; | ||
getUUID: () => string | Promise<string>; | ||
}; | ||
} | ||
export default class SAMLProvider { | ||
@@ -71,24 +65,8 @@ static create(options: OptionsWithoutMetadata | OptionsWithMetadata): Promise<SAMLProvider>; | ||
buildLoginRequestRedirectURL(relayState?: string, forceAuthentication?: boolean): Promise<string>; | ||
parseLoginResponse(query: { | ||
parseLoginResponse<T extends { | ||
[key: string]: string; | ||
}>(query: { | ||
[key: string]: any; | ||
}): Promise<{ | ||
response: { | ||
id: string; | ||
inResponseTo: string; | ||
issuer: string; | ||
statusCodes: string[]; | ||
assertions: { | ||
subject: string; | ||
issuer: string; | ||
sessionIndex: string; | ||
notBefore: Date; | ||
notOnOrAfter: Date; | ||
audience: string; | ||
attributes: { | ||
[key: string]: string; | ||
} & { | ||
[x: string]: string; | ||
}; | ||
}[]; | ||
}; | ||
response: LoginResponse.LoginResponse<T>; | ||
relayState: string; | ||
@@ -95,0 +73,0 @@ }>; |
@@ -19,2 +19,3 @@ "use strict"; | ||
const login_request_1 = require("./templates/login-request"); | ||
const certificate_1 = require("./helpers/certificate"); | ||
function hasMetadata(options) { | ||
@@ -59,3 +60,5 @@ return typeof options.idp === 'string'; | ||
}); | ||
const xml = this.preferences.signLoginRequests ? crypto_1.signXML(request, this.serviceProvider.signature) : request; | ||
const xml = this.preferences.signLoginRequests | ||
? crypto_1.signXML(request, certificate_1.getNonExpired(this.serviceProvider.signature)) | ||
: request; | ||
// Google uses SingleSignOnService URLs that have a query param set in them so we need to detect that and build | ||
@@ -79,3 +82,3 @@ // the url accordingly | ||
// Potentially decrypt the assertions | ||
const decryptedResponse = yield crypto_1.decryptXML(rawResponse, this.serviceProvider.encryption.key); | ||
const decryptedResponse = yield crypto_1.decryptXML(rawResponse, certificate_1.getNonExpired(this.serviceProvider.encryption).key); | ||
// Check the signature - this should throw if there is an error | ||
@@ -82,0 +85,0 @@ crypto_1.checkSignature(decryptedResponse, this.identityProvider.signature); |
@@ -25,7 +25,16 @@ "use strict"; | ||
singleLogoutUrl: 'http://localhost:7000/sp/logout', | ||
signature: { | ||
algorithm: 'sha256', | ||
certificate: testCert, | ||
key: testKey | ||
} | ||
signature: [ | ||
{ | ||
algorithm: 'sha256', | ||
certificate: 'MIIE/EXPIRED/Q==', | ||
notAfter: new Date(Date.now() - 100000), | ||
key: testKey | ||
}, | ||
{ | ||
algorithm: 'sha256', | ||
certificate: testCert, | ||
notAfter: new Date(Date.now() + 100000), | ||
key: testKey | ||
} | ||
] | ||
}, | ||
@@ -32,0 +41,0 @@ idp: { |
@@ -5,3 +5,3 @@ declare type Options = { | ||
certificate: string; | ||
}; | ||
}[]; | ||
singleLogoutUrl: string; | ||
@@ -8,0 +8,0 @@ assertionUrl: string; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const certificate_1 = require("../helpers/certificate"); | ||
function keyDescriptor(certificates, use) { | ||
return certificates.map(({ certificate }) => `<KeyDescriptor use="${use}"> | ||
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> | ||
<X509Data> | ||
<X509Certificate>${certificate_1.toX059(certificate)}</X509Certificate> | ||
</X509Data> | ||
</KeyInfo> | ||
</KeyDescriptor>`); | ||
} | ||
exports.default = (options, nameIdFormat) => `<EntityDescriptor | ||
@@ -13,19 +22,6 @@ xmlns="urn:oasis:names:tc:SAML:2.0:metadata" | ||
<KeyDescriptor use="signing"> | ||
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> | ||
<X509Data> | ||
<X509Certificate>${certificate_1.toX059(options.signature.certificate)}</X509Certificate> | ||
</X509Data> | ||
</KeyInfo> | ||
</KeyDescriptor> | ||
${keyDescriptor(options.signature, 'signing').join('\n')} | ||
<KeyDescriptor use="encryption"> | ||
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> | ||
<X509Data> | ||
<X509Certificate>${certificate_1.toX059(options.signature.certificate)}</X509Certificate> | ||
</X509Data> | ||
</KeyInfo> | ||
${keyDescriptor(options.signature, 'encryption').join('\n')} | ||
</KeyDescriptor> | ||
<SingleLogoutService | ||
@@ -32,0 +28,0 @@ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" |
{ | ||
"name": "@zaptic-external/saml", | ||
"version": "1.1.0", | ||
"version": "2.0.0", | ||
"description": "Minimal saml service provider with support for sp-initiated redirect login only", | ||
@@ -27,7 +27,7 @@ "main": "dist/service-provider.js", | ||
"@types/chai": "^4.1.4", | ||
"@types/libxmljs": "^0.18.3", | ||
"@types/libxmljs": "0.18.3", | ||
"@types/mocha": "^5.2.4", | ||
"@types/node": "^10.5.2", | ||
"@types/xml2js": "^0.4.3", | ||
"@types/xmldom": "^0.1.29", | ||
"@types/xml2js": "0.4.3", | ||
"@types/xmldom": "0.1.29", | ||
"chai": "^4.1.2", | ||
@@ -41,9 +41,9 @@ "mocha": "^5.2.0", | ||
"dependencies": { | ||
"libxmljs": "^0.19.5", | ||
"xml-crypto": "^0.10.1", | ||
"xml-encryption": "^0.11.2", | ||
"xml2js": "^0.4.19", | ||
"xmldom": "^0.1.27", | ||
"xpath": "^0.0.27" | ||
"libxmljs": "0.19.7", | ||
"xml-crypto": "0.10.1", | ||
"xml-encryption": "0.11.2", | ||
"xml2js": "0.4.19", | ||
"xmldom": "0.1.27", | ||
"xpath": "0.0.27" | ||
} | ||
} |
139
README.md
@@ -7,1 +7,140 @@ [![Known Vulnerabilities](https://snyk.io/test/github/Zaptic/saml/badge.svg?targetFile=package.json)](https://snyk.io/test/github/Zaptic/saml?targetFile=package.json) | ||
At the moment it supports only service provider initiated login (using redirect binding) but more will come. | ||
## Options | ||
The options object has 4 top level components: | ||
`sp`, `idp`, `preferences` and `getUUID` | ||
### The `sp` property | ||
It should contain all configuration tied to the service provider. | ||
These options will be used to generate the service provider metadata file and populate the login requests. | ||
`sp.id` | ||
A globally unique identifier that identifies your service provider. This corresponds to the entityId saml field. | ||
It is often a URL as they make it easy to namespace things so long as you own the domain. | ||
If your id might contain numbers the saml spec says that the id should start with an "\_". | ||
`sp.assertionUrl` | ||
URL that will receive a POST request containing an assertion - usually login response - from the identity provider. | ||
`sp.singleLogoutUrl` | ||
URL that will receive single log out requests from the identity provider. | ||
At this time this library does not provide with a way to parse logout requests but it's an open issue. | ||
`sp.signature` | ||
The `signature` object see relevant section. | ||
`sp.encryption` - optional | ||
This is also a `signature` object but the certificates provided in this one are for encrypting the login responses. | ||
When nothing is provided it defaults to the signing certificates under the `sp.signature` object. | ||
### The `idp` property | ||
It should be either an object or string. | ||
If it's a string, it should be a string containing the identity provider's metadata xml. | ||
It is recommended that you use the metadata file as it's easier to maintain than the manually assigned properties. | ||
`idp.id` | ||
A globally unique identifier that identifies the identity provider. This corresponds to the entityId saml field. | ||
Same restrictions as for the sp id apply. | ||
`idp.loginUrl` | ||
The url that we should send auth requests to | ||
`idp.signature` | ||
This is the object that contains the certificates and signature algorithm that we should accept for signing the | ||
identity provider's assertions | ||
`idp.signature.algorithm` | ||
Currently, supported are `sha256` and `sha512` this is the algorithm with which the identity provider will sign the | ||
assertions | ||
`idp.signature.allowedCertificates` | ||
These are the public certificates that correspond to the private key the identity provider is using to sign the | ||
assertions. It must have at least one entry as we don't support unsigned requests at the moment. | ||
### The `signature` object | ||
It should contain everything needed to sign our request to the identity provider. | ||
Because we are trying to be secure by default, this is not optional. | ||
`signature.algorithm` | ||
The signing algorithm to use. Only `sha256` and `sha512` are supported at the moment. | ||
`signature.certificate` | ||
The pem encoded public key used to sign your requests. | ||
`signature.key` | ||
The pem encoded public key used to sign your requests. | ||
### The `preferences` object | ||
`preferences.signLoginRequests` default true | ||
Set to true to sign login requests (aka authnRequests). When set to false the login requests will not be signed | ||
`preferences.signLoginRequests` default false | ||
When set to true, it ensures that the identity provider sends `notBefore` and `notOnOrAfter` and that the assertion | ||
is received in the interval. | ||
When set to false it will not error if the identity provider does not send them but will still check that the assertion | ||
is in the interval if the dates are provided. | ||
`preferences.nameIdFormat` default 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' | ||
The name id policy you want to use. For now only this one is supported so I would recommend not changing this | ||
`preferences.addNameIdPolicy` default false | ||
Set to true to not send the `nameIdFormat` along with login requests. This voids the previous parameter when set to true. | ||
`preferences.forceAuthenticationByDefault` default false | ||
Request the identity provider to prompt the user with a challenge (e.g user name + password) even if they have a valid | ||
session when set to true. | ||
`preferences.attributeMapping` default '{}' | ||
Mapping for the attributes given by the identity provider in the Attribute field. I would not worry about that too much | ||
unless you need extra claims from the identity provider. | ||
Example: | ||
``` | ||
{ | ||
'http://schemas.microsoft.com/identity/claims/objectidentifier': 'id', | ||
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name': 'email' | ||
} | ||
``` | ||
Full example: | ||
```typescript | ||
const options = { | ||
sp: { | ||
id: 'http://service-provider.zaptic', | ||
assertionUrl: 'http://localhost:7000/sp/login', | ||
singleLogoutUrl: 'http://localhost:7000/sp/logout', | ||
signature: [ | ||
{ | ||
algorithm: <'sha256'>'sha256', | ||
certificate: testCert, | ||
key: testKey | ||
} | ||
] | ||
}, | ||
idp: { | ||
id: 'test-idp', | ||
loginUrl: 'http://localhost:7000/idp/requestLogin', | ||
signature: { | ||
algorithm: <'sha256'>'sha256', | ||
allowedCertificates: [] | ||
} | ||
}, | ||
preferences: { | ||
forceAuthenticationByDefault: true, | ||
signLoginRequests: false, | ||
strictTimeCheck: true, | ||
addNameIdPolicy: true, | ||
attributeMapping: { | ||
'http://schemas.microsoft.com/identity/claims/objectidentifier': 'id', | ||
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name': 'email' | ||
} | ||
}, | ||
getUUID: () => 'test-uuid' | ||
} | ||
``` |
declare module 'xml-crypto' { | ||
type SignedXmlOptions = { | ||
interface SignedXmlOptions { | ||
prefix?: string | ||
@@ -4,0 +4,0 @@ attrs?: {} |
declare module 'xml-encryption' { | ||
export type EncryptOptions = { | ||
export interface EncryptOptions { | ||
rsa_pub: string | ||
@@ -9,3 +9,3 @@ pem: string | ||
export type DecryptOptions = { | ||
export interface DecryptOptions { | ||
key: string | ||
@@ -12,0 +12,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Copyleft License
License(Experimental) Copyleft license information was found
Found 1 instance in 1 package
No License Found
License(Experimental) License information could not be found
Found 1 instance in 1 package
Non-permissive License
License(Experimental) A license not known to be considered permissive was found
Found 1 instance in 1 package
166921
0
146
1479
+ Addedansi-regex@2.1.1(transitive)
+ Addedaproba@1.2.0(transitive)
+ Addedare-we-there-yet@1.1.7(transitive)
+ Addedchownr@1.1.4(transitive)
+ Addedcode-point-at@1.1.0(transitive)
+ Addedcore-util-is@1.0.3(transitive)
+ Addeddebug@3.2.7(transitive)
+ Addeddeep-extend@0.6.0(transitive)
+ Addeddetect-libc@1.0.3(transitive)
+ Addedfs-minipass@1.2.7(transitive)
+ Addedgauge@2.7.4(transitive)
+ Addediconv-lite@0.4.24(transitive)
+ Addedignore-walk@3.0.4(transitive)
+ Addedini@1.3.8(transitive)
+ Addedis-fullwidth-code-point@1.0.0(transitive)
+ Addedisarray@1.0.0(transitive)
+ Addedlibxmljs@0.19.7(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedminipass@2.9.0(transitive)
+ Addedminizlib@1.3.3(transitive)
+ Addedmkdirp@0.5.6(transitive)
+ Addedms@2.1.3(transitive)
+ Addedneedle@2.9.1(transitive)
+ Addednode-pre-gyp@0.11.0(transitive)
+ Addednopt@4.0.3(transitive)
+ Addednpm-bundled@1.1.2(transitive)
+ Addednpm-normalize-package-bin@1.0.1(transitive)
+ Addednpm-packlist@1.4.8(transitive)
+ Addednpmlog@4.1.2(transitive)
+ Addednumber-is-nan@1.0.1(transitive)
+ Addedos-homedir@1.0.2(transitive)
+ Addedos-tmpdir@1.0.2(transitive)
+ Addedosenv@0.1.5(transitive)
+ Addedprocess-nextick-args@2.0.1(transitive)
+ Addedrc@1.2.8(transitive)
+ Addedreadable-stream@2.3.8(transitive)
+ Addedrimraf@2.7.1(transitive)
+ Addedsafe-buffer@5.1.2(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsemver@5.7.2(transitive)
+ Addedstring-width@1.0.2(transitive)
+ Addedstring_decoder@1.1.1(transitive)
+ Addedstrip-ansi@3.0.1(transitive)
+ Addedstrip-json-comments@2.0.1(transitive)
+ Addedtar@4.4.19(transitive)
+ Addedxml2js@0.4.19(transitive)
+ Addedxmlbuilder@9.0.7(transitive)
+ Addedxmldom@0.1.27(transitive)
+ Addedyallist@3.1.1(transitive)
- Removed@mapbox/node-pre-gyp@1.0.11(transitive)
- Removedagent-base@6.0.2(transitive)
- Removedansi-regex@5.0.1(transitive)
- Removedaproba@2.0.0(transitive)
- Removedare-we-there-yet@2.0.0(transitive)
- Removedchownr@2.0.0(transitive)
- Removedcolor-support@1.1.3(transitive)
- Removeddebug@4.3.4(transitive)
- Removeddetect-libc@2.0.3(transitive)
- Removedemoji-regex@8.0.0(transitive)
- Removedfs-minipass@2.1.0(transitive)
- Removedgauge@3.0.2(transitive)
- Removedhttps-proxy-agent@5.0.1(transitive)
- Removedis-fullwidth-code-point@3.0.0(transitive)
- Removedlibxmljs@0.19.10(transitive)
- Removedlru-cache@6.0.0(transitive)
- Removedmake-dir@3.1.0(transitive)
- Removedminipass@3.3.65.0.0(transitive)
- Removedminizlib@2.1.2(transitive)
- Removedmkdirp@1.0.4(transitive)
- Removedms@2.1.2(transitive)
- Removednode-fetch@2.7.0(transitive)
- Removednopt@5.0.0(transitive)
- Removednpmlog@5.0.1(transitive)
- Removedreadable-stream@3.6.2(transitive)
- Removedrimraf@3.0.2(transitive)
- Removedsemver@6.3.17.6.0(transitive)
- Removedstring-width@4.2.3(transitive)
- Removedstring_decoder@1.3.0(transitive)
- Removedstrip-ansi@6.0.1(transitive)
- Removedtar@6.2.1(transitive)
- Removedtr46@0.0.3(transitive)
- Removedwebidl-conversions@3.0.1(transitive)
- Removedwhatwg-url@5.0.0(transitive)
- Removedxml2js@0.4.23(transitive)
- Removedxmlbuilder@11.0.1(transitive)
- Removedxmldom@0.1.31(transitive)
- Removedyallist@4.0.0(transitive)
Updatedlibxmljs@0.19.7
Updatedxml-crypto@0.10.1
Updatedxml-encryption@0.11.2
Updatedxml2js@0.4.19
Updatedxmldom@0.1.27
Updatedxpath@0.0.27