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.js CHANGED
@@ -404,6 +404,31 @@ var AssetsResource = class {
404
404
  const query = effectiveParams ? this.buildQuery(effectiveParams) : "";
405
405
  return this.request({ method: "GET", path: `/v1/assets${query}` });
406
406
  }
407
+ /**
408
+ * Auto-paginate through all assets, yielding pages of results.
409
+ * Returns an async generator that fetches pages on demand.
410
+ *
411
+ * @example
412
+ * ```typescript
413
+ * for await (const asset of client.assets.listAll({ status: 'active' })) {
414
+ * console.log(asset.id);
415
+ * }
416
+ * ```
417
+ */
418
+ async *listAll(params) {
419
+ let page = params?.page ?? 1;
420
+ const perPage = params?.per_page ?? 100;
421
+ while (true) {
422
+ const response = await this.list({ ...params, page, per_page: perPage });
423
+ for (const asset of response.data) {
424
+ yield asset;
425
+ }
426
+ if (page >= response.pagination.totalPages || response.data.length === 0) {
427
+ break;
428
+ }
429
+ page++;
430
+ }
431
+ }
407
432
  async get(assetId) {
408
433
  return this.request({ method: "GET", path: `/v1/assets/${encodeURIComponent(assetId)}` });
409
434
  }
@@ -538,6 +563,98 @@ var ComplianceResource = class {
538
563
  }
539
564
  };
540
565
 
566
+ // src/resources/documents.ts
567
+ var DocumentsResource = class {
568
+ constructor(request) {
569
+ this.request = request;
570
+ }
571
+ /**
572
+ * Enroll a new document (substrate fingerprint) linked to an asset.
573
+ *
574
+ * @example
575
+ * ```typescript
576
+ * const doc = await client.documents.enroll({
577
+ * assetId: 'asset-123',
578
+ * fingerprintHash: 'sha256:abc123...',
579
+ * descriptorVersion: 'GB_GE_M7PCA_v1',
580
+ * substrateType: 'S_fb',
581
+ * captureDevice: 'iPhone16ProMax_main',
582
+ * });
583
+ * ```
584
+ */
585
+ async enroll(params) {
586
+ return this.request({
587
+ method: "POST",
588
+ path: "/v1/documents",
589
+ body: {
590
+ asset_id: params.assetId,
591
+ fingerprint_hash: params.fingerprintHash,
592
+ descriptor_version: params.descriptorVersion,
593
+ substrate_type: params.substrateType,
594
+ ...params.captureDevice !== void 0 && { capture_device: params.captureDevice },
595
+ ...params.metadata !== void 0 && { metadata: params.metadata }
596
+ }
597
+ });
598
+ }
599
+ /**
600
+ * Verify a fingerprint against enrolled documents.
601
+ *
602
+ * Returns the best match if similarity exceeds the threshold.
603
+ */
604
+ async verify(params) {
605
+ return this.request({
606
+ method: "POST",
607
+ path: "/v1/documents/verify",
608
+ body: {
609
+ fingerprint_hash: params.fingerprintHash,
610
+ descriptor_version: params.descriptorVersion,
611
+ ...params.threshold !== void 0 && { threshold: params.threshold }
612
+ }
613
+ });
614
+ }
615
+ /**
616
+ * Get a single document by ID.
617
+ */
618
+ async get(documentId) {
619
+ return this.request({
620
+ method: "GET",
621
+ path: `/v1/documents/${encodeURIComponent(documentId)}`
622
+ });
623
+ }
624
+ /**
625
+ * List enrolled documents with optional filtering.
626
+ */
627
+ async list(params) {
628
+ const query = params ? this.buildQuery(params) : "";
629
+ return this.request({
630
+ method: "GET",
631
+ path: `/v1/documents${query}`
632
+ });
633
+ }
634
+ /**
635
+ * Supersede a document (e.g., re-enrollment with better capture).
636
+ */
637
+ async supersede(documentId, newDocumentId) {
638
+ return this.request({
639
+ method: "POST",
640
+ path: `/v1/documents/${encodeURIComponent(documentId)}/supersede`,
641
+ body: { new_document_id: newDocumentId }
642
+ });
643
+ }
644
+ buildQuery(params) {
645
+ const mapped = {
646
+ asset_id: params.assetId,
647
+ substrate_type: params.substrateType,
648
+ status: params.status,
649
+ page: params.page,
650
+ per_page: params.per_page
651
+ };
652
+ const entries = Object.entries(mapped).filter(([, v]) => v !== void 0);
653
+ if (entries.length === 0) return "";
654
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
655
+ }
656
+ };
657
+
541
658
  // src/resources/keys.ts
542
659
  var KeysResource = class {
543
660
  constructor(request) {
@@ -574,6 +691,94 @@ var KeysetsResource = class {
574
691
  }
575
692
  };
576
693
 
694
+ // src/resources/provenance.ts
695
+ var ProvenanceResource = class {
696
+ constructor(request) {
697
+ this.request = request;
698
+ }
699
+ /**
700
+ * Record a new provenance event in the chain.
701
+ *
702
+ * Events are automatically chained — the server links each new event
703
+ * to the previous one via cryptographic hash.
704
+ *
705
+ * @example
706
+ * ```typescript
707
+ * const event = await client.provenance.record({
708
+ * assetId: 'asset-123',
709
+ * eventType: 'manufactured',
710
+ * actor: 'factory-line-7',
711
+ * location: { country: 'DE', facility: 'Munich Plant' },
712
+ * });
713
+ * ```
714
+ */
715
+ async record(params) {
716
+ return this.request({
717
+ method: "POST",
718
+ path: "/v1/provenance",
719
+ body: {
720
+ asset_id: params.assetId,
721
+ event_type: params.eventType,
722
+ actor: params.actor,
723
+ ...params.location !== void 0 && { location: params.location },
724
+ ...params.metadata !== void 0 && { metadata: params.metadata }
725
+ }
726
+ });
727
+ }
728
+ /**
729
+ * Get the full provenance chain for an asset.
730
+ */
731
+ async getChain(assetId) {
732
+ return this.request({
733
+ method: "GET",
734
+ path: `/v1/provenance/chain/${encodeURIComponent(assetId)}`
735
+ });
736
+ }
737
+ /**
738
+ * Get a single provenance event by ID.
739
+ */
740
+ async get(eventId) {
741
+ return this.request({
742
+ method: "GET",
743
+ path: `/v1/provenance/${encodeURIComponent(eventId)}`
744
+ });
745
+ }
746
+ /**
747
+ * List provenance events with optional filtering.
748
+ */
749
+ async list(params) {
750
+ const query = params ? this.buildQuery(params) : "";
751
+ return this.request({
752
+ method: "GET",
753
+ path: `/v1/provenance${query}`
754
+ });
755
+ }
756
+ /**
757
+ * Verify the integrity of an asset's provenance chain.
758
+ *
759
+ * Checks that all events are correctly linked via cryptographic hashes.
760
+ */
761
+ async verifyChain(assetId) {
762
+ return this.request({
763
+ method: "POST",
764
+ path: `/v1/provenance/chain/${encodeURIComponent(assetId)}/verify`
765
+ });
766
+ }
767
+ buildQuery(params) {
768
+ const mapped = {
769
+ asset_id: params.assetId,
770
+ event_type: params.eventType,
771
+ from_date: params.from_date,
772
+ to_date: params.to_date,
773
+ page: params.page,
774
+ per_page: params.per_page
775
+ };
776
+ const entries = Object.entries(mapped).filter(([, v]) => v !== void 0);
777
+ if (entries.length === 0) return "";
778
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
779
+ }
780
+ };
781
+
577
782
  // src/resources/schemas.ts
578
783
  function checkType(value, expected) {
579
784
  switch (expected) {
@@ -701,7 +906,7 @@ var SchemasResource = class {
701
906
  // src/client.ts
702
907
  var DEFAULT_BASE_URL = "https://api.optropic.com";
703
908
  var DEFAULT_TIMEOUT = 3e4;
704
- var SDK_VERSION = "2.0.0";
909
+ var SDK_VERSION = "2.2.0";
705
910
  var SANDBOX_PREFIXES = ["optr_test_"];
706
911
  var DEFAULT_RETRY_CONFIG = {
707
912
  maxRetries: 3,
@@ -716,8 +921,10 @@ var OptropicClient = class {
716
921
  assets;
717
922
  audit;
718
923
  compliance;
924
+ documents;
719
925
  keys;
720
926
  keysets;
927
+ provenance;
721
928
  schemas;
722
929
  constructor(config) {
723
930
  if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
@@ -747,8 +954,10 @@ var OptropicClient = class {
747
954
  this.assets = new AssetsResource(boundRequest, this);
748
955
  this.audit = new AuditResource(boundRequest);
749
956
  this.compliance = new ComplianceResource(boundRequest);
957
+ this.documents = new DocumentsResource(boundRequest);
750
958
  this.keys = new KeysResource(boundRequest);
751
959
  this.keysets = new KeysetsResource(boundRequest);
960
+ this.provenance = new ProvenanceResource(boundRequest);
752
961
  this.schemas = new SchemasResource(boundRequest);
753
962
  }
754
963
  // ─────────────────────────────────────────────────────────────────────────
@@ -773,7 +982,7 @@ var OptropicClient = class {
773
982
  return /^optr_(live|test)_[a-zA-Z0-9_-]{20,}$/.test(apiKey);
774
983
  }
775
984
  async request(options) {
776
- const { method, path, body, headers = {}, timeout = this.config.timeout } = options;
985
+ const { method, path, body, headers = {}, timeout = this.config.timeout, idempotencyKey } = options;
777
986
  const url = `${this.baseUrl}${path}`;
778
987
  const requestHeaders = {
779
988
  "Content-Type": "application/json",
@@ -784,6 +993,11 @@ var OptropicClient = class {
784
993
  ...this.config.headers,
785
994
  ...headers
786
995
  };
996
+ if (idempotencyKey) {
997
+ requestHeaders["Idempotency-Key"] = idempotencyKey;
998
+ } else if (["POST", "PUT", "PATCH"].includes(method)) {
999
+ requestHeaders["Idempotency-Key"] = crypto.randomUUID();
1000
+ }
787
1001
  let lastError = null;
788
1002
  let attempt = 0;
789
1003
  while (attempt <= this.retryConfig.maxRetries) {
@@ -804,10 +1018,12 @@ var OptropicClient = class {
804
1018
  if (attempt >= this.retryConfig.maxRetries) {
805
1019
  throw error;
806
1020
  }
807
- const delay = Math.min(
1021
+ const baseDelay = Math.min(
808
1022
  this.retryConfig.baseDelay * Math.pow(2, attempt),
809
1023
  this.retryConfig.maxDelay
810
1024
  );
1025
+ const jitter = baseDelay * 0.5 * Math.random();
1026
+ const delay = baseDelay + jitter;
811
1027
  if (error instanceof RateLimitedError) {
812
1028
  await this.sleep(error.retryAfter * 1e3);
813
1029
  } else {
@@ -895,6 +1111,203 @@ function createClient(config) {
895
1111
  return new OptropicClient(config);
896
1112
  }
897
1113
 
1114
+ // src/filter-verify.ts
1115
+ var HEADER_SIZE = 19;
1116
+ var SIGNATURE_SIZE = 64;
1117
+ var SLOTS_PER_BUCKET = 4;
1118
+ var StaleFilterError = class extends Error {
1119
+ code = "FILTER_STALE_CRITICAL";
1120
+ ageSeconds;
1121
+ trustWindowSeconds;
1122
+ constructor(ageSeconds, trustWindowSeconds) {
1123
+ super(`Filter age ${ageSeconds}s exceeds trust window ${trustWindowSeconds}s`);
1124
+ this.name = "StaleFilterError";
1125
+ this.ageSeconds = ageSeconds;
1126
+ this.trustWindowSeconds = trustWindowSeconds;
1127
+ }
1128
+ };
1129
+ async function sha256(data) {
1130
+ const buf = new Uint8Array(data).buffer;
1131
+ const hash = await globalThis.crypto.subtle.digest("SHA-256", buf);
1132
+ return new Uint8Array(hash);
1133
+ }
1134
+ async function sha256Hex(input) {
1135
+ const encoded = new TextEncoder().encode(input);
1136
+ const hash = await sha256(encoded);
1137
+ return Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
1138
+ }
1139
+ function uint32BE(buf, offset) {
1140
+ return (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
1141
+ }
1142
+ function uint16BE(buf, offset) {
1143
+ return buf[offset] << 8 | buf[offset + 1];
1144
+ }
1145
+ function int64BE(buf, offset) {
1146
+ const high = (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
1147
+ const low = (buf[offset + 4] << 24 | buf[offset + 5] << 16 | buf[offset + 6] << 8 | buf[offset + 7]) >>> 0;
1148
+ return high * 4294967296 + low;
1149
+ }
1150
+ async function fingerprint(item) {
1151
+ const encoded = new TextEncoder().encode(item);
1152
+ let digest = await sha256(encoded);
1153
+ let fp = uint16BE(digest, 4);
1154
+ while (fp === 0) {
1155
+ digest = await sha256(digest);
1156
+ fp = uint16BE(digest, 4);
1157
+ }
1158
+ return fp;
1159
+ }
1160
+ async function h1(item, capacity) {
1161
+ const encoded = new TextEncoder().encode(item);
1162
+ const digest = await sha256(encoded);
1163
+ return uint32BE(digest, 0) % capacity;
1164
+ }
1165
+ async function altIndex(index, fp, capacity) {
1166
+ const fpBuf = new Uint8Array(2);
1167
+ fpBuf[0] = fp >> 8 & 255;
1168
+ fpBuf[1] = fp & 255;
1169
+ const fpDigest = await sha256(fpBuf);
1170
+ return ((index ^ uint32BE(fpDigest, 0) % capacity) & 4294967295) >>> 0;
1171
+ }
1172
+ async function filterLookup(filterData, capacity, item) {
1173
+ const fp = await fingerprint(item);
1174
+ const i1 = await h1(item, capacity);
1175
+ const i2 = await altIndex(i1, fp, capacity);
1176
+ const b1Offset = i1 * SLOTS_PER_BUCKET * 2;
1177
+ for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
1178
+ const slotVal = uint16BE(filterData, b1Offset + s * 2);
1179
+ if (slotVal === fp) return true;
1180
+ }
1181
+ const b2Offset = i2 * SLOTS_PER_BUCKET * 2;
1182
+ for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
1183
+ const slotVal = uint16BE(filterData, b2Offset + s * 2);
1184
+ if (slotVal === fp) return true;
1185
+ }
1186
+ return false;
1187
+ }
1188
+ function parseFilterHeader(buf) {
1189
+ if (buf.length < HEADER_SIZE) {
1190
+ throw new Error("Buffer too small for filter header");
1191
+ }
1192
+ return {
1193
+ version: buf[0],
1194
+ issuedAt: int64BE(buf, 1),
1195
+ itemCount: uint32BE(buf, 9),
1196
+ keyId: uint16BE(buf, 13),
1197
+ capacity: uint32BE(buf, 15)
1198
+ };
1199
+ }
1200
+ function parseSaltsHeader(header) {
1201
+ const salts = /* @__PURE__ */ new Map();
1202
+ if (!header) return salts;
1203
+ for (const pair of header.split(",")) {
1204
+ const colonIdx = pair.indexOf(":");
1205
+ if (colonIdx === -1) continue;
1206
+ const tenantId = pair.slice(0, colonIdx).trim();
1207
+ const saltHex = pair.slice(colonIdx + 1).trim();
1208
+ if (tenantId && saltHex) {
1209
+ const bytes = new Uint8Array(saltHex.length / 2);
1210
+ for (let i = 0; i < bytes.length; i++) {
1211
+ bytes[i] = parseInt(saltHex.slice(i * 2, i * 2 + 2), 16);
1212
+ }
1213
+ salts.set(tenantId, bytes);
1214
+ }
1215
+ }
1216
+ return salts;
1217
+ }
1218
+ function toHex(bytes) {
1219
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1220
+ }
1221
+ async function verifyOffline(options) {
1222
+ const {
1223
+ assetId,
1224
+ filterBytes,
1225
+ salts,
1226
+ filterPolicy = "permissive",
1227
+ trustWindowSeconds = 259200
1228
+ // 72 hours
1229
+ } = options;
1230
+ const header = parseFilterHeader(filterBytes);
1231
+ const nowSeconds = Math.floor(Date.now() / 1e3);
1232
+ const ageSeconds = nowSeconds - header.issuedAt;
1233
+ if (ageSeconds > trustWindowSeconds) {
1234
+ if (filterPolicy === "strict") {
1235
+ throw new StaleFilterError(ageSeconds, trustWindowSeconds);
1236
+ }
1237
+ }
1238
+ const filterDataEnd = filterBytes.length - SIGNATURE_SIZE;
1239
+ const filterData = filterBytes.slice(HEADER_SIZE, filterDataEnd);
1240
+ const capacity = header.capacity;
1241
+ let revoked = false;
1242
+ for (const salt of salts.values()) {
1243
+ const saltHex = toHex(salt);
1244
+ const saltedInput = assetId + saltHex;
1245
+ const saltedHash = await sha256Hex(saltedInput);
1246
+ if (await filterLookup(filterData, capacity, saltedHash)) {
1247
+ revoked = true;
1248
+ break;
1249
+ }
1250
+ }
1251
+ const freshness = ageSeconds > trustWindowSeconds ? "stale" : "current";
1252
+ return {
1253
+ signatureValid: true,
1254
+ revocationStatus: revoked ? "revoked" : "clear",
1255
+ filterAgeSeconds: ageSeconds,
1256
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
1257
+ verificationMode: "offline",
1258
+ freshness
1259
+ };
1260
+ }
1261
+
1262
+ // src/dpp.ts
1263
+ function buildDPPConfig(metadata) {
1264
+ const config = {
1265
+ product_id: metadata.productId,
1266
+ product_name: metadata.productName,
1267
+ manufacturer: metadata.manufacturer,
1268
+ country_of_origin: metadata.countryOfOrigin,
1269
+ dpp_category: metadata.category
1270
+ };
1271
+ if (metadata.carbonFootprint !== void 0) config.carbon_footprint_kg_co2e = metadata.carbonFootprint;
1272
+ if (metadata.recycledContent !== void 0) config.recycled_content_percent = metadata.recycledContent;
1273
+ if (metadata.durabilityYears !== void 0) config.durability_years = metadata.durabilityYears;
1274
+ if (metadata.repairabilityScore !== void 0) config.repairability_score = metadata.repairabilityScore;
1275
+ if (metadata.substancesOfConcern) config.substances_of_concern = metadata.substancesOfConcern;
1276
+ if (metadata.dppRegistryId) config.dpp_registry_id = metadata.dppRegistryId;
1277
+ if (metadata.conformityDeclarations) config.conformity_declarations = metadata.conformityDeclarations;
1278
+ if (metadata.sectorData) config.sector_data = metadata.sectorData;
1279
+ return config;
1280
+ }
1281
+ function validateDPPMetadata(metadata) {
1282
+ const errors = [];
1283
+ if (!metadata.productId) errors.push("productId is required");
1284
+ if (!metadata.productName) errors.push("productName is required");
1285
+ if (!metadata.manufacturer) errors.push("manufacturer is required");
1286
+ if (!metadata.countryOfOrigin) errors.push("countryOfOrigin is required");
1287
+ if (metadata.countryOfOrigin && !/^[A-Z]{2}$/.test(metadata.countryOfOrigin)) {
1288
+ errors.push('countryOfOrigin must be ISO 3166-1 alpha-2 (e.g., "DE")');
1289
+ }
1290
+ if (metadata.recycledContent !== void 0 && (metadata.recycledContent < 0 || metadata.recycledContent > 100)) {
1291
+ errors.push("recycledContent must be between 0 and 100");
1292
+ }
1293
+ if (metadata.category === "battery" && metadata.sectorData) {
1294
+ const battery = metadata.sectorData;
1295
+ if (battery.type === "battery") {
1296
+ if (!battery.chemistry) errors.push("battery.chemistry is required for battery passports");
1297
+ if (!battery.capacityKwh) errors.push("battery.capacityKwh is required for battery passports");
1298
+ }
1299
+ }
1300
+ if (metadata.category === "textile" && metadata.sectorData) {
1301
+ const textile = metadata.sectorData;
1302
+ if (textile.type === "textile") {
1303
+ if (!textile.fiberComposition || textile.fiberComposition.length === 0) {
1304
+ errors.push("textile.fiberComposition is required for textile passports");
1305
+ }
1306
+ }
1307
+ }
1308
+ return { valid: errors.length === 0, errors };
1309
+ }
1310
+
898
1311
  // src/webhooks.ts
899
1312
  async function computeHmacSha256(secret, message) {
900
1313
  const encoder = new TextEncoder();
@@ -941,7 +1354,7 @@ async function verifyWebhookSignature(options) {
941
1354
  }
942
1355
 
943
1356
  // src/index.ts
944
- var SDK_VERSION2 = "2.0.0";
1357
+ var SDK_VERSION2 = "2.2.0";
945
1358
  export {
946
1359
  AssetsResource,
947
1360
  AuditResource,
@@ -949,6 +1362,7 @@ export {
949
1362
  BatchNotFoundError,
950
1363
  CodeNotFoundError,
951
1364
  ComplianceResource,
1365
+ DocumentsResource,
952
1366
  InvalidCodeError,
953
1367
  InvalidGTINError,
954
1368
  InvalidSerialError,
@@ -957,13 +1371,21 @@ export {
957
1371
  NetworkError,
958
1372
  OptropicClient,
959
1373
  OptropicError,
1374
+ ProvenanceResource,
960
1375
  QuotaExceededError,
961
1376
  RateLimitedError,
962
1377
  RevokedCodeError,
963
1378
  SDK_VERSION2 as SDK_VERSION,
964
1379
  SchemasResource,
965
1380
  ServiceUnavailableError,
1381
+ StaleFilterError,
966
1382
  TimeoutError,
1383
+ buildDPPConfig,
967
1384
  createClient,
1385
+ createErrorFromResponse,
1386
+ parseFilterHeader,
1387
+ parseSaltsHeader,
1388
+ validateDPPMetadata,
1389
+ verifyOffline,
968
1390
  verifyWebhookSignature
969
1391
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "optropic",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Official Optropic SDK for TypeScript and JavaScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",