optropic 2.1.0 → 2.3.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,23 +973,27 @@ 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.3.0";
763
977
  var SANDBOX_PREFIXES = ["optr_test_"];
764
978
  var DEFAULT_RETRY_CONFIG = {
765
979
  maxRetries: 3,
766
980
  baseDelay: 1e3,
767
981
  maxDelay: 1e4
768
982
  };
983
+ var KEY_REDACT_RE = /(optr_(?:live|test)_)[a-zA-Z0-9_-]+/g;
769
984
  var OptropicClient = class {
770
985
  config;
771
986
  baseUrl;
772
987
  retryConfig;
773
988
  _sandbox;
989
+ _debug;
774
990
  assets;
775
991
  audit;
776
992
  compliance;
993
+ documents;
777
994
  keys;
778
995
  keysets;
996
+ provenance;
779
997
  schemas;
780
998
  constructor(config) {
781
999
  if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
@@ -787,6 +1005,7 @@ var OptropicClient = class {
787
1005
  ...config,
788
1006
  timeout: config.timeout ?? DEFAULT_TIMEOUT
789
1007
  };
1008
+ this._debug = config.debug ?? false;
790
1009
  if (config.sandbox !== void 0) {
791
1010
  this._sandbox = config.sandbox;
792
1011
  } else {
@@ -805,8 +1024,10 @@ var OptropicClient = class {
805
1024
  this.assets = new AssetsResource(boundRequest, this);
806
1025
  this.audit = new AuditResource(boundRequest);
807
1026
  this.compliance = new ComplianceResource(boundRequest);
1027
+ this.documents = new DocumentsResource(boundRequest);
808
1028
  this.keys = new KeysResource(boundRequest);
809
1029
  this.keysets = new KeysetsResource(boundRequest);
1030
+ this.provenance = new ProvenanceResource(boundRequest);
810
1031
  this.schemas = new SchemasResource(boundRequest);
811
1032
  }
812
1033
  // ─────────────────────────────────────────────────────────────────────────
@@ -825,23 +1046,59 @@ var OptropicClient = class {
825
1046
  return this._sandbox ? "sandbox" : "live";
826
1047
  }
827
1048
  // ─────────────────────────────────────────────────────────────────────────
1049
+ // DEBUG LOGGING
1050
+ // ─────────────────────────────────────────────────────────────────────────
1051
+ redact(text) {
1052
+ return text.replace(KEY_REDACT_RE, "$1****");
1053
+ }
1054
+ logRequest(method, url, requestId, idempotencyKey) {
1055
+ if (!this._debug) return;
1056
+ const parts = [`${method} ${this.redact(url)}`, `req=${requestId}`];
1057
+ if (idempotencyKey) parts.push(`idempotency=${idempotencyKey}`);
1058
+ console.log(`[optropic] ${parts.join(" ")}`);
1059
+ }
1060
+ logResponse(method, path, status, durationMs, requestId, serverRequestId, attempt) {
1061
+ if (!this._debug) return;
1062
+ const parts = [`${method} ${path} \u2192 ${status} (${Math.round(durationMs)}ms)`, `req=${requestId}`];
1063
+ if (serverRequestId) parts.push(`server_req=${serverRequestId}`);
1064
+ if (attempt > 0) parts.push(`attempt=${attempt + 1}`);
1065
+ console.log(`[optropic] ${parts.join(" ")}`);
1066
+ }
1067
+ logRetry(method, path, attempt, delay, reason) {
1068
+ if (!this._debug) return;
1069
+ console.log(
1070
+ `[optropic] ${method} ${path} retry #${attempt + 1} in ${(delay / 1e3).toFixed(1)}s (${reason})`
1071
+ );
1072
+ }
1073
+ // ─────────────────────────────────────────────────────────────────────────
828
1074
  // PRIVATE METHODS
829
1075
  // ─────────────────────────────────────────────────────────────────────────
830
1076
  isValidApiKey(apiKey) {
831
1077
  return /^optr_(live|test)_[a-zA-Z0-9_-]{20,}$/.test(apiKey);
832
1078
  }
833
1079
  async request(options) {
834
- const { method, path, body, headers = {}, timeout = this.config.timeout } = options;
1080
+ const { method, path, body, headers = {}, timeout = this.config.timeout, idempotencyKey } = options;
835
1081
  const url = `${this.baseUrl}${path}`;
1082
+ const requestId = crypto.randomUUID();
836
1083
  const requestHeaders = {
837
1084
  "Content-Type": "application/json",
838
1085
  "Accept": "application/json",
839
1086
  "x-api-key": this.config.apiKey,
840
1087
  "X-SDK-Version": SDK_VERSION,
841
1088
  "X-SDK-Language": "typescript",
1089
+ "X-Request-ID": requestId,
842
1090
  ...this.config.headers,
843
1091
  ...headers
844
1092
  };
1093
+ let effectiveIdempotencyKey;
1094
+ if (idempotencyKey) {
1095
+ requestHeaders["Idempotency-Key"] = idempotencyKey;
1096
+ effectiveIdempotencyKey = idempotencyKey;
1097
+ } else if (["POST", "PUT", "PATCH"].includes(method)) {
1098
+ effectiveIdempotencyKey = crypto.randomUUID();
1099
+ requestHeaders["Idempotency-Key"] = effectiveIdempotencyKey;
1100
+ }
1101
+ this.logRequest(method, url, requestId, effectiveIdempotencyKey);
845
1102
  let lastError = null;
846
1103
  let attempt = 0;
847
1104
  while (attempt <= this.retryConfig.maxRetries) {
@@ -851,7 +1108,10 @@ var OptropicClient = class {
851
1108
  method,
852
1109
  requestHeaders,
853
1110
  body,
854
- timeout
1111
+ timeout,
1112
+ requestId,
1113
+ path,
1114
+ attempt
855
1115
  );
856
1116
  return response;
857
1117
  } catch (error) {
@@ -862,13 +1122,19 @@ var OptropicClient = class {
862
1122
  if (attempt >= this.retryConfig.maxRetries) {
863
1123
  throw error;
864
1124
  }
865
- const delay = Math.min(
1125
+ const baseDelay = Math.min(
866
1126
  this.retryConfig.baseDelay * Math.pow(2, attempt),
867
1127
  this.retryConfig.maxDelay
868
1128
  );
1129
+ const jitter = baseDelay * 0.5 * Math.random();
1130
+ const delay = baseDelay + jitter;
869
1131
  if (error instanceof RateLimitedError) {
870
- await this.sleep(error.retryAfter * 1e3);
1132
+ const retryDelay = error.retryAfter * 1e3;
1133
+ this.logRetry(method, path, attempt, retryDelay, "rate_limited");
1134
+ await this.sleep(retryDelay);
871
1135
  } else {
1136
+ const statusCode = error instanceof OptropicError ? error.statusCode : 0;
1137
+ this.logRetry(method, path, attempt, delay, `status=${statusCode}`);
872
1138
  await this.sleep(delay);
873
1139
  }
874
1140
  attempt++;
@@ -876,9 +1142,10 @@ var OptropicClient = class {
876
1142
  }
877
1143
  throw lastError ?? new OptropicError("UNKNOWN_ERROR", "Request failed");
878
1144
  }
879
- async executeRequest(url, method, headers, body, timeout) {
1145
+ async executeRequest(url, method, headers, body, timeout, requestId, path, attempt) {
880
1146
  const controller = new AbortController();
881
1147
  const timeoutId = setTimeout(() => controller.abort(), timeout);
1148
+ const t0 = performance.now();
882
1149
  try {
883
1150
  const response = await fetch(url, {
884
1151
  method,
@@ -887,7 +1154,9 @@ var OptropicClient = class {
887
1154
  signal: controller.signal
888
1155
  });
889
1156
  clearTimeout(timeoutId);
890
- const requestId = response.headers.get("x-request-id") ?? "";
1157
+ const durationMs = performance.now() - t0;
1158
+ const serverRequestId = response.headers.get("x-request-id") ?? "";
1159
+ this.logResponse(method, path, response.status, durationMs, requestId, serverRequestId, attempt);
891
1160
  if (!response.ok) {
892
1161
  let errorBody;
893
1162
  try {
@@ -910,7 +1179,7 @@ var OptropicClient = class {
910
1179
  code: errorBody.code,
911
1180
  message: errorBody.message,
912
1181
  details: errorBody.details,
913
- requestId
1182
+ requestId: serverRequestId || requestId
914
1183
  });
915
1184
  }
916
1185
  if (response.status === 204) {
@@ -936,8 +1205,12 @@ var OptropicClient = class {
936
1205
  }
937
1206
  if (error instanceof Error) {
938
1207
  if (error.name === "AbortError") {
1208
+ const durationMs2 = performance.now() - t0;
1209
+ this.logResponse(method, path, 408, durationMs2, requestId, "", attempt);
939
1210
  throw new TimeoutError(timeout);
940
1211
  }
1212
+ const durationMs = performance.now() - t0;
1213
+ this.logResponse(method, path, 0, durationMs, requestId, "", attempt);
941
1214
  throw new NetworkError(error.message, { cause: error });
942
1215
  }
943
1216
  throw new OptropicError("UNKNOWN_ERROR", "An unexpected error occurred", {
@@ -953,6 +1226,203 @@ function createClient(config) {
953
1226
  return new OptropicClient(config);
954
1227
  }
955
1228
 
1229
+ // src/filter-verify.ts
1230
+ var HEADER_SIZE = 19;
1231
+ var SIGNATURE_SIZE = 64;
1232
+ var SLOTS_PER_BUCKET = 4;
1233
+ var StaleFilterError = class extends Error {
1234
+ code = "FILTER_STALE_CRITICAL";
1235
+ ageSeconds;
1236
+ trustWindowSeconds;
1237
+ constructor(ageSeconds, trustWindowSeconds) {
1238
+ super(`Filter age ${ageSeconds}s exceeds trust window ${trustWindowSeconds}s`);
1239
+ this.name = "StaleFilterError";
1240
+ this.ageSeconds = ageSeconds;
1241
+ this.trustWindowSeconds = trustWindowSeconds;
1242
+ }
1243
+ };
1244
+ async function sha256(data) {
1245
+ const buf = new Uint8Array(data).buffer;
1246
+ const hash = await globalThis.crypto.subtle.digest("SHA-256", buf);
1247
+ return new Uint8Array(hash);
1248
+ }
1249
+ async function sha256Hex(input) {
1250
+ const encoded = new TextEncoder().encode(input);
1251
+ const hash = await sha256(encoded);
1252
+ return Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
1253
+ }
1254
+ function uint32BE(buf, offset) {
1255
+ return (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
1256
+ }
1257
+ function uint16BE(buf, offset) {
1258
+ return buf[offset] << 8 | buf[offset + 1];
1259
+ }
1260
+ function int64BE(buf, offset) {
1261
+ const high = (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
1262
+ const low = (buf[offset + 4] << 24 | buf[offset + 5] << 16 | buf[offset + 6] << 8 | buf[offset + 7]) >>> 0;
1263
+ return high * 4294967296 + low;
1264
+ }
1265
+ async function fingerprint(item) {
1266
+ const encoded = new TextEncoder().encode(item);
1267
+ let digest = await sha256(encoded);
1268
+ let fp = uint16BE(digest, 4);
1269
+ while (fp === 0) {
1270
+ digest = await sha256(digest);
1271
+ fp = uint16BE(digest, 4);
1272
+ }
1273
+ return fp;
1274
+ }
1275
+ async function h1(item, capacity) {
1276
+ const encoded = new TextEncoder().encode(item);
1277
+ const digest = await sha256(encoded);
1278
+ return uint32BE(digest, 0) % capacity;
1279
+ }
1280
+ async function altIndex(index, fp, capacity) {
1281
+ const fpBuf = new Uint8Array(2);
1282
+ fpBuf[0] = fp >> 8 & 255;
1283
+ fpBuf[1] = fp & 255;
1284
+ const fpDigest = await sha256(fpBuf);
1285
+ return ((index ^ uint32BE(fpDigest, 0) % capacity) & 4294967295) >>> 0;
1286
+ }
1287
+ async function filterLookup(filterData, capacity, item) {
1288
+ const fp = await fingerprint(item);
1289
+ const i1 = await h1(item, capacity);
1290
+ const i2 = await altIndex(i1, fp, capacity);
1291
+ const b1Offset = i1 * SLOTS_PER_BUCKET * 2;
1292
+ for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
1293
+ const slotVal = uint16BE(filterData, b1Offset + s * 2);
1294
+ if (slotVal === fp) return true;
1295
+ }
1296
+ const b2Offset = i2 * SLOTS_PER_BUCKET * 2;
1297
+ for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
1298
+ const slotVal = uint16BE(filterData, b2Offset + s * 2);
1299
+ if (slotVal === fp) return true;
1300
+ }
1301
+ return false;
1302
+ }
1303
+ function parseFilterHeader(buf) {
1304
+ if (buf.length < HEADER_SIZE) {
1305
+ throw new Error("Buffer too small for filter header");
1306
+ }
1307
+ return {
1308
+ version: buf[0],
1309
+ issuedAt: int64BE(buf, 1),
1310
+ itemCount: uint32BE(buf, 9),
1311
+ keyId: uint16BE(buf, 13),
1312
+ capacity: uint32BE(buf, 15)
1313
+ };
1314
+ }
1315
+ function parseSaltsHeader(header) {
1316
+ const salts = /* @__PURE__ */ new Map();
1317
+ if (!header) return salts;
1318
+ for (const pair of header.split(",")) {
1319
+ const colonIdx = pair.indexOf(":");
1320
+ if (colonIdx === -1) continue;
1321
+ const tenantId = pair.slice(0, colonIdx).trim();
1322
+ const saltHex = pair.slice(colonIdx + 1).trim();
1323
+ if (tenantId && saltHex) {
1324
+ const bytes = new Uint8Array(saltHex.length / 2);
1325
+ for (let i = 0; i < bytes.length; i++) {
1326
+ bytes[i] = parseInt(saltHex.slice(i * 2, i * 2 + 2), 16);
1327
+ }
1328
+ salts.set(tenantId, bytes);
1329
+ }
1330
+ }
1331
+ return salts;
1332
+ }
1333
+ function toHex(bytes) {
1334
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1335
+ }
1336
+ async function verifyOffline(options) {
1337
+ const {
1338
+ assetId,
1339
+ filterBytes,
1340
+ salts,
1341
+ filterPolicy = "permissive",
1342
+ trustWindowSeconds = 259200
1343
+ // 72 hours
1344
+ } = options;
1345
+ const header = parseFilterHeader(filterBytes);
1346
+ const nowSeconds = Math.floor(Date.now() / 1e3);
1347
+ const ageSeconds = nowSeconds - header.issuedAt;
1348
+ if (ageSeconds > trustWindowSeconds) {
1349
+ if (filterPolicy === "strict") {
1350
+ throw new StaleFilterError(ageSeconds, trustWindowSeconds);
1351
+ }
1352
+ }
1353
+ const filterDataEnd = filterBytes.length - SIGNATURE_SIZE;
1354
+ const filterData = filterBytes.slice(HEADER_SIZE, filterDataEnd);
1355
+ const capacity = header.capacity;
1356
+ let revoked = false;
1357
+ for (const salt of salts.values()) {
1358
+ const saltHex = toHex(salt);
1359
+ const saltedInput = assetId + saltHex;
1360
+ const saltedHash = await sha256Hex(saltedInput);
1361
+ if (await filterLookup(filterData, capacity, saltedHash)) {
1362
+ revoked = true;
1363
+ break;
1364
+ }
1365
+ }
1366
+ const freshness = ageSeconds > trustWindowSeconds ? "stale" : "current";
1367
+ return {
1368
+ signatureValid: true,
1369
+ revocationStatus: revoked ? "revoked" : "clear",
1370
+ filterAgeSeconds: ageSeconds,
1371
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
1372
+ verificationMode: "offline",
1373
+ freshness
1374
+ };
1375
+ }
1376
+
1377
+ // src/dpp.ts
1378
+ function buildDPPConfig(metadata) {
1379
+ const config = {
1380
+ product_id: metadata.productId,
1381
+ product_name: metadata.productName,
1382
+ manufacturer: metadata.manufacturer,
1383
+ country_of_origin: metadata.countryOfOrigin,
1384
+ dpp_category: metadata.category
1385
+ };
1386
+ if (metadata.carbonFootprint !== void 0) config.carbon_footprint_kg_co2e = metadata.carbonFootprint;
1387
+ if (metadata.recycledContent !== void 0) config.recycled_content_percent = metadata.recycledContent;
1388
+ if (metadata.durabilityYears !== void 0) config.durability_years = metadata.durabilityYears;
1389
+ if (metadata.repairabilityScore !== void 0) config.repairability_score = metadata.repairabilityScore;
1390
+ if (metadata.substancesOfConcern) config.substances_of_concern = metadata.substancesOfConcern;
1391
+ if (metadata.dppRegistryId) config.dpp_registry_id = metadata.dppRegistryId;
1392
+ if (metadata.conformityDeclarations) config.conformity_declarations = metadata.conformityDeclarations;
1393
+ if (metadata.sectorData) config.sector_data = metadata.sectorData;
1394
+ return config;
1395
+ }
1396
+ function validateDPPMetadata(metadata) {
1397
+ const errors = [];
1398
+ if (!metadata.productId) errors.push("productId is required");
1399
+ if (!metadata.productName) errors.push("productName is required");
1400
+ if (!metadata.manufacturer) errors.push("manufacturer is required");
1401
+ if (!metadata.countryOfOrigin) errors.push("countryOfOrigin is required");
1402
+ if (metadata.countryOfOrigin && !/^[A-Z]{2}$/.test(metadata.countryOfOrigin)) {
1403
+ errors.push('countryOfOrigin must be ISO 3166-1 alpha-2 (e.g., "DE")');
1404
+ }
1405
+ if (metadata.recycledContent !== void 0 && (metadata.recycledContent < 0 || metadata.recycledContent > 100)) {
1406
+ errors.push("recycledContent must be between 0 and 100");
1407
+ }
1408
+ if (metadata.category === "battery" && metadata.sectorData) {
1409
+ const battery = metadata.sectorData;
1410
+ if (battery.type === "battery") {
1411
+ if (!battery.chemistry) errors.push("battery.chemistry is required for battery passports");
1412
+ if (!battery.capacityKwh) errors.push("battery.capacityKwh is required for battery passports");
1413
+ }
1414
+ }
1415
+ if (metadata.category === "textile" && metadata.sectorData) {
1416
+ const textile = metadata.sectorData;
1417
+ if (textile.type === "textile") {
1418
+ if (!textile.fiberComposition || textile.fiberComposition.length === 0) {
1419
+ errors.push("textile.fiberComposition is required for textile passports");
1420
+ }
1421
+ }
1422
+ }
1423
+ return { valid: errors.length === 0, errors };
1424
+ }
1425
+
956
1426
  // src/webhooks.ts
957
1427
  async function computeHmacSha256(secret, message) {
958
1428
  const encoder = new TextEncoder();
@@ -999,7 +1469,7 @@ async function verifyWebhookSignature(options) {
999
1469
  }
1000
1470
 
1001
1471
  // src/index.ts
1002
- var SDK_VERSION2 = "2.0.0";
1472
+ var SDK_VERSION2 = "2.3.0";
1003
1473
  // Annotate the CommonJS export names for ESM import in node:
1004
1474
  0 && (module.exports = {
1005
1475
  AssetsResource,
@@ -1008,6 +1478,7 @@ var SDK_VERSION2 = "2.0.0";
1008
1478
  BatchNotFoundError,
1009
1479
  CodeNotFoundError,
1010
1480
  ComplianceResource,
1481
+ DocumentsResource,
1011
1482
  InvalidCodeError,
1012
1483
  InvalidGTINError,
1013
1484
  InvalidSerialError,
@@ -1016,13 +1487,21 @@ var SDK_VERSION2 = "2.0.0";
1016
1487
  NetworkError,
1017
1488
  OptropicClient,
1018
1489
  OptropicError,
1490
+ ProvenanceResource,
1019
1491
  QuotaExceededError,
1020
1492
  RateLimitedError,
1021
1493
  RevokedCodeError,
1022
1494
  SDK_VERSION,
1023
1495
  SchemasResource,
1024
1496
  ServiceUnavailableError,
1497
+ StaleFilterError,
1025
1498
  TimeoutError,
1499
+ buildDPPConfig,
1026
1500
  createClient,
1501
+ createErrorFromResponse,
1502
+ parseFilterHeader,
1503
+ parseSaltsHeader,
1504
+ validateDPPMetadata,
1505
+ verifyOffline,
1027
1506
  verifyWebhookSignature
1028
1507
  });