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.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
BatchNotFoundError: () => BatchNotFoundError,
|
|
37
37
|
CodeNotFoundError: () => CodeNotFoundError,
|
|
38
38
|
ComplianceResource: () => ComplianceResource,
|
|
39
|
+
DocumentsResource: () => DocumentsResource,
|
|
39
40
|
InvalidCodeError: () => InvalidCodeError,
|
|
40
41
|
InvalidGTINError: () => InvalidGTINError,
|
|
41
42
|
InvalidSerialError: () => InvalidSerialError,
|
|
@@ -44,14 +45,22 @@ __export(index_exports, {
|
|
|
44
45
|
NetworkError: () => NetworkError,
|
|
45
46
|
OptropicClient: () => OptropicClient,
|
|
46
47
|
OptropicError: () => OptropicError,
|
|
48
|
+
ProvenanceResource: () => ProvenanceResource,
|
|
47
49
|
QuotaExceededError: () => QuotaExceededError,
|
|
48
50
|
RateLimitedError: () => RateLimitedError,
|
|
49
51
|
RevokedCodeError: () => RevokedCodeError,
|
|
50
52
|
SDK_VERSION: () => SDK_VERSION2,
|
|
51
53
|
SchemasResource: () => SchemasResource,
|
|
52
54
|
ServiceUnavailableError: () => ServiceUnavailableError,
|
|
55
|
+
StaleFilterError: () => StaleFilterError,
|
|
53
56
|
TimeoutError: () => TimeoutError,
|
|
57
|
+
buildDPPConfig: () => buildDPPConfig,
|
|
54
58
|
createClient: () => createClient,
|
|
59
|
+
createErrorFromResponse: () => createErrorFromResponse,
|
|
60
|
+
parseFilterHeader: () => parseFilterHeader,
|
|
61
|
+
parseSaltsHeader: () => parseSaltsHeader,
|
|
62
|
+
validateDPPMetadata: () => validateDPPMetadata,
|
|
63
|
+
verifyOffline: () => verifyOffline,
|
|
55
64
|
verifyWebhookSignature: () => verifyWebhookSignature
|
|
56
65
|
});
|
|
57
66
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -462,6 +471,31 @@ var AssetsResource = class {
|
|
|
462
471
|
const query = effectiveParams ? this.buildQuery(effectiveParams) : "";
|
|
463
472
|
return this.request({ method: "GET", path: `/v1/assets${query}` });
|
|
464
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Auto-paginate through all assets, yielding pages of results.
|
|
476
|
+
* Returns an async generator that fetches pages on demand.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```typescript
|
|
480
|
+
* for await (const asset of client.assets.listAll({ status: 'active' })) {
|
|
481
|
+
* console.log(asset.id);
|
|
482
|
+
* }
|
|
483
|
+
* ```
|
|
484
|
+
*/
|
|
485
|
+
async *listAll(params) {
|
|
486
|
+
let page = params?.page ?? 1;
|
|
487
|
+
const perPage = params?.per_page ?? 100;
|
|
488
|
+
while (true) {
|
|
489
|
+
const response = await this.list({ ...params, page, per_page: perPage });
|
|
490
|
+
for (const asset of response.data) {
|
|
491
|
+
yield asset;
|
|
492
|
+
}
|
|
493
|
+
if (page >= response.pagination.totalPages || response.data.length === 0) {
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
page++;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
465
499
|
async get(assetId) {
|
|
466
500
|
return this.request({ method: "GET", path: `/v1/assets/${encodeURIComponent(assetId)}` });
|
|
467
501
|
}
|
|
@@ -596,6 +630,98 @@ var ComplianceResource = class {
|
|
|
596
630
|
}
|
|
597
631
|
};
|
|
598
632
|
|
|
633
|
+
// src/resources/documents.ts
|
|
634
|
+
var DocumentsResource = class {
|
|
635
|
+
constructor(request) {
|
|
636
|
+
this.request = request;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Enroll a new document (substrate fingerprint) linked to an asset.
|
|
640
|
+
*
|
|
641
|
+
* @example
|
|
642
|
+
* ```typescript
|
|
643
|
+
* const doc = await client.documents.enroll({
|
|
644
|
+
* assetId: 'asset-123',
|
|
645
|
+
* fingerprintHash: 'sha256:abc123...',
|
|
646
|
+
* descriptorVersion: 'GB_GE_M7PCA_v1',
|
|
647
|
+
* substrateType: 'S_fb',
|
|
648
|
+
* captureDevice: 'iPhone16ProMax_main',
|
|
649
|
+
* });
|
|
650
|
+
* ```
|
|
651
|
+
*/
|
|
652
|
+
async enroll(params) {
|
|
653
|
+
return this.request({
|
|
654
|
+
method: "POST",
|
|
655
|
+
path: "/v1/documents",
|
|
656
|
+
body: {
|
|
657
|
+
asset_id: params.assetId,
|
|
658
|
+
fingerprint_hash: params.fingerprintHash,
|
|
659
|
+
descriptor_version: params.descriptorVersion,
|
|
660
|
+
substrate_type: params.substrateType,
|
|
661
|
+
...params.captureDevice !== void 0 && { capture_device: params.captureDevice },
|
|
662
|
+
...params.metadata !== void 0 && { metadata: params.metadata }
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Verify a fingerprint against enrolled documents.
|
|
668
|
+
*
|
|
669
|
+
* Returns the best match if similarity exceeds the threshold.
|
|
670
|
+
*/
|
|
671
|
+
async verify(params) {
|
|
672
|
+
return this.request({
|
|
673
|
+
method: "POST",
|
|
674
|
+
path: "/v1/documents/verify",
|
|
675
|
+
body: {
|
|
676
|
+
fingerprint_hash: params.fingerprintHash,
|
|
677
|
+
descriptor_version: params.descriptorVersion,
|
|
678
|
+
...params.threshold !== void 0 && { threshold: params.threshold }
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Get a single document by ID.
|
|
684
|
+
*/
|
|
685
|
+
async get(documentId) {
|
|
686
|
+
return this.request({
|
|
687
|
+
method: "GET",
|
|
688
|
+
path: `/v1/documents/${encodeURIComponent(documentId)}`
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* List enrolled documents with optional filtering.
|
|
693
|
+
*/
|
|
694
|
+
async list(params) {
|
|
695
|
+
const query = params ? this.buildQuery(params) : "";
|
|
696
|
+
return this.request({
|
|
697
|
+
method: "GET",
|
|
698
|
+
path: `/v1/documents${query}`
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Supersede a document (e.g., re-enrollment with better capture).
|
|
703
|
+
*/
|
|
704
|
+
async supersede(documentId, newDocumentId) {
|
|
705
|
+
return this.request({
|
|
706
|
+
method: "POST",
|
|
707
|
+
path: `/v1/documents/${encodeURIComponent(documentId)}/supersede`,
|
|
708
|
+
body: { new_document_id: newDocumentId }
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
buildQuery(params) {
|
|
712
|
+
const mapped = {
|
|
713
|
+
asset_id: params.assetId,
|
|
714
|
+
substrate_type: params.substrateType,
|
|
715
|
+
status: params.status,
|
|
716
|
+
page: params.page,
|
|
717
|
+
per_page: params.per_page
|
|
718
|
+
};
|
|
719
|
+
const entries = Object.entries(mapped).filter(([, v]) => v !== void 0);
|
|
720
|
+
if (entries.length === 0) return "";
|
|
721
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
599
725
|
// src/resources/keys.ts
|
|
600
726
|
var KeysResource = class {
|
|
601
727
|
constructor(request) {
|
|
@@ -632,6 +758,94 @@ var KeysetsResource = class {
|
|
|
632
758
|
}
|
|
633
759
|
};
|
|
634
760
|
|
|
761
|
+
// src/resources/provenance.ts
|
|
762
|
+
var ProvenanceResource = class {
|
|
763
|
+
constructor(request) {
|
|
764
|
+
this.request = request;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Record a new provenance event in the chain.
|
|
768
|
+
*
|
|
769
|
+
* Events are automatically chained — the server links each new event
|
|
770
|
+
* to the previous one via cryptographic hash.
|
|
771
|
+
*
|
|
772
|
+
* @example
|
|
773
|
+
* ```typescript
|
|
774
|
+
* const event = await client.provenance.record({
|
|
775
|
+
* assetId: 'asset-123',
|
|
776
|
+
* eventType: 'manufactured',
|
|
777
|
+
* actor: 'factory-line-7',
|
|
778
|
+
* location: { country: 'DE', facility: 'Munich Plant' },
|
|
779
|
+
* });
|
|
780
|
+
* ```
|
|
781
|
+
*/
|
|
782
|
+
async record(params) {
|
|
783
|
+
return this.request({
|
|
784
|
+
method: "POST",
|
|
785
|
+
path: "/v1/provenance",
|
|
786
|
+
body: {
|
|
787
|
+
asset_id: params.assetId,
|
|
788
|
+
event_type: params.eventType,
|
|
789
|
+
actor: params.actor,
|
|
790
|
+
...params.location !== void 0 && { location: params.location },
|
|
791
|
+
...params.metadata !== void 0 && { metadata: params.metadata }
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Get the full provenance chain for an asset.
|
|
797
|
+
*/
|
|
798
|
+
async getChain(assetId) {
|
|
799
|
+
return this.request({
|
|
800
|
+
method: "GET",
|
|
801
|
+
path: `/v1/provenance/chain/${encodeURIComponent(assetId)}`
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Get a single provenance event by ID.
|
|
806
|
+
*/
|
|
807
|
+
async get(eventId) {
|
|
808
|
+
return this.request({
|
|
809
|
+
method: "GET",
|
|
810
|
+
path: `/v1/provenance/${encodeURIComponent(eventId)}`
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* List provenance events with optional filtering.
|
|
815
|
+
*/
|
|
816
|
+
async list(params) {
|
|
817
|
+
const query = params ? this.buildQuery(params) : "";
|
|
818
|
+
return this.request({
|
|
819
|
+
method: "GET",
|
|
820
|
+
path: `/v1/provenance${query}`
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Verify the integrity of an asset's provenance chain.
|
|
825
|
+
*
|
|
826
|
+
* Checks that all events are correctly linked via cryptographic hashes.
|
|
827
|
+
*/
|
|
828
|
+
async verifyChain(assetId) {
|
|
829
|
+
return this.request({
|
|
830
|
+
method: "POST",
|
|
831
|
+
path: `/v1/provenance/chain/${encodeURIComponent(assetId)}/verify`
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
buildQuery(params) {
|
|
835
|
+
const mapped = {
|
|
836
|
+
asset_id: params.assetId,
|
|
837
|
+
event_type: params.eventType,
|
|
838
|
+
from_date: params.from_date,
|
|
839
|
+
to_date: params.to_date,
|
|
840
|
+
page: params.page,
|
|
841
|
+
per_page: params.per_page
|
|
842
|
+
};
|
|
843
|
+
const entries = Object.entries(mapped).filter(([, v]) => v !== void 0);
|
|
844
|
+
if (entries.length === 0) return "";
|
|
845
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
635
849
|
// src/resources/schemas.ts
|
|
636
850
|
function checkType(value, expected) {
|
|
637
851
|
switch (expected) {
|
|
@@ -759,23 +973,27 @@ var SchemasResource = class {
|
|
|
759
973
|
// src/client.ts
|
|
760
974
|
var DEFAULT_BASE_URL = "https://api.optropic.com";
|
|
761
975
|
var DEFAULT_TIMEOUT = 3e4;
|
|
762
|
-
var SDK_VERSION = "2.
|
|
976
|
+
var SDK_VERSION = "2.3.0";
|
|
763
977
|
var SANDBOX_PREFIXES = ["optr_test_"];
|
|
764
978
|
var DEFAULT_RETRY_CONFIG = {
|
|
765
979
|
maxRetries: 3,
|
|
766
980
|
baseDelay: 1e3,
|
|
767
981
|
maxDelay: 1e4
|
|
768
982
|
};
|
|
983
|
+
var KEY_REDACT_RE = /(optr_(?:live|test)_)[a-zA-Z0-9_-]+/g;
|
|
769
984
|
var OptropicClient = class {
|
|
770
985
|
config;
|
|
771
986
|
baseUrl;
|
|
772
987
|
retryConfig;
|
|
773
988
|
_sandbox;
|
|
989
|
+
_debug;
|
|
774
990
|
assets;
|
|
775
991
|
audit;
|
|
776
992
|
compliance;
|
|
993
|
+
documents;
|
|
777
994
|
keys;
|
|
778
995
|
keysets;
|
|
996
|
+
provenance;
|
|
779
997
|
schemas;
|
|
780
998
|
constructor(config) {
|
|
781
999
|
if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
|
|
@@ -787,6 +1005,7 @@ var OptropicClient = class {
|
|
|
787
1005
|
...config,
|
|
788
1006
|
timeout: config.timeout ?? DEFAULT_TIMEOUT
|
|
789
1007
|
};
|
|
1008
|
+
this._debug = config.debug ?? false;
|
|
790
1009
|
if (config.sandbox !== void 0) {
|
|
791
1010
|
this._sandbox = config.sandbox;
|
|
792
1011
|
} else {
|
|
@@ -805,8 +1024,10 @@ var OptropicClient = class {
|
|
|
805
1024
|
this.assets = new AssetsResource(boundRequest, this);
|
|
806
1025
|
this.audit = new AuditResource(boundRequest);
|
|
807
1026
|
this.compliance = new ComplianceResource(boundRequest);
|
|
1027
|
+
this.documents = new DocumentsResource(boundRequest);
|
|
808
1028
|
this.keys = new KeysResource(boundRequest);
|
|
809
1029
|
this.keysets = new KeysetsResource(boundRequest);
|
|
1030
|
+
this.provenance = new ProvenanceResource(boundRequest);
|
|
810
1031
|
this.schemas = new SchemasResource(boundRequest);
|
|
811
1032
|
}
|
|
812
1033
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -825,23 +1046,59 @@ var OptropicClient = class {
|
|
|
825
1046
|
return this._sandbox ? "sandbox" : "live";
|
|
826
1047
|
}
|
|
827
1048
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1049
|
+
// DEBUG LOGGING
|
|
1050
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1051
|
+
redact(text) {
|
|
1052
|
+
return text.replace(KEY_REDACT_RE, "$1****");
|
|
1053
|
+
}
|
|
1054
|
+
logRequest(method, url, requestId, idempotencyKey) {
|
|
1055
|
+
if (!this._debug) return;
|
|
1056
|
+
const parts = [`${method} ${this.redact(url)}`, `req=${requestId}`];
|
|
1057
|
+
if (idempotencyKey) parts.push(`idempotency=${idempotencyKey}`);
|
|
1058
|
+
console.log(`[optropic] ${parts.join(" ")}`);
|
|
1059
|
+
}
|
|
1060
|
+
logResponse(method, path, status, durationMs, requestId, serverRequestId, attempt) {
|
|
1061
|
+
if (!this._debug) return;
|
|
1062
|
+
const parts = [`${method} ${path} \u2192 ${status} (${Math.round(durationMs)}ms)`, `req=${requestId}`];
|
|
1063
|
+
if (serverRequestId) parts.push(`server_req=${serverRequestId}`);
|
|
1064
|
+
if (attempt > 0) parts.push(`attempt=${attempt + 1}`);
|
|
1065
|
+
console.log(`[optropic] ${parts.join(" ")}`);
|
|
1066
|
+
}
|
|
1067
|
+
logRetry(method, path, attempt, delay, reason) {
|
|
1068
|
+
if (!this._debug) return;
|
|
1069
|
+
console.log(
|
|
1070
|
+
`[optropic] ${method} ${path} retry #${attempt + 1} in ${(delay / 1e3).toFixed(1)}s (${reason})`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
828
1074
|
// PRIVATE METHODS
|
|
829
1075
|
// ─────────────────────────────────────────────────────────────────────────
|
|
830
1076
|
isValidApiKey(apiKey) {
|
|
831
1077
|
return /^optr_(live|test)_[a-zA-Z0-9_-]{20,}$/.test(apiKey);
|
|
832
1078
|
}
|
|
833
1079
|
async request(options) {
|
|
834
|
-
const { method, path, body, headers = {}, timeout = this.config.timeout } = options;
|
|
1080
|
+
const { method, path, body, headers = {}, timeout = this.config.timeout, idempotencyKey } = options;
|
|
835
1081
|
const url = `${this.baseUrl}${path}`;
|
|
1082
|
+
const requestId = crypto.randomUUID();
|
|
836
1083
|
const requestHeaders = {
|
|
837
1084
|
"Content-Type": "application/json",
|
|
838
1085
|
"Accept": "application/json",
|
|
839
1086
|
"x-api-key": this.config.apiKey,
|
|
840
1087
|
"X-SDK-Version": SDK_VERSION,
|
|
841
1088
|
"X-SDK-Language": "typescript",
|
|
1089
|
+
"X-Request-ID": requestId,
|
|
842
1090
|
...this.config.headers,
|
|
843
1091
|
...headers
|
|
844
1092
|
};
|
|
1093
|
+
let effectiveIdempotencyKey;
|
|
1094
|
+
if (idempotencyKey) {
|
|
1095
|
+
requestHeaders["Idempotency-Key"] = idempotencyKey;
|
|
1096
|
+
effectiveIdempotencyKey = idempotencyKey;
|
|
1097
|
+
} else if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
1098
|
+
effectiveIdempotencyKey = crypto.randomUUID();
|
|
1099
|
+
requestHeaders["Idempotency-Key"] = effectiveIdempotencyKey;
|
|
1100
|
+
}
|
|
1101
|
+
this.logRequest(method, url, requestId, effectiveIdempotencyKey);
|
|
845
1102
|
let lastError = null;
|
|
846
1103
|
let attempt = 0;
|
|
847
1104
|
while (attempt <= this.retryConfig.maxRetries) {
|
|
@@ -851,7 +1108,10 @@ var OptropicClient = class {
|
|
|
851
1108
|
method,
|
|
852
1109
|
requestHeaders,
|
|
853
1110
|
body,
|
|
854
|
-
timeout
|
|
1111
|
+
timeout,
|
|
1112
|
+
requestId,
|
|
1113
|
+
path,
|
|
1114
|
+
attempt
|
|
855
1115
|
);
|
|
856
1116
|
return response;
|
|
857
1117
|
} catch (error) {
|
|
@@ -862,13 +1122,19 @@ var OptropicClient = class {
|
|
|
862
1122
|
if (attempt >= this.retryConfig.maxRetries) {
|
|
863
1123
|
throw error;
|
|
864
1124
|
}
|
|
865
|
-
const
|
|
1125
|
+
const baseDelay = Math.min(
|
|
866
1126
|
this.retryConfig.baseDelay * Math.pow(2, attempt),
|
|
867
1127
|
this.retryConfig.maxDelay
|
|
868
1128
|
);
|
|
1129
|
+
const jitter = baseDelay * 0.5 * Math.random();
|
|
1130
|
+
const delay = baseDelay + jitter;
|
|
869
1131
|
if (error instanceof RateLimitedError) {
|
|
870
|
-
|
|
1132
|
+
const retryDelay = error.retryAfter * 1e3;
|
|
1133
|
+
this.logRetry(method, path, attempt, retryDelay, "rate_limited");
|
|
1134
|
+
await this.sleep(retryDelay);
|
|
871
1135
|
} else {
|
|
1136
|
+
const statusCode = error instanceof OptropicError ? error.statusCode : 0;
|
|
1137
|
+
this.logRetry(method, path, attempt, delay, `status=${statusCode}`);
|
|
872
1138
|
await this.sleep(delay);
|
|
873
1139
|
}
|
|
874
1140
|
attempt++;
|
|
@@ -876,9 +1142,10 @@ var OptropicClient = class {
|
|
|
876
1142
|
}
|
|
877
1143
|
throw lastError ?? new OptropicError("UNKNOWN_ERROR", "Request failed");
|
|
878
1144
|
}
|
|
879
|
-
async executeRequest(url, method, headers, body, timeout) {
|
|
1145
|
+
async executeRequest(url, method, headers, body, timeout, requestId, path, attempt) {
|
|
880
1146
|
const controller = new AbortController();
|
|
881
1147
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1148
|
+
const t0 = performance.now();
|
|
882
1149
|
try {
|
|
883
1150
|
const response = await fetch(url, {
|
|
884
1151
|
method,
|
|
@@ -887,7 +1154,9 @@ var OptropicClient = class {
|
|
|
887
1154
|
signal: controller.signal
|
|
888
1155
|
});
|
|
889
1156
|
clearTimeout(timeoutId);
|
|
890
|
-
const
|
|
1157
|
+
const durationMs = performance.now() - t0;
|
|
1158
|
+
const serverRequestId = response.headers.get("x-request-id") ?? "";
|
|
1159
|
+
this.logResponse(method, path, response.status, durationMs, requestId, serverRequestId, attempt);
|
|
891
1160
|
if (!response.ok) {
|
|
892
1161
|
let errorBody;
|
|
893
1162
|
try {
|
|
@@ -910,7 +1179,7 @@ var OptropicClient = class {
|
|
|
910
1179
|
code: errorBody.code,
|
|
911
1180
|
message: errorBody.message,
|
|
912
1181
|
details: errorBody.details,
|
|
913
|
-
requestId
|
|
1182
|
+
requestId: serverRequestId || requestId
|
|
914
1183
|
});
|
|
915
1184
|
}
|
|
916
1185
|
if (response.status === 204) {
|
|
@@ -936,8 +1205,12 @@ var OptropicClient = class {
|
|
|
936
1205
|
}
|
|
937
1206
|
if (error instanceof Error) {
|
|
938
1207
|
if (error.name === "AbortError") {
|
|
1208
|
+
const durationMs2 = performance.now() - t0;
|
|
1209
|
+
this.logResponse(method, path, 408, durationMs2, requestId, "", attempt);
|
|
939
1210
|
throw new TimeoutError(timeout);
|
|
940
1211
|
}
|
|
1212
|
+
const durationMs = performance.now() - t0;
|
|
1213
|
+
this.logResponse(method, path, 0, durationMs, requestId, "", attempt);
|
|
941
1214
|
throw new NetworkError(error.message, { cause: error });
|
|
942
1215
|
}
|
|
943
1216
|
throw new OptropicError("UNKNOWN_ERROR", "An unexpected error occurred", {
|
|
@@ -953,6 +1226,203 @@ function createClient(config) {
|
|
|
953
1226
|
return new OptropicClient(config);
|
|
954
1227
|
}
|
|
955
1228
|
|
|
1229
|
+
// src/filter-verify.ts
|
|
1230
|
+
var HEADER_SIZE = 19;
|
|
1231
|
+
var SIGNATURE_SIZE = 64;
|
|
1232
|
+
var SLOTS_PER_BUCKET = 4;
|
|
1233
|
+
var StaleFilterError = class extends Error {
|
|
1234
|
+
code = "FILTER_STALE_CRITICAL";
|
|
1235
|
+
ageSeconds;
|
|
1236
|
+
trustWindowSeconds;
|
|
1237
|
+
constructor(ageSeconds, trustWindowSeconds) {
|
|
1238
|
+
super(`Filter age ${ageSeconds}s exceeds trust window ${trustWindowSeconds}s`);
|
|
1239
|
+
this.name = "StaleFilterError";
|
|
1240
|
+
this.ageSeconds = ageSeconds;
|
|
1241
|
+
this.trustWindowSeconds = trustWindowSeconds;
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
async function sha256(data) {
|
|
1245
|
+
const buf = new Uint8Array(data).buffer;
|
|
1246
|
+
const hash = await globalThis.crypto.subtle.digest("SHA-256", buf);
|
|
1247
|
+
return new Uint8Array(hash);
|
|
1248
|
+
}
|
|
1249
|
+
async function sha256Hex(input) {
|
|
1250
|
+
const encoded = new TextEncoder().encode(input);
|
|
1251
|
+
const hash = await sha256(encoded);
|
|
1252
|
+
return Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1253
|
+
}
|
|
1254
|
+
function uint32BE(buf, offset) {
|
|
1255
|
+
return (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
|
|
1256
|
+
}
|
|
1257
|
+
function uint16BE(buf, offset) {
|
|
1258
|
+
return buf[offset] << 8 | buf[offset + 1];
|
|
1259
|
+
}
|
|
1260
|
+
function int64BE(buf, offset) {
|
|
1261
|
+
const high = (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
|
|
1262
|
+
const low = (buf[offset + 4] << 24 | buf[offset + 5] << 16 | buf[offset + 6] << 8 | buf[offset + 7]) >>> 0;
|
|
1263
|
+
return high * 4294967296 + low;
|
|
1264
|
+
}
|
|
1265
|
+
async function fingerprint(item) {
|
|
1266
|
+
const encoded = new TextEncoder().encode(item);
|
|
1267
|
+
let digest = await sha256(encoded);
|
|
1268
|
+
let fp = uint16BE(digest, 4);
|
|
1269
|
+
while (fp === 0) {
|
|
1270
|
+
digest = await sha256(digest);
|
|
1271
|
+
fp = uint16BE(digest, 4);
|
|
1272
|
+
}
|
|
1273
|
+
return fp;
|
|
1274
|
+
}
|
|
1275
|
+
async function h1(item, capacity) {
|
|
1276
|
+
const encoded = new TextEncoder().encode(item);
|
|
1277
|
+
const digest = await sha256(encoded);
|
|
1278
|
+
return uint32BE(digest, 0) % capacity;
|
|
1279
|
+
}
|
|
1280
|
+
async function altIndex(index, fp, capacity) {
|
|
1281
|
+
const fpBuf = new Uint8Array(2);
|
|
1282
|
+
fpBuf[0] = fp >> 8 & 255;
|
|
1283
|
+
fpBuf[1] = fp & 255;
|
|
1284
|
+
const fpDigest = await sha256(fpBuf);
|
|
1285
|
+
return ((index ^ uint32BE(fpDigest, 0) % capacity) & 4294967295) >>> 0;
|
|
1286
|
+
}
|
|
1287
|
+
async function filterLookup(filterData, capacity, item) {
|
|
1288
|
+
const fp = await fingerprint(item);
|
|
1289
|
+
const i1 = await h1(item, capacity);
|
|
1290
|
+
const i2 = await altIndex(i1, fp, capacity);
|
|
1291
|
+
const b1Offset = i1 * SLOTS_PER_BUCKET * 2;
|
|
1292
|
+
for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
|
|
1293
|
+
const slotVal = uint16BE(filterData, b1Offset + s * 2);
|
|
1294
|
+
if (slotVal === fp) return true;
|
|
1295
|
+
}
|
|
1296
|
+
const b2Offset = i2 * SLOTS_PER_BUCKET * 2;
|
|
1297
|
+
for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
|
|
1298
|
+
const slotVal = uint16BE(filterData, b2Offset + s * 2);
|
|
1299
|
+
if (slotVal === fp) return true;
|
|
1300
|
+
}
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
function parseFilterHeader(buf) {
|
|
1304
|
+
if (buf.length < HEADER_SIZE) {
|
|
1305
|
+
throw new Error("Buffer too small for filter header");
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
version: buf[0],
|
|
1309
|
+
issuedAt: int64BE(buf, 1),
|
|
1310
|
+
itemCount: uint32BE(buf, 9),
|
|
1311
|
+
keyId: uint16BE(buf, 13),
|
|
1312
|
+
capacity: uint32BE(buf, 15)
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
function parseSaltsHeader(header) {
|
|
1316
|
+
const salts = /* @__PURE__ */ new Map();
|
|
1317
|
+
if (!header) return salts;
|
|
1318
|
+
for (const pair of header.split(",")) {
|
|
1319
|
+
const colonIdx = pair.indexOf(":");
|
|
1320
|
+
if (colonIdx === -1) continue;
|
|
1321
|
+
const tenantId = pair.slice(0, colonIdx).trim();
|
|
1322
|
+
const saltHex = pair.slice(colonIdx + 1).trim();
|
|
1323
|
+
if (tenantId && saltHex) {
|
|
1324
|
+
const bytes = new Uint8Array(saltHex.length / 2);
|
|
1325
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1326
|
+
bytes[i] = parseInt(saltHex.slice(i * 2, i * 2 + 2), 16);
|
|
1327
|
+
}
|
|
1328
|
+
salts.set(tenantId, bytes);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return salts;
|
|
1332
|
+
}
|
|
1333
|
+
function toHex(bytes) {
|
|
1334
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1335
|
+
}
|
|
1336
|
+
async function verifyOffline(options) {
|
|
1337
|
+
const {
|
|
1338
|
+
assetId,
|
|
1339
|
+
filterBytes,
|
|
1340
|
+
salts,
|
|
1341
|
+
filterPolicy = "permissive",
|
|
1342
|
+
trustWindowSeconds = 259200
|
|
1343
|
+
// 72 hours
|
|
1344
|
+
} = options;
|
|
1345
|
+
const header = parseFilterHeader(filterBytes);
|
|
1346
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
1347
|
+
const ageSeconds = nowSeconds - header.issuedAt;
|
|
1348
|
+
if (ageSeconds > trustWindowSeconds) {
|
|
1349
|
+
if (filterPolicy === "strict") {
|
|
1350
|
+
throw new StaleFilterError(ageSeconds, trustWindowSeconds);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const filterDataEnd = filterBytes.length - SIGNATURE_SIZE;
|
|
1354
|
+
const filterData = filterBytes.slice(HEADER_SIZE, filterDataEnd);
|
|
1355
|
+
const capacity = header.capacity;
|
|
1356
|
+
let revoked = false;
|
|
1357
|
+
for (const salt of salts.values()) {
|
|
1358
|
+
const saltHex = toHex(salt);
|
|
1359
|
+
const saltedInput = assetId + saltHex;
|
|
1360
|
+
const saltedHash = await sha256Hex(saltedInput);
|
|
1361
|
+
if (await filterLookup(filterData, capacity, saltedHash)) {
|
|
1362
|
+
revoked = true;
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const freshness = ageSeconds > trustWindowSeconds ? "stale" : "current";
|
|
1367
|
+
return {
|
|
1368
|
+
signatureValid: true,
|
|
1369
|
+
revocationStatus: revoked ? "revoked" : "clear",
|
|
1370
|
+
filterAgeSeconds: ageSeconds,
|
|
1371
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1372
|
+
verificationMode: "offline",
|
|
1373
|
+
freshness
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/dpp.ts
|
|
1378
|
+
function buildDPPConfig(metadata) {
|
|
1379
|
+
const config = {
|
|
1380
|
+
product_id: metadata.productId,
|
|
1381
|
+
product_name: metadata.productName,
|
|
1382
|
+
manufacturer: metadata.manufacturer,
|
|
1383
|
+
country_of_origin: metadata.countryOfOrigin,
|
|
1384
|
+
dpp_category: metadata.category
|
|
1385
|
+
};
|
|
1386
|
+
if (metadata.carbonFootprint !== void 0) config.carbon_footprint_kg_co2e = metadata.carbonFootprint;
|
|
1387
|
+
if (metadata.recycledContent !== void 0) config.recycled_content_percent = metadata.recycledContent;
|
|
1388
|
+
if (metadata.durabilityYears !== void 0) config.durability_years = metadata.durabilityYears;
|
|
1389
|
+
if (metadata.repairabilityScore !== void 0) config.repairability_score = metadata.repairabilityScore;
|
|
1390
|
+
if (metadata.substancesOfConcern) config.substances_of_concern = metadata.substancesOfConcern;
|
|
1391
|
+
if (metadata.dppRegistryId) config.dpp_registry_id = metadata.dppRegistryId;
|
|
1392
|
+
if (metadata.conformityDeclarations) config.conformity_declarations = metadata.conformityDeclarations;
|
|
1393
|
+
if (metadata.sectorData) config.sector_data = metadata.sectorData;
|
|
1394
|
+
return config;
|
|
1395
|
+
}
|
|
1396
|
+
function validateDPPMetadata(metadata) {
|
|
1397
|
+
const errors = [];
|
|
1398
|
+
if (!metadata.productId) errors.push("productId is required");
|
|
1399
|
+
if (!metadata.productName) errors.push("productName is required");
|
|
1400
|
+
if (!metadata.manufacturer) errors.push("manufacturer is required");
|
|
1401
|
+
if (!metadata.countryOfOrigin) errors.push("countryOfOrigin is required");
|
|
1402
|
+
if (metadata.countryOfOrigin && !/^[A-Z]{2}$/.test(metadata.countryOfOrigin)) {
|
|
1403
|
+
errors.push('countryOfOrigin must be ISO 3166-1 alpha-2 (e.g., "DE")');
|
|
1404
|
+
}
|
|
1405
|
+
if (metadata.recycledContent !== void 0 && (metadata.recycledContent < 0 || metadata.recycledContent > 100)) {
|
|
1406
|
+
errors.push("recycledContent must be between 0 and 100");
|
|
1407
|
+
}
|
|
1408
|
+
if (metadata.category === "battery" && metadata.sectorData) {
|
|
1409
|
+
const battery = metadata.sectorData;
|
|
1410
|
+
if (battery.type === "battery") {
|
|
1411
|
+
if (!battery.chemistry) errors.push("battery.chemistry is required for battery passports");
|
|
1412
|
+
if (!battery.capacityKwh) errors.push("battery.capacityKwh is required for battery passports");
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (metadata.category === "textile" && metadata.sectorData) {
|
|
1416
|
+
const textile = metadata.sectorData;
|
|
1417
|
+
if (textile.type === "textile") {
|
|
1418
|
+
if (!textile.fiberComposition || textile.fiberComposition.length === 0) {
|
|
1419
|
+
errors.push("textile.fiberComposition is required for textile passports");
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return { valid: errors.length === 0, errors };
|
|
1424
|
+
}
|
|
1425
|
+
|
|
956
1426
|
// src/webhooks.ts
|
|
957
1427
|
async function computeHmacSha256(secret, message) {
|
|
958
1428
|
const encoder = new TextEncoder();
|
|
@@ -999,7 +1469,7 @@ async function verifyWebhookSignature(options) {
|
|
|
999
1469
|
}
|
|
1000
1470
|
|
|
1001
1471
|
// src/index.ts
|
|
1002
|
-
var SDK_VERSION2 = "2.
|
|
1472
|
+
var SDK_VERSION2 = "2.3.0";
|
|
1003
1473
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1004
1474
|
0 && (module.exports = {
|
|
1005
1475
|
AssetsResource,
|
|
@@ -1008,6 +1478,7 @@ var SDK_VERSION2 = "2.0.0";
|
|
|
1008
1478
|
BatchNotFoundError,
|
|
1009
1479
|
CodeNotFoundError,
|
|
1010
1480
|
ComplianceResource,
|
|
1481
|
+
DocumentsResource,
|
|
1011
1482
|
InvalidCodeError,
|
|
1012
1483
|
InvalidGTINError,
|
|
1013
1484
|
InvalidSerialError,
|
|
@@ -1016,13 +1487,21 @@ var SDK_VERSION2 = "2.0.0";
|
|
|
1016
1487
|
NetworkError,
|
|
1017
1488
|
OptropicClient,
|
|
1018
1489
|
OptropicError,
|
|
1490
|
+
ProvenanceResource,
|
|
1019
1491
|
QuotaExceededError,
|
|
1020
1492
|
RateLimitedError,
|
|
1021
1493
|
RevokedCodeError,
|
|
1022
1494
|
SDK_VERSION,
|
|
1023
1495
|
SchemasResource,
|
|
1024
1496
|
ServiceUnavailableError,
|
|
1497
|
+
StaleFilterError,
|
|
1025
1498
|
TimeoutError,
|
|
1499
|
+
buildDPPConfig,
|
|
1026
1500
|
createClient,
|
|
1501
|
+
createErrorFromResponse,
|
|
1502
|
+
parseFilterHeader,
|
|
1503
|
+
parseSaltsHeader,
|
|
1504
|
+
validateDPPMetadata,
|
|
1505
|
+
verifyOffline,
|
|
1027
1506
|
verifyWebhookSignature
|
|
1028
1507
|
});
|