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 +488 -9
- package/dist/index.d.cts +485 -4
- package/dist/index.d.ts +485 -4
- package/dist/index.js +479 -9
- package/package.json +1 -1
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,23 +906,27 @@ 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.
|
|
909
|
+
var SDK_VERSION = "2.3.0";
|
|
705
910
|
var SANDBOX_PREFIXES = ["optr_test_"];
|
|
706
911
|
var DEFAULT_RETRY_CONFIG = {
|
|
707
912
|
maxRetries: 3,
|
|
708
913
|
baseDelay: 1e3,
|
|
709
914
|
maxDelay: 1e4
|
|
710
915
|
};
|
|
916
|
+
var KEY_REDACT_RE = /(optr_(?:live|test)_)[a-zA-Z0-9_-]+/g;
|
|
711
917
|
var OptropicClient = class {
|
|
712
918
|
config;
|
|
713
919
|
baseUrl;
|
|
714
920
|
retryConfig;
|
|
715
921
|
_sandbox;
|
|
922
|
+
_debug;
|
|
716
923
|
assets;
|
|
717
924
|
audit;
|
|
718
925
|
compliance;
|
|
926
|
+
documents;
|
|
719
927
|
keys;
|
|
720
928
|
keysets;
|
|
929
|
+
provenance;
|
|
721
930
|
schemas;
|
|
722
931
|
constructor(config) {
|
|
723
932
|
if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
|
|
@@ -729,6 +938,7 @@ var OptropicClient = class {
|
|
|
729
938
|
...config,
|
|
730
939
|
timeout: config.timeout ?? DEFAULT_TIMEOUT
|
|
731
940
|
};
|
|
941
|
+
this._debug = config.debug ?? false;
|
|
732
942
|
if (config.sandbox !== void 0) {
|
|
733
943
|
this._sandbox = config.sandbox;
|
|
734
944
|
} else {
|
|
@@ -747,8 +957,10 @@ var OptropicClient = class {
|
|
|
747
957
|
this.assets = new AssetsResource(boundRequest, this);
|
|
748
958
|
this.audit = new AuditResource(boundRequest);
|
|
749
959
|
this.compliance = new ComplianceResource(boundRequest);
|
|
960
|
+
this.documents = new DocumentsResource(boundRequest);
|
|
750
961
|
this.keys = new KeysResource(boundRequest);
|
|
751
962
|
this.keysets = new KeysetsResource(boundRequest);
|
|
963
|
+
this.provenance = new ProvenanceResource(boundRequest);
|
|
752
964
|
this.schemas = new SchemasResource(boundRequest);
|
|
753
965
|
}
|
|
754
966
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -767,23 +979,59 @@ var OptropicClient = class {
|
|
|
767
979
|
return this._sandbox ? "sandbox" : "live";
|
|
768
980
|
}
|
|
769
981
|
// ─────────────────────────────────────────────────────────────────────────
|
|
982
|
+
// DEBUG LOGGING
|
|
983
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
984
|
+
redact(text) {
|
|
985
|
+
return text.replace(KEY_REDACT_RE, "$1****");
|
|
986
|
+
}
|
|
987
|
+
logRequest(method, url, requestId, idempotencyKey) {
|
|
988
|
+
if (!this._debug) return;
|
|
989
|
+
const parts = [`${method} ${this.redact(url)}`, `req=${requestId}`];
|
|
990
|
+
if (idempotencyKey) parts.push(`idempotency=${idempotencyKey}`);
|
|
991
|
+
console.log(`[optropic] ${parts.join(" ")}`);
|
|
992
|
+
}
|
|
993
|
+
logResponse(method, path, status, durationMs, requestId, serverRequestId, attempt) {
|
|
994
|
+
if (!this._debug) return;
|
|
995
|
+
const parts = [`${method} ${path} \u2192 ${status} (${Math.round(durationMs)}ms)`, `req=${requestId}`];
|
|
996
|
+
if (serverRequestId) parts.push(`server_req=${serverRequestId}`);
|
|
997
|
+
if (attempt > 0) parts.push(`attempt=${attempt + 1}`);
|
|
998
|
+
console.log(`[optropic] ${parts.join(" ")}`);
|
|
999
|
+
}
|
|
1000
|
+
logRetry(method, path, attempt, delay, reason) {
|
|
1001
|
+
if (!this._debug) return;
|
|
1002
|
+
console.log(
|
|
1003
|
+
`[optropic] ${method} ${path} retry #${attempt + 1} in ${(delay / 1e3).toFixed(1)}s (${reason})`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
770
1007
|
// PRIVATE METHODS
|
|
771
1008
|
// ─────────────────────────────────────────────────────────────────────────
|
|
772
1009
|
isValidApiKey(apiKey) {
|
|
773
1010
|
return /^optr_(live|test)_[a-zA-Z0-9_-]{20,}$/.test(apiKey);
|
|
774
1011
|
}
|
|
775
1012
|
async request(options) {
|
|
776
|
-
const { method, path, body, headers = {}, timeout = this.config.timeout } = options;
|
|
1013
|
+
const { method, path, body, headers = {}, timeout = this.config.timeout, idempotencyKey } = options;
|
|
777
1014
|
const url = `${this.baseUrl}${path}`;
|
|
1015
|
+
const requestId = crypto.randomUUID();
|
|
778
1016
|
const requestHeaders = {
|
|
779
1017
|
"Content-Type": "application/json",
|
|
780
1018
|
"Accept": "application/json",
|
|
781
1019
|
"x-api-key": this.config.apiKey,
|
|
782
1020
|
"X-SDK-Version": SDK_VERSION,
|
|
783
1021
|
"X-SDK-Language": "typescript",
|
|
1022
|
+
"X-Request-ID": requestId,
|
|
784
1023
|
...this.config.headers,
|
|
785
1024
|
...headers
|
|
786
1025
|
};
|
|
1026
|
+
let effectiveIdempotencyKey;
|
|
1027
|
+
if (idempotencyKey) {
|
|
1028
|
+
requestHeaders["Idempotency-Key"] = idempotencyKey;
|
|
1029
|
+
effectiveIdempotencyKey = idempotencyKey;
|
|
1030
|
+
} else if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
1031
|
+
effectiveIdempotencyKey = crypto.randomUUID();
|
|
1032
|
+
requestHeaders["Idempotency-Key"] = effectiveIdempotencyKey;
|
|
1033
|
+
}
|
|
1034
|
+
this.logRequest(method, url, requestId, effectiveIdempotencyKey);
|
|
787
1035
|
let lastError = null;
|
|
788
1036
|
let attempt = 0;
|
|
789
1037
|
while (attempt <= this.retryConfig.maxRetries) {
|
|
@@ -793,7 +1041,10 @@ var OptropicClient = class {
|
|
|
793
1041
|
method,
|
|
794
1042
|
requestHeaders,
|
|
795
1043
|
body,
|
|
796
|
-
timeout
|
|
1044
|
+
timeout,
|
|
1045
|
+
requestId,
|
|
1046
|
+
path,
|
|
1047
|
+
attempt
|
|
797
1048
|
);
|
|
798
1049
|
return response;
|
|
799
1050
|
} catch (error) {
|
|
@@ -804,13 +1055,19 @@ var OptropicClient = class {
|
|
|
804
1055
|
if (attempt >= this.retryConfig.maxRetries) {
|
|
805
1056
|
throw error;
|
|
806
1057
|
}
|
|
807
|
-
const
|
|
1058
|
+
const baseDelay = Math.min(
|
|
808
1059
|
this.retryConfig.baseDelay * Math.pow(2, attempt),
|
|
809
1060
|
this.retryConfig.maxDelay
|
|
810
1061
|
);
|
|
1062
|
+
const jitter = baseDelay * 0.5 * Math.random();
|
|
1063
|
+
const delay = baseDelay + jitter;
|
|
811
1064
|
if (error instanceof RateLimitedError) {
|
|
812
|
-
|
|
1065
|
+
const retryDelay = error.retryAfter * 1e3;
|
|
1066
|
+
this.logRetry(method, path, attempt, retryDelay, "rate_limited");
|
|
1067
|
+
await this.sleep(retryDelay);
|
|
813
1068
|
} else {
|
|
1069
|
+
const statusCode = error instanceof OptropicError ? error.statusCode : 0;
|
|
1070
|
+
this.logRetry(method, path, attempt, delay, `status=${statusCode}`);
|
|
814
1071
|
await this.sleep(delay);
|
|
815
1072
|
}
|
|
816
1073
|
attempt++;
|
|
@@ -818,9 +1075,10 @@ var OptropicClient = class {
|
|
|
818
1075
|
}
|
|
819
1076
|
throw lastError ?? new OptropicError("UNKNOWN_ERROR", "Request failed");
|
|
820
1077
|
}
|
|
821
|
-
async executeRequest(url, method, headers, body, timeout) {
|
|
1078
|
+
async executeRequest(url, method, headers, body, timeout, requestId, path, attempt) {
|
|
822
1079
|
const controller = new AbortController();
|
|
823
1080
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1081
|
+
const t0 = performance.now();
|
|
824
1082
|
try {
|
|
825
1083
|
const response = await fetch(url, {
|
|
826
1084
|
method,
|
|
@@ -829,7 +1087,9 @@ var OptropicClient = class {
|
|
|
829
1087
|
signal: controller.signal
|
|
830
1088
|
});
|
|
831
1089
|
clearTimeout(timeoutId);
|
|
832
|
-
const
|
|
1090
|
+
const durationMs = performance.now() - t0;
|
|
1091
|
+
const serverRequestId = response.headers.get("x-request-id") ?? "";
|
|
1092
|
+
this.logResponse(method, path, response.status, durationMs, requestId, serverRequestId, attempt);
|
|
833
1093
|
if (!response.ok) {
|
|
834
1094
|
let errorBody;
|
|
835
1095
|
try {
|
|
@@ -852,7 +1112,7 @@ var OptropicClient = class {
|
|
|
852
1112
|
code: errorBody.code,
|
|
853
1113
|
message: errorBody.message,
|
|
854
1114
|
details: errorBody.details,
|
|
855
|
-
requestId
|
|
1115
|
+
requestId: serverRequestId || requestId
|
|
856
1116
|
});
|
|
857
1117
|
}
|
|
858
1118
|
if (response.status === 204) {
|
|
@@ -878,8 +1138,12 @@ var OptropicClient = class {
|
|
|
878
1138
|
}
|
|
879
1139
|
if (error instanceof Error) {
|
|
880
1140
|
if (error.name === "AbortError") {
|
|
1141
|
+
const durationMs2 = performance.now() - t0;
|
|
1142
|
+
this.logResponse(method, path, 408, durationMs2, requestId, "", attempt);
|
|
881
1143
|
throw new TimeoutError(timeout);
|
|
882
1144
|
}
|
|
1145
|
+
const durationMs = performance.now() - t0;
|
|
1146
|
+
this.logResponse(method, path, 0, durationMs, requestId, "", attempt);
|
|
883
1147
|
throw new NetworkError(error.message, { cause: error });
|
|
884
1148
|
}
|
|
885
1149
|
throw new OptropicError("UNKNOWN_ERROR", "An unexpected error occurred", {
|
|
@@ -895,6 +1159,203 @@ function createClient(config) {
|
|
|
895
1159
|
return new OptropicClient(config);
|
|
896
1160
|
}
|
|
897
1161
|
|
|
1162
|
+
// src/filter-verify.ts
|
|
1163
|
+
var HEADER_SIZE = 19;
|
|
1164
|
+
var SIGNATURE_SIZE = 64;
|
|
1165
|
+
var SLOTS_PER_BUCKET = 4;
|
|
1166
|
+
var StaleFilterError = class extends Error {
|
|
1167
|
+
code = "FILTER_STALE_CRITICAL";
|
|
1168
|
+
ageSeconds;
|
|
1169
|
+
trustWindowSeconds;
|
|
1170
|
+
constructor(ageSeconds, trustWindowSeconds) {
|
|
1171
|
+
super(`Filter age ${ageSeconds}s exceeds trust window ${trustWindowSeconds}s`);
|
|
1172
|
+
this.name = "StaleFilterError";
|
|
1173
|
+
this.ageSeconds = ageSeconds;
|
|
1174
|
+
this.trustWindowSeconds = trustWindowSeconds;
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
async function sha256(data) {
|
|
1178
|
+
const buf = new Uint8Array(data).buffer;
|
|
1179
|
+
const hash = await globalThis.crypto.subtle.digest("SHA-256", buf);
|
|
1180
|
+
return new Uint8Array(hash);
|
|
1181
|
+
}
|
|
1182
|
+
async function sha256Hex(input) {
|
|
1183
|
+
const encoded = new TextEncoder().encode(input);
|
|
1184
|
+
const hash = await sha256(encoded);
|
|
1185
|
+
return Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1186
|
+
}
|
|
1187
|
+
function uint32BE(buf, offset) {
|
|
1188
|
+
return (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
|
|
1189
|
+
}
|
|
1190
|
+
function uint16BE(buf, offset) {
|
|
1191
|
+
return buf[offset] << 8 | buf[offset + 1];
|
|
1192
|
+
}
|
|
1193
|
+
function int64BE(buf, offset) {
|
|
1194
|
+
const high = (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
|
|
1195
|
+
const low = (buf[offset + 4] << 24 | buf[offset + 5] << 16 | buf[offset + 6] << 8 | buf[offset + 7]) >>> 0;
|
|
1196
|
+
return high * 4294967296 + low;
|
|
1197
|
+
}
|
|
1198
|
+
async function fingerprint(item) {
|
|
1199
|
+
const encoded = new TextEncoder().encode(item);
|
|
1200
|
+
let digest = await sha256(encoded);
|
|
1201
|
+
let fp = uint16BE(digest, 4);
|
|
1202
|
+
while (fp === 0) {
|
|
1203
|
+
digest = await sha256(digest);
|
|
1204
|
+
fp = uint16BE(digest, 4);
|
|
1205
|
+
}
|
|
1206
|
+
return fp;
|
|
1207
|
+
}
|
|
1208
|
+
async function h1(item, capacity) {
|
|
1209
|
+
const encoded = new TextEncoder().encode(item);
|
|
1210
|
+
const digest = await sha256(encoded);
|
|
1211
|
+
return uint32BE(digest, 0) % capacity;
|
|
1212
|
+
}
|
|
1213
|
+
async function altIndex(index, fp, capacity) {
|
|
1214
|
+
const fpBuf = new Uint8Array(2);
|
|
1215
|
+
fpBuf[0] = fp >> 8 & 255;
|
|
1216
|
+
fpBuf[1] = fp & 255;
|
|
1217
|
+
const fpDigest = await sha256(fpBuf);
|
|
1218
|
+
return ((index ^ uint32BE(fpDigest, 0) % capacity) & 4294967295) >>> 0;
|
|
1219
|
+
}
|
|
1220
|
+
async function filterLookup(filterData, capacity, item) {
|
|
1221
|
+
const fp = await fingerprint(item);
|
|
1222
|
+
const i1 = await h1(item, capacity);
|
|
1223
|
+
const i2 = await altIndex(i1, fp, capacity);
|
|
1224
|
+
const b1Offset = i1 * SLOTS_PER_BUCKET * 2;
|
|
1225
|
+
for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
|
|
1226
|
+
const slotVal = uint16BE(filterData, b1Offset + s * 2);
|
|
1227
|
+
if (slotVal === fp) return true;
|
|
1228
|
+
}
|
|
1229
|
+
const b2Offset = i2 * SLOTS_PER_BUCKET * 2;
|
|
1230
|
+
for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
|
|
1231
|
+
const slotVal = uint16BE(filterData, b2Offset + s * 2);
|
|
1232
|
+
if (slotVal === fp) return true;
|
|
1233
|
+
}
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
function parseFilterHeader(buf) {
|
|
1237
|
+
if (buf.length < HEADER_SIZE) {
|
|
1238
|
+
throw new Error("Buffer too small for filter header");
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
version: buf[0],
|
|
1242
|
+
issuedAt: int64BE(buf, 1),
|
|
1243
|
+
itemCount: uint32BE(buf, 9),
|
|
1244
|
+
keyId: uint16BE(buf, 13),
|
|
1245
|
+
capacity: uint32BE(buf, 15)
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
function parseSaltsHeader(header) {
|
|
1249
|
+
const salts = /* @__PURE__ */ new Map();
|
|
1250
|
+
if (!header) return salts;
|
|
1251
|
+
for (const pair of header.split(",")) {
|
|
1252
|
+
const colonIdx = pair.indexOf(":");
|
|
1253
|
+
if (colonIdx === -1) continue;
|
|
1254
|
+
const tenantId = pair.slice(0, colonIdx).trim();
|
|
1255
|
+
const saltHex = pair.slice(colonIdx + 1).trim();
|
|
1256
|
+
if (tenantId && saltHex) {
|
|
1257
|
+
const bytes = new Uint8Array(saltHex.length / 2);
|
|
1258
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1259
|
+
bytes[i] = parseInt(saltHex.slice(i * 2, i * 2 + 2), 16);
|
|
1260
|
+
}
|
|
1261
|
+
salts.set(tenantId, bytes);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return salts;
|
|
1265
|
+
}
|
|
1266
|
+
function toHex(bytes) {
|
|
1267
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1268
|
+
}
|
|
1269
|
+
async function verifyOffline(options) {
|
|
1270
|
+
const {
|
|
1271
|
+
assetId,
|
|
1272
|
+
filterBytes,
|
|
1273
|
+
salts,
|
|
1274
|
+
filterPolicy = "permissive",
|
|
1275
|
+
trustWindowSeconds = 259200
|
|
1276
|
+
// 72 hours
|
|
1277
|
+
} = options;
|
|
1278
|
+
const header = parseFilterHeader(filterBytes);
|
|
1279
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
1280
|
+
const ageSeconds = nowSeconds - header.issuedAt;
|
|
1281
|
+
if (ageSeconds > trustWindowSeconds) {
|
|
1282
|
+
if (filterPolicy === "strict") {
|
|
1283
|
+
throw new StaleFilterError(ageSeconds, trustWindowSeconds);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const filterDataEnd = filterBytes.length - SIGNATURE_SIZE;
|
|
1287
|
+
const filterData = filterBytes.slice(HEADER_SIZE, filterDataEnd);
|
|
1288
|
+
const capacity = header.capacity;
|
|
1289
|
+
let revoked = false;
|
|
1290
|
+
for (const salt of salts.values()) {
|
|
1291
|
+
const saltHex = toHex(salt);
|
|
1292
|
+
const saltedInput = assetId + saltHex;
|
|
1293
|
+
const saltedHash = await sha256Hex(saltedInput);
|
|
1294
|
+
if (await filterLookup(filterData, capacity, saltedHash)) {
|
|
1295
|
+
revoked = true;
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const freshness = ageSeconds > trustWindowSeconds ? "stale" : "current";
|
|
1300
|
+
return {
|
|
1301
|
+
signatureValid: true,
|
|
1302
|
+
revocationStatus: revoked ? "revoked" : "clear",
|
|
1303
|
+
filterAgeSeconds: ageSeconds,
|
|
1304
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1305
|
+
verificationMode: "offline",
|
|
1306
|
+
freshness
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// src/dpp.ts
|
|
1311
|
+
function buildDPPConfig(metadata) {
|
|
1312
|
+
const config = {
|
|
1313
|
+
product_id: metadata.productId,
|
|
1314
|
+
product_name: metadata.productName,
|
|
1315
|
+
manufacturer: metadata.manufacturer,
|
|
1316
|
+
country_of_origin: metadata.countryOfOrigin,
|
|
1317
|
+
dpp_category: metadata.category
|
|
1318
|
+
};
|
|
1319
|
+
if (metadata.carbonFootprint !== void 0) config.carbon_footprint_kg_co2e = metadata.carbonFootprint;
|
|
1320
|
+
if (metadata.recycledContent !== void 0) config.recycled_content_percent = metadata.recycledContent;
|
|
1321
|
+
if (metadata.durabilityYears !== void 0) config.durability_years = metadata.durabilityYears;
|
|
1322
|
+
if (metadata.repairabilityScore !== void 0) config.repairability_score = metadata.repairabilityScore;
|
|
1323
|
+
if (metadata.substancesOfConcern) config.substances_of_concern = metadata.substancesOfConcern;
|
|
1324
|
+
if (metadata.dppRegistryId) config.dpp_registry_id = metadata.dppRegistryId;
|
|
1325
|
+
if (metadata.conformityDeclarations) config.conformity_declarations = metadata.conformityDeclarations;
|
|
1326
|
+
if (metadata.sectorData) config.sector_data = metadata.sectorData;
|
|
1327
|
+
return config;
|
|
1328
|
+
}
|
|
1329
|
+
function validateDPPMetadata(metadata) {
|
|
1330
|
+
const errors = [];
|
|
1331
|
+
if (!metadata.productId) errors.push("productId is required");
|
|
1332
|
+
if (!metadata.productName) errors.push("productName is required");
|
|
1333
|
+
if (!metadata.manufacturer) errors.push("manufacturer is required");
|
|
1334
|
+
if (!metadata.countryOfOrigin) errors.push("countryOfOrigin is required");
|
|
1335
|
+
if (metadata.countryOfOrigin && !/^[A-Z]{2}$/.test(metadata.countryOfOrigin)) {
|
|
1336
|
+
errors.push('countryOfOrigin must be ISO 3166-1 alpha-2 (e.g., "DE")');
|
|
1337
|
+
}
|
|
1338
|
+
if (metadata.recycledContent !== void 0 && (metadata.recycledContent < 0 || metadata.recycledContent > 100)) {
|
|
1339
|
+
errors.push("recycledContent must be between 0 and 100");
|
|
1340
|
+
}
|
|
1341
|
+
if (metadata.category === "battery" && metadata.sectorData) {
|
|
1342
|
+
const battery = metadata.sectorData;
|
|
1343
|
+
if (battery.type === "battery") {
|
|
1344
|
+
if (!battery.chemistry) errors.push("battery.chemistry is required for battery passports");
|
|
1345
|
+
if (!battery.capacityKwh) errors.push("battery.capacityKwh is required for battery passports");
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (metadata.category === "textile" && metadata.sectorData) {
|
|
1349
|
+
const textile = metadata.sectorData;
|
|
1350
|
+
if (textile.type === "textile") {
|
|
1351
|
+
if (!textile.fiberComposition || textile.fiberComposition.length === 0) {
|
|
1352
|
+
errors.push("textile.fiberComposition is required for textile passports");
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return { valid: errors.length === 0, errors };
|
|
1357
|
+
}
|
|
1358
|
+
|
|
898
1359
|
// src/webhooks.ts
|
|
899
1360
|
async function computeHmacSha256(secret, message) {
|
|
900
1361
|
const encoder = new TextEncoder();
|
|
@@ -941,7 +1402,7 @@ async function verifyWebhookSignature(options) {
|
|
|
941
1402
|
}
|
|
942
1403
|
|
|
943
1404
|
// src/index.ts
|
|
944
|
-
var SDK_VERSION2 = "2.
|
|
1405
|
+
var SDK_VERSION2 = "2.3.0";
|
|
945
1406
|
export {
|
|
946
1407
|
AssetsResource,
|
|
947
1408
|
AuditResource,
|
|
@@ -949,6 +1410,7 @@ export {
|
|
|
949
1410
|
BatchNotFoundError,
|
|
950
1411
|
CodeNotFoundError,
|
|
951
1412
|
ComplianceResource,
|
|
1413
|
+
DocumentsResource,
|
|
952
1414
|
InvalidCodeError,
|
|
953
1415
|
InvalidGTINError,
|
|
954
1416
|
InvalidSerialError,
|
|
@@ -957,13 +1419,21 @@ export {
|
|
|
957
1419
|
NetworkError,
|
|
958
1420
|
OptropicClient,
|
|
959
1421
|
OptropicError,
|
|
1422
|
+
ProvenanceResource,
|
|
960
1423
|
QuotaExceededError,
|
|
961
1424
|
RateLimitedError,
|
|
962
1425
|
RevokedCodeError,
|
|
963
1426
|
SDK_VERSION2 as SDK_VERSION,
|
|
964
1427
|
SchemasResource,
|
|
965
1428
|
ServiceUnavailableError,
|
|
1429
|
+
StaleFilterError,
|
|
966
1430
|
TimeoutError,
|
|
1431
|
+
buildDPPConfig,
|
|
967
1432
|
createClient,
|
|
1433
|
+
createErrorFromResponse,
|
|
1434
|
+
parseFilterHeader,
|
|
1435
|
+
parseSaltsHeader,
|
|
1436
|
+
validateDPPMetadata,
|
|
1437
|
+
verifyOffline,
|
|
968
1438
|
verifyWebhookSignature
|
|
969
1439
|
};
|