@deuriib/dgii-ecf 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +213 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -2
- package/dist/index.d.ts +88 -2
- package/dist/index.js +208 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,8 +31,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
BaseUrl: () => BaseUrl,
|
|
34
|
+
CONSUMPTION_SUMMARY_THRESHOLD: () => CONSUMPTION_SUMMARY_THRESHOLD,
|
|
34
35
|
DgiiError: () => DgiiError,
|
|
35
36
|
ECF: () => ECF,
|
|
37
|
+
ECF_TYPE_CODES: () => ECF_TYPE_CODES,
|
|
36
38
|
Endpoint: () => Endpoint,
|
|
37
39
|
Environment: () => Environment,
|
|
38
40
|
Err: () => import_neverthrow.Err,
|
|
@@ -47,6 +49,7 @@ __export(index_exports, {
|
|
|
47
49
|
Transformer: () => Transformer,
|
|
48
50
|
buildSenderReceiverUrl: () => buildSenderReceiverUrl,
|
|
49
51
|
buildUrl: () => buildUrl,
|
|
52
|
+
compareDecimal: () => compareDecimal,
|
|
50
53
|
convertECF32ToRFCE: () => convertECF32ToRFCE,
|
|
51
54
|
createResiliencePolicies: () => createResiliencePolicies,
|
|
52
55
|
defaultResilienceOptions: () => defaultResilienceOptions,
|
|
@@ -62,10 +65,12 @@ __export(index_exports, {
|
|
|
62
65
|
getCurrentFormattedDate: () => getCurrentFormattedDate,
|
|
63
66
|
getCurrentFormattedDateTime: () => getCurrentFormattedDateTime,
|
|
64
67
|
getXmlFromBodyResponse: () => getXmlFromBodyResponse,
|
|
68
|
+
isEcfTypeCode: () => isEcfTypeCode,
|
|
65
69
|
isRetryableError: () => isRetryableError,
|
|
66
70
|
isRetryableStatus: () => isRetryableStatus,
|
|
67
71
|
ok: () => import_neverthrow.ok,
|
|
68
72
|
okAsync: () => import_neverthrow.okAsync,
|
|
73
|
+
resolveReceptionRoute: () => resolveReceptionRoute,
|
|
69
74
|
roundHalfUpToCents: () => roundHalfUpToCents,
|
|
70
75
|
toDgiiError: () => toDgiiError,
|
|
71
76
|
validateXMLCertificate: () => validateXMLCertificate
|
|
@@ -141,9 +146,10 @@ var readMensajes = (value) => {
|
|
|
141
146
|
if (typeof item === "string" && item.trim()) {
|
|
142
147
|
out.push({ valor: item.trim(), codigo: 0 });
|
|
143
148
|
} else if (isObject(item) && typeof item.valor === "string") {
|
|
149
|
+
const codigo = item.codigo;
|
|
144
150
|
out.push({
|
|
145
151
|
valor: item.valor,
|
|
146
|
-
codigo: typeof
|
|
152
|
+
codigo: typeof codigo === "number" || typeof codigo === "string" ? codigo : 0
|
|
147
153
|
});
|
|
148
154
|
}
|
|
149
155
|
}
|
|
@@ -422,9 +428,13 @@ var HttpClient = class {
|
|
|
422
428
|
policies;
|
|
423
429
|
fetchImpl;
|
|
424
430
|
baseUrl;
|
|
431
|
+
beforeRequest;
|
|
432
|
+
onUnauthorized;
|
|
425
433
|
constructor(options) {
|
|
426
434
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
427
435
|
this.token = options.token;
|
|
436
|
+
this.beforeRequest = options.beforeRequest;
|
|
437
|
+
this.onUnauthorized = options.onUnauthorized;
|
|
428
438
|
this.policies = options.policies ?? createResiliencePolicies({
|
|
429
439
|
...options.resilience,
|
|
430
440
|
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {}
|
|
@@ -434,6 +444,9 @@ var HttpClient = class {
|
|
|
434
444
|
setToken(token) {
|
|
435
445
|
this.token = token;
|
|
436
446
|
}
|
|
447
|
+
getToken() {
|
|
448
|
+
return this.token;
|
|
449
|
+
}
|
|
437
450
|
clearToken() {
|
|
438
451
|
this.token = void 0;
|
|
439
452
|
}
|
|
@@ -493,6 +506,19 @@ var HttpClient = class {
|
|
|
493
506
|
return (0, import_neverthrow.err)(error);
|
|
494
507
|
}
|
|
495
508
|
async request(options) {
|
|
509
|
+
if (!options.skipAuthHooks) {
|
|
510
|
+
await this.beforeRequest?.();
|
|
511
|
+
}
|
|
512
|
+
const first = await this.execute(options);
|
|
513
|
+
if (!options.skipAuthHooks && first.isErr() && first.error.status === 401 && this.onUnauthorized !== void 0) {
|
|
514
|
+
const replayed = await this.onUnauthorized();
|
|
515
|
+
if (replayed) {
|
|
516
|
+
return this.execute(options);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return first;
|
|
520
|
+
}
|
|
521
|
+
async execute(options) {
|
|
496
522
|
const url = this.buildUrl(options.path, options.absoluteUrl, options.query);
|
|
497
523
|
const method = options.method ?? "GET";
|
|
498
524
|
const headers = this.authHeaders(options.headers);
|
|
@@ -521,7 +547,8 @@ var HttpClient = class {
|
|
|
521
547
|
path,
|
|
522
548
|
query,
|
|
523
549
|
absoluteUrl: opts?.absoluteUrl,
|
|
524
|
-
resource: opts?.resource
|
|
550
|
+
resource: opts?.resource,
|
|
551
|
+
skipAuthHooks: opts?.skipAuthHooks
|
|
525
552
|
});
|
|
526
553
|
}
|
|
527
554
|
/**
|
|
@@ -537,7 +564,8 @@ var HttpClient = class {
|
|
|
537
564
|
body: form,
|
|
538
565
|
headers: { Accept: "application/json" },
|
|
539
566
|
absoluteUrl: opts?.absoluteUrl,
|
|
540
|
-
resource: opts?.resource
|
|
567
|
+
resource: opts?.resource,
|
|
568
|
+
skipAuthHooks: opts?.skipAuthHooks
|
|
541
569
|
});
|
|
542
570
|
}
|
|
543
571
|
};
|
|
@@ -885,6 +913,98 @@ var extractElementText = (xml, tag) => {
|
|
|
885
913
|
const text = match?.[1]?.trim();
|
|
886
914
|
return text === void 0 || text === "" ? void 0 : text;
|
|
887
915
|
};
|
|
916
|
+
var ROOT_PATTERN = /<\s*(?:[A-Za-z_][\w.-]*:)?([A-Za-z_][\w.-]*)[\s/>]/;
|
|
917
|
+
var extractRootElementName = (xml) => {
|
|
918
|
+
const stripped = xml.replace(/<\?[\s\S]*?\?>/g, "").replace(/<!--[\s\S]*?-->/g, "").trimStart();
|
|
919
|
+
return ROOT_PATTERN.exec(stripped)?.[1];
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
// src/reception.ts
|
|
923
|
+
var ECF_TYPE_CODES = [
|
|
924
|
+
"31",
|
|
925
|
+
"32",
|
|
926
|
+
"33",
|
|
927
|
+
"34",
|
|
928
|
+
"41",
|
|
929
|
+
"43",
|
|
930
|
+
"44",
|
|
931
|
+
"45",
|
|
932
|
+
"46",
|
|
933
|
+
"47"
|
|
934
|
+
];
|
|
935
|
+
var CONSUMPTION_SUMMARY_THRESHOLD = "250000";
|
|
936
|
+
var CONSUMPTION_TYPE = "32";
|
|
937
|
+
var GENERAL = {
|
|
938
|
+
kind: "general",
|
|
939
|
+
endpoint: "recepcion/api/FacturasElectronicas" /* SendInvoice */
|
|
940
|
+
};
|
|
941
|
+
var SUMMARY = {
|
|
942
|
+
kind: "summary",
|
|
943
|
+
endpoint: "recepcionfc/api/recepcion/ecf" /* SendSummary */
|
|
944
|
+
};
|
|
945
|
+
var isEcfTypeCode = (value) => ECF_TYPE_CODES.includes(value);
|
|
946
|
+
var DECIMAL_PATTERN = /^\d+(?:\.\d+)?$/;
|
|
947
|
+
var compareDecimal = (a, b) => {
|
|
948
|
+
const [aInt = "", aFrac = ""] = a.split(".");
|
|
949
|
+
const [bInt = "", bFrac = ""] = b.split(".");
|
|
950
|
+
const aWhole = aInt.replace(/^0+(?=\d)/, "");
|
|
951
|
+
const bWhole = bInt.replace(/^0+(?=\d)/, "");
|
|
952
|
+
if (aWhole.length !== bWhole.length) {
|
|
953
|
+
return aWhole.length - bWhole.length;
|
|
954
|
+
}
|
|
955
|
+
if (aWhole !== bWhole) {
|
|
956
|
+
return aWhole < bWhole ? -1 : 1;
|
|
957
|
+
}
|
|
958
|
+
const width = Math.max(aFrac.length, bFrac.length);
|
|
959
|
+
const aPadded = aFrac.padEnd(width, "0");
|
|
960
|
+
const bPadded = bFrac.padEnd(width, "0");
|
|
961
|
+
if (aPadded === bPadded) {
|
|
962
|
+
return 0;
|
|
963
|
+
}
|
|
964
|
+
return aPadded < bPadded ? -1 : 1;
|
|
965
|
+
};
|
|
966
|
+
var validationError = (message) => new DgiiError(message, { code: "VALIDATION" });
|
|
967
|
+
var resolveReceptionRoute = (signedXml) => {
|
|
968
|
+
const root = extractRootElementName(signedXml);
|
|
969
|
+
if (root === "RFCE") {
|
|
970
|
+
return (0, import_neverthrow.ok)(SUMMARY);
|
|
971
|
+
}
|
|
972
|
+
if (root !== "ECF") {
|
|
973
|
+
return (0, import_neverthrow.err)(
|
|
974
|
+
validationError(
|
|
975
|
+
`Unsupported document root "${root ?? "unknown"}"; expected "ECF" or "RFCE"`
|
|
976
|
+
)
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
const tipo = extractElementText(signedXml, "TipoeCF");
|
|
980
|
+
if (tipo === void 0) {
|
|
981
|
+
return (0, import_neverthrow.err)(validationError("TipoeCF is missing from the document"));
|
|
982
|
+
}
|
|
983
|
+
if (!isEcfTypeCode(tipo)) {
|
|
984
|
+
return (0, import_neverthrow.err)(validationError(`Unknown TipoeCF "${tipo}"`));
|
|
985
|
+
}
|
|
986
|
+
if (tipo !== CONSUMPTION_TYPE) {
|
|
987
|
+
return (0, import_neverthrow.ok)(GENERAL);
|
|
988
|
+
}
|
|
989
|
+
const total = extractElementText(signedXml, "MontoTotal");
|
|
990
|
+
if (total === void 0) {
|
|
991
|
+
return (0, import_neverthrow.err)(
|
|
992
|
+
validationError(
|
|
993
|
+
"MontoTotal is required to route an E32 consumption invoice"
|
|
994
|
+
)
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
if (!DECIMAL_PATTERN.test(total)) {
|
|
998
|
+
return (0, import_neverthrow.err)(
|
|
999
|
+
validationError(`MontoTotal "${total}" is not a decimal amount`)
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
return compareDecimal(total, CONSUMPTION_SUMMARY_THRESHOLD) < 0 ? (0, import_neverthrow.err)(
|
|
1003
|
+
validationError(
|
|
1004
|
+
"An E32 below RD$250,000 must be sent as an RFCE summary; convert it with convertECF32ToRFCE first"
|
|
1005
|
+
)
|
|
1006
|
+
) : (0, import_neverthrow.ok)(GENERAL);
|
|
1007
|
+
};
|
|
888
1008
|
|
|
889
1009
|
// src/utils.ts
|
|
890
1010
|
var getCodeSixDigitfromSignature = (signedXml) => {
|
|
@@ -1048,22 +1168,39 @@ var import_node_crypto2 = require("crypto");
|
|
|
1048
1168
|
var PENDING_SUBMISSION = { pending: true };
|
|
1049
1169
|
var isPendingSubmission = (value) => typeof value === "object" && value !== null && value.pending === true;
|
|
1050
1170
|
var sha256Hex = (value) => (0, import_node_crypto2.createHash)("sha256").update(value, "utf8").digest("hex");
|
|
1171
|
+
var parseExpiry = (value) => {
|
|
1172
|
+
if (value === void 0) {
|
|
1173
|
+
return void 0;
|
|
1174
|
+
}
|
|
1175
|
+
const parsed = Date.parse(value);
|
|
1176
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1177
|
+
};
|
|
1051
1178
|
var ECF = class {
|
|
1052
1179
|
environment;
|
|
1053
1180
|
http;
|
|
1054
1181
|
idempotency;
|
|
1055
1182
|
credentials;
|
|
1056
1183
|
inFlight = /* @__PURE__ */ new Map();
|
|
1184
|
+
tokenRefreshSkewMs;
|
|
1185
|
+
now;
|
|
1186
|
+
tokenExpiresAt;
|
|
1187
|
+
refreshPromise;
|
|
1057
1188
|
constructor(credentials, environment = "TesteCF" /* Dev */, options = {}) {
|
|
1058
1189
|
this.credentials = credentials;
|
|
1059
1190
|
this.environment = environment;
|
|
1191
|
+
this.tokenRefreshSkewMs = options.tokenRefreshSkewMs ?? 5 * 60 * 1e3;
|
|
1192
|
+
this.now = options.now ?? Date.now;
|
|
1060
1193
|
this.idempotency = options.idempotencyStore ?? new InMemoryIdempotencyStore();
|
|
1061
1194
|
this.http = new HttpClient({
|
|
1062
1195
|
baseUrl: BaseUrl.ECF,
|
|
1063
1196
|
...options.token !== void 0 ? { token: options.token } : {},
|
|
1064
1197
|
...options.resilience !== void 0 ? { resilience: options.resilience } : {},
|
|
1065
1198
|
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
1066
|
-
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
1199
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
1200
|
+
beforeRequest: async () => {
|
|
1201
|
+
await this.ensureToken();
|
|
1202
|
+
},
|
|
1203
|
+
onUnauthorized: () => this.reauthenticate()
|
|
1067
1204
|
});
|
|
1068
1205
|
}
|
|
1069
1206
|
buildIdempotencyKey(signedXml) {
|
|
@@ -1115,6 +1252,40 @@ var ECF = class {
|
|
|
1115
1252
|
this.inFlight.delete(key);
|
|
1116
1253
|
}
|
|
1117
1254
|
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Refresh the token when it is missing or about to expire.
|
|
1257
|
+
*
|
|
1258
|
+
* A pre-supplied `token` has no known expiry, so it is never refreshed
|
|
1259
|
+
* proactively — only a 401 triggers a re-authentication for it.
|
|
1260
|
+
*/
|
|
1261
|
+
async ensureToken() {
|
|
1262
|
+
if (this.tokenExpiresAt === void 0) {
|
|
1263
|
+
return (0, import_neverthrow.ok)(void 0);
|
|
1264
|
+
}
|
|
1265
|
+
if (this.now() < this.tokenExpiresAt - this.tokenRefreshSkewMs) {
|
|
1266
|
+
return (0, import_neverthrow.ok)(void 0);
|
|
1267
|
+
}
|
|
1268
|
+
const refreshed = await this.refreshToken();
|
|
1269
|
+
return refreshed.isOk() ? (0, import_neverthrow.ok)(void 0) : (0, import_neverthrow.err)(refreshed.error);
|
|
1270
|
+
}
|
|
1271
|
+
/** Authenticate, coalescing concurrent refreshes into a single round-trip. */
|
|
1272
|
+
refreshToken() {
|
|
1273
|
+
if (this.refreshPromise !== void 0) {
|
|
1274
|
+
return this.refreshPromise;
|
|
1275
|
+
}
|
|
1276
|
+
const promise = this.authenticate().finally(() => {
|
|
1277
|
+
this.refreshPromise = void 0;
|
|
1278
|
+
});
|
|
1279
|
+
this.refreshPromise = promise;
|
|
1280
|
+
return promise;
|
|
1281
|
+
}
|
|
1282
|
+
/** Drop the rejected token and authenticate once. Returns `true` on success. */
|
|
1283
|
+
async reauthenticate() {
|
|
1284
|
+
this.http.clearToken();
|
|
1285
|
+
this.tokenExpiresAt = void 0;
|
|
1286
|
+
const refreshed = await this.refreshToken();
|
|
1287
|
+
return refreshed.isOk();
|
|
1288
|
+
}
|
|
1118
1289
|
/** Authenticate against DGII (or a buyer host) and store the bearer token. */
|
|
1119
1290
|
async authenticate(buyerHost) {
|
|
1120
1291
|
const seed = await this.getSeed(buyerHost);
|
|
@@ -1141,6 +1312,7 @@ var ECF = class {
|
|
|
1141
1312
|
return (0, import_neverthrow.err)(token.error);
|
|
1142
1313
|
}
|
|
1143
1314
|
this.http.setToken(token.value.token);
|
|
1315
|
+
this.tokenExpiresAt = parseExpiry(token.value.expira);
|
|
1144
1316
|
return (0, import_neverthrow.ok)(token.value);
|
|
1145
1317
|
}
|
|
1146
1318
|
getSeed(buyerHost) {
|
|
@@ -1151,20 +1323,23 @@ var ECF = class {
|
|
|
1151
1323
|
);
|
|
1152
1324
|
return this.http.get(url2, void 0, {
|
|
1153
1325
|
absoluteUrl: true,
|
|
1154
|
-
resource: "fe/autenticacion/api/semilla" /* Seed
|
|
1326
|
+
resource: "fe/autenticacion/api/semilla" /* Seed */,
|
|
1327
|
+
skipAuthHooks: true
|
|
1155
1328
|
});
|
|
1156
1329
|
}
|
|
1157
1330
|
const url = buildUrl(this.environment, "Autenticacion/api/Autenticacion/Semilla" /* Seed */);
|
|
1158
1331
|
return this.http.get(url, void 0, {
|
|
1159
1332
|
absoluteUrl: true,
|
|
1160
|
-
resource: "Autenticacion/api/Autenticacion/Semilla" /* Seed
|
|
1333
|
+
resource: "Autenticacion/api/Autenticacion/Semilla" /* Seed */,
|
|
1334
|
+
skipAuthHooks: true
|
|
1161
1335
|
});
|
|
1162
1336
|
}
|
|
1163
1337
|
getAuthToken(signedSeed, buyerHost) {
|
|
1164
1338
|
const url = buyerHost ? buildSenderReceiverUrl(buyerHost, "fe/autenticacion/api/validacioncertificado" /* ValidateSeed */) : buildUrl(this.environment, "autenticacion/api/Autenticacion/ValidarSemilla" /* ValidateSeed */);
|
|
1165
1339
|
return this.http.postMultipart(url, "signed.xml", signedSeed, {
|
|
1166
1340
|
absoluteUrl: true,
|
|
1167
|
-
resource: "autenticacion/api/Autenticacion/ValidarSemilla" /* ValidateSeed
|
|
1341
|
+
resource: "autenticacion/api/Autenticacion/ValidarSemilla" /* ValidateSeed */,
|
|
1342
|
+
skipAuthHooks: true
|
|
1168
1343
|
});
|
|
1169
1344
|
}
|
|
1170
1345
|
/**
|
|
@@ -1196,6 +1371,32 @@ var ECF = class {
|
|
|
1196
1371
|
)
|
|
1197
1372
|
);
|
|
1198
1373
|
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Submit a signed document to the endpoint its own content belongs to.
|
|
1376
|
+
*
|
|
1377
|
+
* Reads the root element, `TipoeCF` and `MontoTotal` from the signed XML and
|
|
1378
|
+
* dispatches to {@link sendSummary} for an RFCE or to
|
|
1379
|
+
* {@link sendElectronicDocument} for an ECF. An E32 below RD$250,000 is
|
|
1380
|
+
* rejected because DGII only accepts the summary form for those.
|
|
1381
|
+
*/
|
|
1382
|
+
async sendDocument(signedXml, fileName, buyerHost) {
|
|
1383
|
+
const route = resolveReceptionRoute(signedXml);
|
|
1384
|
+
if (route.isErr()) {
|
|
1385
|
+
return (0, import_neverthrow.err)(route.error);
|
|
1386
|
+
}
|
|
1387
|
+
if (route.value.kind === "summary") {
|
|
1388
|
+
if (buyerHost !== void 0) {
|
|
1389
|
+
return (0, import_neverthrow.err)(
|
|
1390
|
+
new DgiiError(
|
|
1391
|
+
"An RFCE summary cannot be delivered directly to a buyer host",
|
|
1392
|
+
{ code: "VALIDATION" }
|
|
1393
|
+
)
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
return this.sendSummary(signedXml, fileName);
|
|
1397
|
+
}
|
|
1398
|
+
return this.sendElectronicDocument(signedXml, fileName, buyerHost);
|
|
1399
|
+
}
|
|
1199
1400
|
statusTrackId(trackId) {
|
|
1200
1401
|
const url = buildUrl(this.environment, "consultaresultado/api/Consultas/Estado" /* TrackResultStatus */);
|
|
1201
1402
|
return this.http.get(
|
|
@@ -1290,8 +1491,10 @@ var ECF = class {
|
|
|
1290
1491
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1291
1492
|
0 && (module.exports = {
|
|
1292
1493
|
BaseUrl,
|
|
1494
|
+
CONSUMPTION_SUMMARY_THRESHOLD,
|
|
1293
1495
|
DgiiError,
|
|
1294
1496
|
ECF,
|
|
1497
|
+
ECF_TYPE_CODES,
|
|
1295
1498
|
Endpoint,
|
|
1296
1499
|
Environment,
|
|
1297
1500
|
Err,
|
|
@@ -1306,6 +1509,7 @@ var ECF = class {
|
|
|
1306
1509
|
Transformer,
|
|
1307
1510
|
buildSenderReceiverUrl,
|
|
1308
1511
|
buildUrl,
|
|
1512
|
+
compareDecimal,
|
|
1309
1513
|
convertECF32ToRFCE,
|
|
1310
1514
|
createResiliencePolicies,
|
|
1311
1515
|
defaultResilienceOptions,
|
|
@@ -1321,10 +1525,12 @@ var ECF = class {
|
|
|
1321
1525
|
getCurrentFormattedDate,
|
|
1322
1526
|
getCurrentFormattedDateTime,
|
|
1323
1527
|
getXmlFromBodyResponse,
|
|
1528
|
+
isEcfTypeCode,
|
|
1324
1529
|
isRetryableError,
|
|
1325
1530
|
isRetryableStatus,
|
|
1326
1531
|
ok,
|
|
1327
1532
|
okAsync,
|
|
1533
|
+
resolveReceptionRoute,
|
|
1328
1534
|
roundHalfUpToCents,
|
|
1329
1535
|
toDgiiError,
|
|
1330
1536
|
validateXMLCertificate
|