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 +435 -4
- package/dist/index.d.cts +470 -3
- package/dist/index.d.ts +470 -3
- package/dist/index.js +426 -4
- package/package.json +1 -1
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.
|
|
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
|
|
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.
|
|
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
|
});
|