optropic 2.0.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
@@ -31,9 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AssetsResource: () => AssetsResource,
34
+ AuditResource: () => AuditResource,
34
35
  AuthenticationError: () => AuthenticationError,
35
36
  BatchNotFoundError: () => BatchNotFoundError,
36
37
  CodeNotFoundError: () => CodeNotFoundError,
38
+ ComplianceResource: () => ComplianceResource,
39
+ DocumentsResource: () => DocumentsResource,
37
40
  InvalidCodeError: () => InvalidCodeError,
38
41
  InvalidGTINError: () => InvalidGTINError,
39
42
  InvalidSerialError: () => InvalidSerialError,
@@ -42,13 +45,22 @@ __export(index_exports, {
42
45
  NetworkError: () => NetworkError,
43
46
  OptropicClient: () => OptropicClient,
44
47
  OptropicError: () => OptropicError,
48
+ ProvenanceResource: () => ProvenanceResource,
45
49
  QuotaExceededError: () => QuotaExceededError,
46
50
  RateLimitedError: () => RateLimitedError,
47
51
  RevokedCodeError: () => RevokedCodeError,
48
52
  SDK_VERSION: () => SDK_VERSION2,
53
+ SchemasResource: () => SchemasResource,
49
54
  ServiceUnavailableError: () => ServiceUnavailableError,
55
+ StaleFilterError: () => StaleFilterError,
50
56
  TimeoutError: () => TimeoutError,
57
+ buildDPPConfig: () => buildDPPConfig,
51
58
  createClient: () => createClient,
59
+ createErrorFromResponse: () => createErrorFromResponse,
60
+ parseFilterHeader: () => parseFilterHeader,
61
+ parseSaltsHeader: () => parseSaltsHeader,
62
+ validateDPPMetadata: () => validateDPPMetadata,
63
+ verifyOffline: () => verifyOffline,
52
64
  verifyWebhookSignature: () => verifyWebhookSignature
53
65
  });
54
66
  module.exports = __toCommonJS(index_exports);
@@ -459,6 +471,31 @@ var AssetsResource = class {
459
471
  const query = effectiveParams ? this.buildQuery(effectiveParams) : "";
460
472
  return this.request({ method: "GET", path: `/v1/assets${query}` });
461
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
+ }
462
499
  async get(assetId) {
463
500
  return this.request({ method: "GET", path: `/v1/assets/${encodeURIComponent(assetId)}` });
464
501
  }
@@ -482,6 +519,209 @@ var AssetsResource = class {
482
519
  }
483
520
  };
484
521
 
522
+ // src/resources/audit.ts
523
+ var AuditResource = class {
524
+ constructor(request) {
525
+ this.request = request;
526
+ }
527
+ /**
528
+ * List audit events with optional filtering and pagination.
529
+ */
530
+ async list(params) {
531
+ const query = params ? this.buildQuery(params) : "";
532
+ return this.request({ method: "GET", path: `/v1/audit${query}` });
533
+ }
534
+ /**
535
+ * Retrieve a single audit event by ID.
536
+ */
537
+ async get(eventId) {
538
+ return this.request({
539
+ method: "GET",
540
+ path: `/v1/audit/${encodeURIComponent(eventId)}`
541
+ });
542
+ }
543
+ /**
544
+ * Record a custom audit event.
545
+ */
546
+ async create(params) {
547
+ return this.request({
548
+ method: "POST",
549
+ path: "/v1/audit",
550
+ body: {
551
+ event_type: params.eventType,
552
+ ...params.resourceId !== void 0 && { resource_id: params.resourceId },
553
+ ...params.resourceType !== void 0 && { resource_type: params.resourceType },
554
+ ...params.details !== void 0 && { details: params.details }
555
+ }
556
+ });
557
+ }
558
+ buildQuery(params) {
559
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
560
+ if (entries.length === 0) return "";
561
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
562
+ }
563
+ };
564
+
565
+ // src/resources/compliance.ts
566
+ var ComplianceResource = class {
567
+ constructor(request) {
568
+ this.request = request;
569
+ }
570
+ /**
571
+ * Verify the integrity of the full audit chain.
572
+ */
573
+ async verifyChain() {
574
+ return this.request({
575
+ method: "POST",
576
+ path: "/v1/compliance/verify-chain"
577
+ });
578
+ }
579
+ /**
580
+ * Return all Merkle roots.
581
+ */
582
+ async listMerkleRoots() {
583
+ return this.request({
584
+ method: "GET",
585
+ path: "/v1/compliance/merkle-roots"
586
+ });
587
+ }
588
+ /**
589
+ * Return a Merkle inclusion proof for a specific audit event.
590
+ */
591
+ async getMerkleProof(eventId) {
592
+ return this.request({
593
+ method: "GET",
594
+ path: `/v1/compliance/merkle-proof/${encodeURIComponent(eventId)}`
595
+ });
596
+ }
597
+ /**
598
+ * Export audit data as a signed CSV.
599
+ */
600
+ async exportAudit(params) {
601
+ const query = params ? this.buildQuery(params) : "";
602
+ return this.request({
603
+ method: "GET",
604
+ path: `/v1/compliance/export${query}`
605
+ });
606
+ }
607
+ /**
608
+ * Retrieve the current compliance configuration.
609
+ */
610
+ async getConfig() {
611
+ return this.request({
612
+ method: "GET",
613
+ path: "/v1/compliance/config"
614
+ });
615
+ }
616
+ /**
617
+ * Update the compliance mode.
618
+ */
619
+ async updateConfig(mode) {
620
+ return this.request({
621
+ method: "POST",
622
+ path: "/v1/compliance/config",
623
+ body: { compliance_mode: mode }
624
+ });
625
+ }
626
+ buildQuery(params) {
627
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
628
+ if (entries.length === 0) return "";
629
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
630
+ }
631
+ };
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
+
485
725
  // src/resources/keys.ts
486
726
  var KeysResource = class {
487
727
  constructor(request) {
@@ -518,10 +758,222 @@ var KeysetsResource = class {
518
758
  }
519
759
  };
520
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
+
849
+ // src/resources/schemas.ts
850
+ function checkType(value, expected) {
851
+ switch (expected) {
852
+ case "string":
853
+ return typeof value === "string";
854
+ case "number":
855
+ return typeof value === "number" && !Number.isNaN(value);
856
+ case "boolean":
857
+ return typeof value === "boolean";
858
+ case "date":
859
+ return typeof value === "string";
860
+ // ISO 8601 string
861
+ case "array":
862
+ return Array.isArray(value);
863
+ default:
864
+ return true;
865
+ }
866
+ }
867
+ var SchemasResource = class {
868
+ constructor(request) {
869
+ this.request = request;
870
+ }
871
+ /**
872
+ * Register or update a vertical config schema.
873
+ * If a schema already exists for the verticalId, it will be updated.
874
+ */
875
+ async create(params) {
876
+ const body = this.stripUndefined({
877
+ vertical_id: params.verticalId,
878
+ metadata_schema: params.metadataSchema,
879
+ version: params.version,
880
+ export_formats: params.exportFormats,
881
+ description: params.description
882
+ });
883
+ return this.request({ method: "POST", path: "/v1/schemas", body });
884
+ }
885
+ /**
886
+ * List registered vertical schemas with pagination.
887
+ */
888
+ async list(params) {
889
+ const query = params ? this.buildQuery(params) : "";
890
+ return this.request({ method: "GET", path: `/v1/schemas${query}` });
891
+ }
892
+ /**
893
+ * Get the active schema for a specific vertical.
894
+ */
895
+ async get(verticalId) {
896
+ return this.request({
897
+ method: "GET",
898
+ path: `/v1/schemas/${encodeURIComponent(verticalId)}`
899
+ });
900
+ }
901
+ /**
902
+ * Update an existing vertical schema.
903
+ */
904
+ async update(verticalId, params) {
905
+ const body = this.stripUndefined({
906
+ version: params.version,
907
+ metadata_schema: params.metadataSchema,
908
+ export_formats: params.exportFormats,
909
+ description: params.description,
910
+ is_active: params.isActive
911
+ });
912
+ return this.request({
913
+ method: "PUT",
914
+ path: `/v1/schemas/${encodeURIComponent(verticalId)}`,
915
+ body
916
+ });
917
+ }
918
+ /**
919
+ * Deactivate a vertical schema (soft delete).
920
+ */
921
+ async delete(verticalId) {
922
+ await this.request({
923
+ method: "DELETE",
924
+ path: `/v1/schemas/${encodeURIComponent(verticalId)}`
925
+ });
926
+ }
927
+ /**
928
+ * Pre-flight validation: check if assetConfig matches the registered schema.
929
+ *
930
+ * This is a client-side convenience that fetches the schema and validates locally.
931
+ * The server also validates on asset creation.
932
+ */
933
+ async validate(verticalId, assetConfig) {
934
+ let schema;
935
+ try {
936
+ schema = await this.get(verticalId);
937
+ } catch {
938
+ return { valid: true, errors: [] };
939
+ }
940
+ const errors = [];
941
+ const metadataSchema = schema.metadataSchema ?? {};
942
+ for (const [fieldName, fieldDef] of Object.entries(metadataSchema)) {
943
+ if (typeof fieldDef !== "object" || fieldDef === null) continue;
944
+ const def = fieldDef;
945
+ const value = assetConfig[fieldName];
946
+ if (def.required && (value === void 0 || value === null || value === "")) {
947
+ const label = def.label ?? fieldName;
948
+ errors.push({ field: fieldName, message: `Required field "${label}" is missing` });
949
+ continue;
950
+ }
951
+ if (value === void 0 || value === null) continue;
952
+ const expectedType = def.type ?? "string";
953
+ if (!checkType(value, expectedType)) {
954
+ errors.push({
955
+ field: fieldName,
956
+ message: `"${def.label ?? fieldName}" must be a ${expectedType}`,
957
+ received: typeof value
958
+ });
959
+ }
960
+ }
961
+ return { valid: errors.length === 0, errors };
962
+ }
963
+ buildQuery(params) {
964
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
965
+ if (entries.length === 0) return "";
966
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
967
+ }
968
+ stripUndefined(obj) {
969
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
970
+ }
971
+ };
972
+
521
973
  // src/client.ts
522
974
  var DEFAULT_BASE_URL = "https://api.optropic.com";
523
975
  var DEFAULT_TIMEOUT = 3e4;
524
- var SDK_VERSION = "2.0.0";
976
+ var SDK_VERSION = "2.2.0";
525
977
  var SANDBOX_PREFIXES = ["optr_test_"];
526
978
  var DEFAULT_RETRY_CONFIG = {
527
979
  maxRetries: 3,
@@ -534,8 +986,13 @@ var OptropicClient = class {
534
986
  retryConfig;
535
987
  _sandbox;
536
988
  assets;
989
+ audit;
990
+ compliance;
991
+ documents;
537
992
  keys;
538
993
  keysets;
994
+ provenance;
995
+ schemas;
539
996
  constructor(config) {
540
997
  if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
541
998
  throw new AuthenticationError(
@@ -562,8 +1019,13 @@ var OptropicClient = class {
562
1019
  };
563
1020
  const boundRequest = this.request.bind(this);
564
1021
  this.assets = new AssetsResource(boundRequest, this);
1022
+ this.audit = new AuditResource(boundRequest);
1023
+ this.compliance = new ComplianceResource(boundRequest);
1024
+ this.documents = new DocumentsResource(boundRequest);
565
1025
  this.keys = new KeysResource(boundRequest);
566
1026
  this.keysets = new KeysetsResource(boundRequest);
1027
+ this.provenance = new ProvenanceResource(boundRequest);
1028
+ this.schemas = new SchemasResource(boundRequest);
567
1029
  }
568
1030
  // ─────────────────────────────────────────────────────────────────────────
569
1031
  // ENVIRONMENT DETECTION
@@ -587,7 +1049,7 @@ var OptropicClient = class {
587
1049
  return /^optr_(live|test)_[a-zA-Z0-9_-]{20,}$/.test(apiKey);
588
1050
  }
589
1051
  async request(options) {
590
- const { method, path, body, headers = {}, timeout = this.config.timeout } = options;
1052
+ const { method, path, body, headers = {}, timeout = this.config.timeout, idempotencyKey } = options;
591
1053
  const url = `${this.baseUrl}${path}`;
592
1054
  const requestHeaders = {
593
1055
  "Content-Type": "application/json",
@@ -598,6 +1060,11 @@ var OptropicClient = class {
598
1060
  ...this.config.headers,
599
1061
  ...headers
600
1062
  };
1063
+ if (idempotencyKey) {
1064
+ requestHeaders["Idempotency-Key"] = idempotencyKey;
1065
+ } else if (["POST", "PUT", "PATCH"].includes(method)) {
1066
+ requestHeaders["Idempotency-Key"] = crypto.randomUUID();
1067
+ }
601
1068
  let lastError = null;
602
1069
  let attempt = 0;
603
1070
  while (attempt <= this.retryConfig.maxRetries) {
@@ -618,10 +1085,12 @@ var OptropicClient = class {
618
1085
  if (attempt >= this.retryConfig.maxRetries) {
619
1086
  throw error;
620
1087
  }
621
- const delay = Math.min(
1088
+ const baseDelay = Math.min(
622
1089
  this.retryConfig.baseDelay * Math.pow(2, attempt),
623
1090
  this.retryConfig.maxDelay
624
1091
  );
1092
+ const jitter = baseDelay * 0.5 * Math.random();
1093
+ const delay = baseDelay + jitter;
625
1094
  if (error instanceof RateLimitedError) {
626
1095
  await this.sleep(error.retryAfter * 1e3);
627
1096
  } else {
@@ -709,6 +1178,203 @@ function createClient(config) {
709
1178
  return new OptropicClient(config);
710
1179
  }
711
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
+
712
1378
  // src/webhooks.ts
713
1379
  async function computeHmacSha256(secret, message) {
714
1380
  const encoder = new TextEncoder();
@@ -755,13 +1421,16 @@ async function verifyWebhookSignature(options) {
755
1421
  }
756
1422
 
757
1423
  // src/index.ts
758
- var SDK_VERSION2 = "2.0.0";
1424
+ var SDK_VERSION2 = "2.2.0";
759
1425
  // Annotate the CommonJS export names for ESM import in node:
760
1426
  0 && (module.exports = {
761
1427
  AssetsResource,
1428
+ AuditResource,
762
1429
  AuthenticationError,
763
1430
  BatchNotFoundError,
764
1431
  CodeNotFoundError,
1432
+ ComplianceResource,
1433
+ DocumentsResource,
765
1434
  InvalidCodeError,
766
1435
  InvalidGTINError,
767
1436
  InvalidSerialError,
@@ -770,12 +1439,21 @@ var SDK_VERSION2 = "2.0.0";
770
1439
  NetworkError,
771
1440
  OptropicClient,
772
1441
  OptropicError,
1442
+ ProvenanceResource,
773
1443
  QuotaExceededError,
774
1444
  RateLimitedError,
775
1445
  RevokedCodeError,
776
1446
  SDK_VERSION,
1447
+ SchemasResource,
777
1448
  ServiceUnavailableError,
1449
+ StaleFilterError,
778
1450
  TimeoutError,
1451
+ buildDPPConfig,
779
1452
  createClient,
1453
+ createErrorFromResponse,
1454
+ parseFilterHeader,
1455
+ parseSaltsHeader,
1456
+ validateDPPMetadata,
1457
+ verifyOffline,
780
1458
  verifyWebhookSignature
781
1459
  });