optropic 2.1.0 → 2.2.0

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 CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  BatchNotFoundError: () => BatchNotFoundError,
37
37
  CodeNotFoundError: () => CodeNotFoundError,
38
38
  ComplianceResource: () => ComplianceResource,
39
+ DocumentsResource: () => DocumentsResource,
39
40
  InvalidCodeError: () => InvalidCodeError,
40
41
  InvalidGTINError: () => InvalidGTINError,
41
42
  InvalidSerialError: () => InvalidSerialError,
@@ -44,14 +45,22 @@ __export(index_exports, {
44
45
  NetworkError: () => NetworkError,
45
46
  OptropicClient: () => OptropicClient,
46
47
  OptropicError: () => OptropicError,
48
+ ProvenanceResource: () => ProvenanceResource,
47
49
  QuotaExceededError: () => QuotaExceededError,
48
50
  RateLimitedError: () => RateLimitedError,
49
51
  RevokedCodeError: () => RevokedCodeError,
50
52
  SDK_VERSION: () => SDK_VERSION2,
51
53
  SchemasResource: () => SchemasResource,
52
54
  ServiceUnavailableError: () => ServiceUnavailableError,
55
+ StaleFilterError: () => StaleFilterError,
53
56
  TimeoutError: () => TimeoutError,
57
+ buildDPPConfig: () => buildDPPConfig,
54
58
  createClient: () => createClient,
59
+ createErrorFromResponse: () => createErrorFromResponse,
60
+ parseFilterHeader: () => parseFilterHeader,
61
+ parseSaltsHeader: () => parseSaltsHeader,
62
+ validateDPPMetadata: () => validateDPPMetadata,
63
+ verifyOffline: () => verifyOffline,
55
64
  verifyWebhookSignature: () => verifyWebhookSignature
56
65
  });
57
66
  module.exports = __toCommonJS(index_exports);
@@ -462,6 +471,31 @@ var AssetsResource = class {
462
471
  const query = effectiveParams ? this.buildQuery(effectiveParams) : "";
463
472
  return this.request({ method: "GET", path: `/v1/assets${query}` });
464
473
  }
474
+ /**
475
+ * Auto-paginate through all assets, yielding pages of results.
476
+ * Returns an async generator that fetches pages on demand.
477
+ *
478
+ * @example
479
+ * ```typescript
480
+ * for await (const asset of client.assets.listAll({ status: 'active' })) {
481
+ * console.log(asset.id);
482
+ * }
483
+ * ```
484
+ */
485
+ async *listAll(params) {
486
+ let page = params?.page ?? 1;
487
+ const perPage = params?.per_page ?? 100;
488
+ while (true) {
489
+ const response = await this.list({ ...params, page, per_page: perPage });
490
+ for (const asset of response.data) {
491
+ yield asset;
492
+ }
493
+ if (page >= response.pagination.totalPages || response.data.length === 0) {
494
+ break;
495
+ }
496
+ page++;
497
+ }
498
+ }
465
499
  async get(assetId) {
466
500
  return this.request({ method: "GET", path: `/v1/assets/${encodeURIComponent(assetId)}` });
467
501
  }
@@ -596,6 +630,98 @@ var ComplianceResource = class {
596
630
  }
597
631
  };
598
632
 
633
+ // src/resources/documents.ts
634
+ var DocumentsResource = class {
635
+ constructor(request) {
636
+ this.request = request;
637
+ }
638
+ /**
639
+ * Enroll a new document (substrate fingerprint) linked to an asset.
640
+ *
641
+ * @example
642
+ * ```typescript
643
+ * const doc = await client.documents.enroll({
644
+ * assetId: 'asset-123',
645
+ * fingerprintHash: 'sha256:abc123...',
646
+ * descriptorVersion: 'GB_GE_M7PCA_v1',
647
+ * substrateType: 'S_fb',
648
+ * captureDevice: 'iPhone16ProMax_main',
649
+ * });
650
+ * ```
651
+ */
652
+ async enroll(params) {
653
+ return this.request({
654
+ method: "POST",
655
+ path: "/v1/documents",
656
+ body: {
657
+ asset_id: params.assetId,
658
+ fingerprint_hash: params.fingerprintHash,
659
+ descriptor_version: params.descriptorVersion,
660
+ substrate_type: params.substrateType,
661
+ ...params.captureDevice !== void 0 && { capture_device: params.captureDevice },
662
+ ...params.metadata !== void 0 && { metadata: params.metadata }
663
+ }
664
+ });
665
+ }
666
+ /**
667
+ * Verify a fingerprint against enrolled documents.
668
+ *
669
+ * Returns the best match if similarity exceeds the threshold.
670
+ */
671
+ async verify(params) {
672
+ return this.request({
673
+ method: "POST",
674
+ path: "/v1/documents/verify",
675
+ body: {
676
+ fingerprint_hash: params.fingerprintHash,
677
+ descriptor_version: params.descriptorVersion,
678
+ ...params.threshold !== void 0 && { threshold: params.threshold }
679
+ }
680
+ });
681
+ }
682
+ /**
683
+ * Get a single document by ID.
684
+ */
685
+ async get(documentId) {
686
+ return this.request({
687
+ method: "GET",
688
+ path: `/v1/documents/${encodeURIComponent(documentId)}`
689
+ });
690
+ }
691
+ /**
692
+ * List enrolled documents with optional filtering.
693
+ */
694
+ async list(params) {
695
+ const query = params ? this.buildQuery(params) : "";
696
+ return this.request({
697
+ method: "GET",
698
+ path: `/v1/documents${query}`
699
+ });
700
+ }
701
+ /**
702
+ * Supersede a document (e.g., re-enrollment with better capture).
703
+ */
704
+ async supersede(documentId, newDocumentId) {
705
+ return this.request({
706
+ method: "POST",
707
+ path: `/v1/documents/${encodeURIComponent(documentId)}/supersede`,
708
+ body: { new_document_id: newDocumentId }
709
+ });
710
+ }
711
+ buildQuery(params) {
712
+ const mapped = {
713
+ asset_id: params.assetId,
714
+ substrate_type: params.substrateType,
715
+ status: params.status,
716
+ page: params.page,
717
+ per_page: params.per_page
718
+ };
719
+ const entries = Object.entries(mapped).filter(([, v]) => v !== void 0);
720
+ if (entries.length === 0) return "";
721
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
722
+ }
723
+ };
724
+
599
725
  // src/resources/keys.ts
600
726
  var KeysResource = class {
601
727
  constructor(request) {
@@ -632,6 +758,94 @@ var KeysetsResource = class {
632
758
  }
633
759
  };
634
760
 
761
+ // src/resources/provenance.ts
762
+ var ProvenanceResource = class {
763
+ constructor(request) {
764
+ this.request = request;
765
+ }
766
+ /**
767
+ * Record a new provenance event in the chain.
768
+ *
769
+ * Events are automatically chained — the server links each new event
770
+ * to the previous one via cryptographic hash.
771
+ *
772
+ * @example
773
+ * ```typescript
774
+ * const event = await client.provenance.record({
775
+ * assetId: 'asset-123',
776
+ * eventType: 'manufactured',
777
+ * actor: 'factory-line-7',
778
+ * location: { country: 'DE', facility: 'Munich Plant' },
779
+ * });
780
+ * ```
781
+ */
782
+ async record(params) {
783
+ return this.request({
784
+ method: "POST",
785
+ path: "/v1/provenance",
786
+ body: {
787
+ asset_id: params.assetId,
788
+ event_type: params.eventType,
789
+ actor: params.actor,
790
+ ...params.location !== void 0 && { location: params.location },
791
+ ...params.metadata !== void 0 && { metadata: params.metadata }
792
+ }
793
+ });
794
+ }
795
+ /**
796
+ * Get the full provenance chain for an asset.
797
+ */
798
+ async getChain(assetId) {
799
+ return this.request({
800
+ method: "GET",
801
+ path: `/v1/provenance/chain/${encodeURIComponent(assetId)}`
802
+ });
803
+ }
804
+ /**
805
+ * Get a single provenance event by ID.
806
+ */
807
+ async get(eventId) {
808
+ return this.request({
809
+ method: "GET",
810
+ path: `/v1/provenance/${encodeURIComponent(eventId)}`
811
+ });
812
+ }
813
+ /**
814
+ * List provenance events with optional filtering.
815
+ */
816
+ async list(params) {
817
+ const query = params ? this.buildQuery(params) : "";
818
+ return this.request({
819
+ method: "GET",
820
+ path: `/v1/provenance${query}`
821
+ });
822
+ }
823
+ /**
824
+ * Verify the integrity of an asset's provenance chain.
825
+ *
826
+ * Checks that all events are correctly linked via cryptographic hashes.
827
+ */
828
+ async verifyChain(assetId) {
829
+ return this.request({
830
+ method: "POST",
831
+ path: `/v1/provenance/chain/${encodeURIComponent(assetId)}/verify`
832
+ });
833
+ }
834
+ buildQuery(params) {
835
+ const mapped = {
836
+ asset_id: params.assetId,
837
+ event_type: params.eventType,
838
+ from_date: params.from_date,
839
+ to_date: params.to_date,
840
+ page: params.page,
841
+ per_page: params.per_page
842
+ };
843
+ const entries = Object.entries(mapped).filter(([, v]) => v !== void 0);
844
+ if (entries.length === 0) return "";
845
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
846
+ }
847
+ };
848
+
635
849
  // src/resources/schemas.ts
636
850
  function checkType(value, expected) {
637
851
  switch (expected) {
@@ -759,7 +973,7 @@ var SchemasResource = class {
759
973
  // src/client.ts
760
974
  var DEFAULT_BASE_URL = "https://api.optropic.com";
761
975
  var DEFAULT_TIMEOUT = 3e4;
762
- var SDK_VERSION = "2.0.0";
976
+ var SDK_VERSION = "2.2.0";
763
977
  var SANDBOX_PREFIXES = ["optr_test_"];
764
978
  var DEFAULT_RETRY_CONFIG = {
765
979
  maxRetries: 3,
@@ -774,8 +988,10 @@ var OptropicClient = class {
774
988
  assets;
775
989
  audit;
776
990
  compliance;
991
+ documents;
777
992
  keys;
778
993
  keysets;
994
+ provenance;
779
995
  schemas;
780
996
  constructor(config) {
781
997
  if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
@@ -805,8 +1021,10 @@ var OptropicClient = class {
805
1021
  this.assets = new AssetsResource(boundRequest, this);
806
1022
  this.audit = new AuditResource(boundRequest);
807
1023
  this.compliance = new ComplianceResource(boundRequest);
1024
+ this.documents = new DocumentsResource(boundRequest);
808
1025
  this.keys = new KeysResource(boundRequest);
809
1026
  this.keysets = new KeysetsResource(boundRequest);
1027
+ this.provenance = new ProvenanceResource(boundRequest);
810
1028
  this.schemas = new SchemasResource(boundRequest);
811
1029
  }
812
1030
  // ─────────────────────────────────────────────────────────────────────────
@@ -831,7 +1049,7 @@ var OptropicClient = class {
831
1049
  return /^optr_(live|test)_[a-zA-Z0-9_-]{20,}$/.test(apiKey);
832
1050
  }
833
1051
  async request(options) {
834
- const { method, path, body, headers = {}, timeout = this.config.timeout } = options;
1052
+ const { method, path, body, headers = {}, timeout = this.config.timeout, idempotencyKey } = options;
835
1053
  const url = `${this.baseUrl}${path}`;
836
1054
  const requestHeaders = {
837
1055
  "Content-Type": "application/json",
@@ -842,6 +1060,11 @@ var OptropicClient = class {
842
1060
  ...this.config.headers,
843
1061
  ...headers
844
1062
  };
1063
+ if (idempotencyKey) {
1064
+ requestHeaders["Idempotency-Key"] = idempotencyKey;
1065
+ } else if (["POST", "PUT", "PATCH"].includes(method)) {
1066
+ requestHeaders["Idempotency-Key"] = crypto.randomUUID();
1067
+ }
845
1068
  let lastError = null;
846
1069
  let attempt = 0;
847
1070
  while (attempt <= this.retryConfig.maxRetries) {
@@ -862,10 +1085,12 @@ var OptropicClient = class {
862
1085
  if (attempt >= this.retryConfig.maxRetries) {
863
1086
  throw error;
864
1087
  }
865
- const delay = Math.min(
1088
+ const baseDelay = Math.min(
866
1089
  this.retryConfig.baseDelay * Math.pow(2, attempt),
867
1090
  this.retryConfig.maxDelay
868
1091
  );
1092
+ const jitter = baseDelay * 0.5 * Math.random();
1093
+ const delay = baseDelay + jitter;
869
1094
  if (error instanceof RateLimitedError) {
870
1095
  await this.sleep(error.retryAfter * 1e3);
871
1096
  } else {
@@ -953,6 +1178,203 @@ function createClient(config) {
953
1178
  return new OptropicClient(config);
954
1179
  }
955
1180
 
1181
+ // src/filter-verify.ts
1182
+ var HEADER_SIZE = 19;
1183
+ var SIGNATURE_SIZE = 64;
1184
+ var SLOTS_PER_BUCKET = 4;
1185
+ var StaleFilterError = class extends Error {
1186
+ code = "FILTER_STALE_CRITICAL";
1187
+ ageSeconds;
1188
+ trustWindowSeconds;
1189
+ constructor(ageSeconds, trustWindowSeconds) {
1190
+ super(`Filter age ${ageSeconds}s exceeds trust window ${trustWindowSeconds}s`);
1191
+ this.name = "StaleFilterError";
1192
+ this.ageSeconds = ageSeconds;
1193
+ this.trustWindowSeconds = trustWindowSeconds;
1194
+ }
1195
+ };
1196
+ async function sha256(data) {
1197
+ const buf = new Uint8Array(data).buffer;
1198
+ const hash = await globalThis.crypto.subtle.digest("SHA-256", buf);
1199
+ return new Uint8Array(hash);
1200
+ }
1201
+ async function sha256Hex(input) {
1202
+ const encoded = new TextEncoder().encode(input);
1203
+ const hash = await sha256(encoded);
1204
+ return Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
1205
+ }
1206
+ function uint32BE(buf, offset) {
1207
+ return (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
1208
+ }
1209
+ function uint16BE(buf, offset) {
1210
+ return buf[offset] << 8 | buf[offset + 1];
1211
+ }
1212
+ function int64BE(buf, offset) {
1213
+ const high = (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
1214
+ const low = (buf[offset + 4] << 24 | buf[offset + 5] << 16 | buf[offset + 6] << 8 | buf[offset + 7]) >>> 0;
1215
+ return high * 4294967296 + low;
1216
+ }
1217
+ async function fingerprint(item) {
1218
+ const encoded = new TextEncoder().encode(item);
1219
+ let digest = await sha256(encoded);
1220
+ let fp = uint16BE(digest, 4);
1221
+ while (fp === 0) {
1222
+ digest = await sha256(digest);
1223
+ fp = uint16BE(digest, 4);
1224
+ }
1225
+ return fp;
1226
+ }
1227
+ async function h1(item, capacity) {
1228
+ const encoded = new TextEncoder().encode(item);
1229
+ const digest = await sha256(encoded);
1230
+ return uint32BE(digest, 0) % capacity;
1231
+ }
1232
+ async function altIndex(index, fp, capacity) {
1233
+ const fpBuf = new Uint8Array(2);
1234
+ fpBuf[0] = fp >> 8 & 255;
1235
+ fpBuf[1] = fp & 255;
1236
+ const fpDigest = await sha256(fpBuf);
1237
+ return ((index ^ uint32BE(fpDigest, 0) % capacity) & 4294967295) >>> 0;
1238
+ }
1239
+ async function filterLookup(filterData, capacity, item) {
1240
+ const fp = await fingerprint(item);
1241
+ const i1 = await h1(item, capacity);
1242
+ const i2 = await altIndex(i1, fp, capacity);
1243
+ const b1Offset = i1 * SLOTS_PER_BUCKET * 2;
1244
+ for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
1245
+ const slotVal = uint16BE(filterData, b1Offset + s * 2);
1246
+ if (slotVal === fp) return true;
1247
+ }
1248
+ const b2Offset = i2 * SLOTS_PER_BUCKET * 2;
1249
+ for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
1250
+ const slotVal = uint16BE(filterData, b2Offset + s * 2);
1251
+ if (slotVal === fp) return true;
1252
+ }
1253
+ return false;
1254
+ }
1255
+ function parseFilterHeader(buf) {
1256
+ if (buf.length < HEADER_SIZE) {
1257
+ throw new Error("Buffer too small for filter header");
1258
+ }
1259
+ return {
1260
+ version: buf[0],
1261
+ issuedAt: int64BE(buf, 1),
1262
+ itemCount: uint32BE(buf, 9),
1263
+ keyId: uint16BE(buf, 13),
1264
+ capacity: uint32BE(buf, 15)
1265
+ };
1266
+ }
1267
+ function parseSaltsHeader(header) {
1268
+ const salts = /* @__PURE__ */ new Map();
1269
+ if (!header) return salts;
1270
+ for (const pair of header.split(",")) {
1271
+ const colonIdx = pair.indexOf(":");
1272
+ if (colonIdx === -1) continue;
1273
+ const tenantId = pair.slice(0, colonIdx).trim();
1274
+ const saltHex = pair.slice(colonIdx + 1).trim();
1275
+ if (tenantId && saltHex) {
1276
+ const bytes = new Uint8Array(saltHex.length / 2);
1277
+ for (let i = 0; i < bytes.length; i++) {
1278
+ bytes[i] = parseInt(saltHex.slice(i * 2, i * 2 + 2), 16);
1279
+ }
1280
+ salts.set(tenantId, bytes);
1281
+ }
1282
+ }
1283
+ return salts;
1284
+ }
1285
+ function toHex(bytes) {
1286
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1287
+ }
1288
+ async function verifyOffline(options) {
1289
+ const {
1290
+ assetId,
1291
+ filterBytes,
1292
+ salts,
1293
+ filterPolicy = "permissive",
1294
+ trustWindowSeconds = 259200
1295
+ // 72 hours
1296
+ } = options;
1297
+ const header = parseFilterHeader(filterBytes);
1298
+ const nowSeconds = Math.floor(Date.now() / 1e3);
1299
+ const ageSeconds = nowSeconds - header.issuedAt;
1300
+ if (ageSeconds > trustWindowSeconds) {
1301
+ if (filterPolicy === "strict") {
1302
+ throw new StaleFilterError(ageSeconds, trustWindowSeconds);
1303
+ }
1304
+ }
1305
+ const filterDataEnd = filterBytes.length - SIGNATURE_SIZE;
1306
+ const filterData = filterBytes.slice(HEADER_SIZE, filterDataEnd);
1307
+ const capacity = header.capacity;
1308
+ let revoked = false;
1309
+ for (const salt of salts.values()) {
1310
+ const saltHex = toHex(salt);
1311
+ const saltedInput = assetId + saltHex;
1312
+ const saltedHash = await sha256Hex(saltedInput);
1313
+ if (await filterLookup(filterData, capacity, saltedHash)) {
1314
+ revoked = true;
1315
+ break;
1316
+ }
1317
+ }
1318
+ const freshness = ageSeconds > trustWindowSeconds ? "stale" : "current";
1319
+ return {
1320
+ signatureValid: true,
1321
+ revocationStatus: revoked ? "revoked" : "clear",
1322
+ filterAgeSeconds: ageSeconds,
1323
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
1324
+ verificationMode: "offline",
1325
+ freshness
1326
+ };
1327
+ }
1328
+
1329
+ // src/dpp.ts
1330
+ function buildDPPConfig(metadata) {
1331
+ const config = {
1332
+ product_id: metadata.productId,
1333
+ product_name: metadata.productName,
1334
+ manufacturer: metadata.manufacturer,
1335
+ country_of_origin: metadata.countryOfOrigin,
1336
+ dpp_category: metadata.category
1337
+ };
1338
+ if (metadata.carbonFootprint !== void 0) config.carbon_footprint_kg_co2e = metadata.carbonFootprint;
1339
+ if (metadata.recycledContent !== void 0) config.recycled_content_percent = metadata.recycledContent;
1340
+ if (metadata.durabilityYears !== void 0) config.durability_years = metadata.durabilityYears;
1341
+ if (metadata.repairabilityScore !== void 0) config.repairability_score = metadata.repairabilityScore;
1342
+ if (metadata.substancesOfConcern) config.substances_of_concern = metadata.substancesOfConcern;
1343
+ if (metadata.dppRegistryId) config.dpp_registry_id = metadata.dppRegistryId;
1344
+ if (metadata.conformityDeclarations) config.conformity_declarations = metadata.conformityDeclarations;
1345
+ if (metadata.sectorData) config.sector_data = metadata.sectorData;
1346
+ return config;
1347
+ }
1348
+ function validateDPPMetadata(metadata) {
1349
+ const errors = [];
1350
+ if (!metadata.productId) errors.push("productId is required");
1351
+ if (!metadata.productName) errors.push("productName is required");
1352
+ if (!metadata.manufacturer) errors.push("manufacturer is required");
1353
+ if (!metadata.countryOfOrigin) errors.push("countryOfOrigin is required");
1354
+ if (metadata.countryOfOrigin && !/^[A-Z]{2}$/.test(metadata.countryOfOrigin)) {
1355
+ errors.push('countryOfOrigin must be ISO 3166-1 alpha-2 (e.g., "DE")');
1356
+ }
1357
+ if (metadata.recycledContent !== void 0 && (metadata.recycledContent < 0 || metadata.recycledContent > 100)) {
1358
+ errors.push("recycledContent must be between 0 and 100");
1359
+ }
1360
+ if (metadata.category === "battery" && metadata.sectorData) {
1361
+ const battery = metadata.sectorData;
1362
+ if (battery.type === "battery") {
1363
+ if (!battery.chemistry) errors.push("battery.chemistry is required for battery passports");
1364
+ if (!battery.capacityKwh) errors.push("battery.capacityKwh is required for battery passports");
1365
+ }
1366
+ }
1367
+ if (metadata.category === "textile" && metadata.sectorData) {
1368
+ const textile = metadata.sectorData;
1369
+ if (textile.type === "textile") {
1370
+ if (!textile.fiberComposition || textile.fiberComposition.length === 0) {
1371
+ errors.push("textile.fiberComposition is required for textile passports");
1372
+ }
1373
+ }
1374
+ }
1375
+ return { valid: errors.length === 0, errors };
1376
+ }
1377
+
956
1378
  // src/webhooks.ts
957
1379
  async function computeHmacSha256(secret, message) {
958
1380
  const encoder = new TextEncoder();
@@ -999,7 +1421,7 @@ async function verifyWebhookSignature(options) {
999
1421
  }
1000
1422
 
1001
1423
  // src/index.ts
1002
- var SDK_VERSION2 = "2.0.0";
1424
+ var SDK_VERSION2 = "2.2.0";
1003
1425
  // Annotate the CommonJS export names for ESM import in node:
1004
1426
  0 && (module.exports = {
1005
1427
  AssetsResource,
@@ -1008,6 +1430,7 @@ var SDK_VERSION2 = "2.0.0";
1008
1430
  BatchNotFoundError,
1009
1431
  CodeNotFoundError,
1010
1432
  ComplianceResource,
1433
+ DocumentsResource,
1011
1434
  InvalidCodeError,
1012
1435
  InvalidGTINError,
1013
1436
  InvalidSerialError,
@@ -1016,13 +1439,21 @@ var SDK_VERSION2 = "2.0.0";
1016
1439
  NetworkError,
1017
1440
  OptropicClient,
1018
1441
  OptropicError,
1442
+ ProvenanceResource,
1019
1443
  QuotaExceededError,
1020
1444
  RateLimitedError,
1021
1445
  RevokedCodeError,
1022
1446
  SDK_VERSION,
1023
1447
  SchemasResource,
1024
1448
  ServiceUnavailableError,
1449
+ StaleFilterError,
1025
1450
  TimeoutError,
1451
+ buildDPPConfig,
1026
1452
  createClient,
1453
+ createErrorFromResponse,
1454
+ parseFilterHeader,
1455
+ parseSaltsHeader,
1456
+ validateDPPMetadata,
1457
+ verifyOffline,
1027
1458
  verifyWebhookSignature
1028
1459
  });