optropic 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +682 -4
- package/dist/index.d.cts +701 -3
- package/dist/index.d.ts +701 -3
- package/dist/index.js +670 -4
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -31,9 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AssetsResource: () => AssetsResource,
|
|
34
|
+
AuditResource: () => AuditResource,
|
|
34
35
|
AuthenticationError: () => AuthenticationError,
|
|
35
36
|
BatchNotFoundError: () => BatchNotFoundError,
|
|
36
37
|
CodeNotFoundError: () => CodeNotFoundError,
|
|
38
|
+
ComplianceResource: () => ComplianceResource,
|
|
39
|
+
DocumentsResource: () => DocumentsResource,
|
|
37
40
|
InvalidCodeError: () => InvalidCodeError,
|
|
38
41
|
InvalidGTINError: () => InvalidGTINError,
|
|
39
42
|
InvalidSerialError: () => InvalidSerialError,
|
|
@@ -42,13 +45,22 @@ __export(index_exports, {
|
|
|
42
45
|
NetworkError: () => NetworkError,
|
|
43
46
|
OptropicClient: () => OptropicClient,
|
|
44
47
|
OptropicError: () => OptropicError,
|
|
48
|
+
ProvenanceResource: () => ProvenanceResource,
|
|
45
49
|
QuotaExceededError: () => QuotaExceededError,
|
|
46
50
|
RateLimitedError: () => RateLimitedError,
|
|
47
51
|
RevokedCodeError: () => RevokedCodeError,
|
|
48
52
|
SDK_VERSION: () => SDK_VERSION2,
|
|
53
|
+
SchemasResource: () => SchemasResource,
|
|
49
54
|
ServiceUnavailableError: () => ServiceUnavailableError,
|
|
55
|
+
StaleFilterError: () => StaleFilterError,
|
|
50
56
|
TimeoutError: () => TimeoutError,
|
|
57
|
+
buildDPPConfig: () => buildDPPConfig,
|
|
51
58
|
createClient: () => createClient,
|
|
59
|
+
createErrorFromResponse: () => createErrorFromResponse,
|
|
60
|
+
parseFilterHeader: () => parseFilterHeader,
|
|
61
|
+
parseSaltsHeader: () => parseSaltsHeader,
|
|
62
|
+
validateDPPMetadata: () => validateDPPMetadata,
|
|
63
|
+
verifyOffline: () => verifyOffline,
|
|
52
64
|
verifyWebhookSignature: () => verifyWebhookSignature
|
|
53
65
|
});
|
|
54
66
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -459,6 +471,31 @@ var AssetsResource = class {
|
|
|
459
471
|
const query = effectiveParams ? this.buildQuery(effectiveParams) : "";
|
|
460
472
|
return this.request({ method: "GET", path: `/v1/assets${query}` });
|
|
461
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Auto-paginate through all assets, yielding pages of results.
|
|
476
|
+
* Returns an async generator that fetches pages on demand.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```typescript
|
|
480
|
+
* for await (const asset of client.assets.listAll({ status: 'active' })) {
|
|
481
|
+
* console.log(asset.id);
|
|
482
|
+
* }
|
|
483
|
+
* ```
|
|
484
|
+
*/
|
|
485
|
+
async *listAll(params) {
|
|
486
|
+
let page = params?.page ?? 1;
|
|
487
|
+
const perPage = params?.per_page ?? 100;
|
|
488
|
+
while (true) {
|
|
489
|
+
const response = await this.list({ ...params, page, per_page: perPage });
|
|
490
|
+
for (const asset of response.data) {
|
|
491
|
+
yield asset;
|
|
492
|
+
}
|
|
493
|
+
if (page >= response.pagination.totalPages || response.data.length === 0) {
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
page++;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
462
499
|
async get(assetId) {
|
|
463
500
|
return this.request({ method: "GET", path: `/v1/assets/${encodeURIComponent(assetId)}` });
|
|
464
501
|
}
|
|
@@ -482,6 +519,209 @@ var AssetsResource = class {
|
|
|
482
519
|
}
|
|
483
520
|
};
|
|
484
521
|
|
|
522
|
+
// src/resources/audit.ts
|
|
523
|
+
var AuditResource = class {
|
|
524
|
+
constructor(request) {
|
|
525
|
+
this.request = request;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* List audit events with optional filtering and pagination.
|
|
529
|
+
*/
|
|
530
|
+
async list(params) {
|
|
531
|
+
const query = params ? this.buildQuery(params) : "";
|
|
532
|
+
return this.request({ method: "GET", path: `/v1/audit${query}` });
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Retrieve a single audit event by ID.
|
|
536
|
+
*/
|
|
537
|
+
async get(eventId) {
|
|
538
|
+
return this.request({
|
|
539
|
+
method: "GET",
|
|
540
|
+
path: `/v1/audit/${encodeURIComponent(eventId)}`
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Record a custom audit event.
|
|
545
|
+
*/
|
|
546
|
+
async create(params) {
|
|
547
|
+
return this.request({
|
|
548
|
+
method: "POST",
|
|
549
|
+
path: "/v1/audit",
|
|
550
|
+
body: {
|
|
551
|
+
event_type: params.eventType,
|
|
552
|
+
...params.resourceId !== void 0 && { resource_id: params.resourceId },
|
|
553
|
+
...params.resourceType !== void 0 && { resource_type: params.resourceType },
|
|
554
|
+
...params.details !== void 0 && { details: params.details }
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
buildQuery(params) {
|
|
559
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0);
|
|
560
|
+
if (entries.length === 0) return "";
|
|
561
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// src/resources/compliance.ts
|
|
566
|
+
var ComplianceResource = class {
|
|
567
|
+
constructor(request) {
|
|
568
|
+
this.request = request;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Verify the integrity of the full audit chain.
|
|
572
|
+
*/
|
|
573
|
+
async verifyChain() {
|
|
574
|
+
return this.request({
|
|
575
|
+
method: "POST",
|
|
576
|
+
path: "/v1/compliance/verify-chain"
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Return all Merkle roots.
|
|
581
|
+
*/
|
|
582
|
+
async listMerkleRoots() {
|
|
583
|
+
return this.request({
|
|
584
|
+
method: "GET",
|
|
585
|
+
path: "/v1/compliance/merkle-roots"
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Return a Merkle inclusion proof for a specific audit event.
|
|
590
|
+
*/
|
|
591
|
+
async getMerkleProof(eventId) {
|
|
592
|
+
return this.request({
|
|
593
|
+
method: "GET",
|
|
594
|
+
path: `/v1/compliance/merkle-proof/${encodeURIComponent(eventId)}`
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Export audit data as a signed CSV.
|
|
599
|
+
*/
|
|
600
|
+
async exportAudit(params) {
|
|
601
|
+
const query = params ? this.buildQuery(params) : "";
|
|
602
|
+
return this.request({
|
|
603
|
+
method: "GET",
|
|
604
|
+
path: `/v1/compliance/export${query}`
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Retrieve the current compliance configuration.
|
|
609
|
+
*/
|
|
610
|
+
async getConfig() {
|
|
611
|
+
return this.request({
|
|
612
|
+
method: "GET",
|
|
613
|
+
path: "/v1/compliance/config"
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Update the compliance mode.
|
|
618
|
+
*/
|
|
619
|
+
async updateConfig(mode) {
|
|
620
|
+
return this.request({
|
|
621
|
+
method: "POST",
|
|
622
|
+
path: "/v1/compliance/config",
|
|
623
|
+
body: { compliance_mode: mode }
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
buildQuery(params) {
|
|
627
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0);
|
|
628
|
+
if (entries.length === 0) return "";
|
|
629
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// src/resources/documents.ts
|
|
634
|
+
var DocumentsResource = class {
|
|
635
|
+
constructor(request) {
|
|
636
|
+
this.request = request;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Enroll a new document (substrate fingerprint) linked to an asset.
|
|
640
|
+
*
|
|
641
|
+
* @example
|
|
642
|
+
* ```typescript
|
|
643
|
+
* const doc = await client.documents.enroll({
|
|
644
|
+
* assetId: 'asset-123',
|
|
645
|
+
* fingerprintHash: 'sha256:abc123...',
|
|
646
|
+
* descriptorVersion: 'GB_GE_M7PCA_v1',
|
|
647
|
+
* substrateType: 'S_fb',
|
|
648
|
+
* captureDevice: 'iPhone16ProMax_main',
|
|
649
|
+
* });
|
|
650
|
+
* ```
|
|
651
|
+
*/
|
|
652
|
+
async enroll(params) {
|
|
653
|
+
return this.request({
|
|
654
|
+
method: "POST",
|
|
655
|
+
path: "/v1/documents",
|
|
656
|
+
body: {
|
|
657
|
+
asset_id: params.assetId,
|
|
658
|
+
fingerprint_hash: params.fingerprintHash,
|
|
659
|
+
descriptor_version: params.descriptorVersion,
|
|
660
|
+
substrate_type: params.substrateType,
|
|
661
|
+
...params.captureDevice !== void 0 && { capture_device: params.captureDevice },
|
|
662
|
+
...params.metadata !== void 0 && { metadata: params.metadata }
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Verify a fingerprint against enrolled documents.
|
|
668
|
+
*
|
|
669
|
+
* Returns the best match if similarity exceeds the threshold.
|
|
670
|
+
*/
|
|
671
|
+
async verify(params) {
|
|
672
|
+
return this.request({
|
|
673
|
+
method: "POST",
|
|
674
|
+
path: "/v1/documents/verify",
|
|
675
|
+
body: {
|
|
676
|
+
fingerprint_hash: params.fingerprintHash,
|
|
677
|
+
descriptor_version: params.descriptorVersion,
|
|
678
|
+
...params.threshold !== void 0 && { threshold: params.threshold }
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Get a single document by ID.
|
|
684
|
+
*/
|
|
685
|
+
async get(documentId) {
|
|
686
|
+
return this.request({
|
|
687
|
+
method: "GET",
|
|
688
|
+
path: `/v1/documents/${encodeURIComponent(documentId)}`
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* List enrolled documents with optional filtering.
|
|
693
|
+
*/
|
|
694
|
+
async list(params) {
|
|
695
|
+
const query = params ? this.buildQuery(params) : "";
|
|
696
|
+
return this.request({
|
|
697
|
+
method: "GET",
|
|
698
|
+
path: `/v1/documents${query}`
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Supersede a document (e.g., re-enrollment with better capture).
|
|
703
|
+
*/
|
|
704
|
+
async supersede(documentId, newDocumentId) {
|
|
705
|
+
return this.request({
|
|
706
|
+
method: "POST",
|
|
707
|
+
path: `/v1/documents/${encodeURIComponent(documentId)}/supersede`,
|
|
708
|
+
body: { new_document_id: newDocumentId }
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
buildQuery(params) {
|
|
712
|
+
const mapped = {
|
|
713
|
+
asset_id: params.assetId,
|
|
714
|
+
substrate_type: params.substrateType,
|
|
715
|
+
status: params.status,
|
|
716
|
+
page: params.page,
|
|
717
|
+
per_page: params.per_page
|
|
718
|
+
};
|
|
719
|
+
const entries = Object.entries(mapped).filter(([, v]) => v !== void 0);
|
|
720
|
+
if (entries.length === 0) return "";
|
|
721
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
485
725
|
// src/resources/keys.ts
|
|
486
726
|
var KeysResource = class {
|
|
487
727
|
constructor(request) {
|
|
@@ -518,10 +758,222 @@ var KeysetsResource = class {
|
|
|
518
758
|
}
|
|
519
759
|
};
|
|
520
760
|
|
|
761
|
+
// src/resources/provenance.ts
|
|
762
|
+
var ProvenanceResource = class {
|
|
763
|
+
constructor(request) {
|
|
764
|
+
this.request = request;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Record a new provenance event in the chain.
|
|
768
|
+
*
|
|
769
|
+
* Events are automatically chained — the server links each new event
|
|
770
|
+
* to the previous one via cryptographic hash.
|
|
771
|
+
*
|
|
772
|
+
* @example
|
|
773
|
+
* ```typescript
|
|
774
|
+
* const event = await client.provenance.record({
|
|
775
|
+
* assetId: 'asset-123',
|
|
776
|
+
* eventType: 'manufactured',
|
|
777
|
+
* actor: 'factory-line-7',
|
|
778
|
+
* location: { country: 'DE', facility: 'Munich Plant' },
|
|
779
|
+
* });
|
|
780
|
+
* ```
|
|
781
|
+
*/
|
|
782
|
+
async record(params) {
|
|
783
|
+
return this.request({
|
|
784
|
+
method: "POST",
|
|
785
|
+
path: "/v1/provenance",
|
|
786
|
+
body: {
|
|
787
|
+
asset_id: params.assetId,
|
|
788
|
+
event_type: params.eventType,
|
|
789
|
+
actor: params.actor,
|
|
790
|
+
...params.location !== void 0 && { location: params.location },
|
|
791
|
+
...params.metadata !== void 0 && { metadata: params.metadata }
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Get the full provenance chain for an asset.
|
|
797
|
+
*/
|
|
798
|
+
async getChain(assetId) {
|
|
799
|
+
return this.request({
|
|
800
|
+
method: "GET",
|
|
801
|
+
path: `/v1/provenance/chain/${encodeURIComponent(assetId)}`
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Get a single provenance event by ID.
|
|
806
|
+
*/
|
|
807
|
+
async get(eventId) {
|
|
808
|
+
return this.request({
|
|
809
|
+
method: "GET",
|
|
810
|
+
path: `/v1/provenance/${encodeURIComponent(eventId)}`
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* List provenance events with optional filtering.
|
|
815
|
+
*/
|
|
816
|
+
async list(params) {
|
|
817
|
+
const query = params ? this.buildQuery(params) : "";
|
|
818
|
+
return this.request({
|
|
819
|
+
method: "GET",
|
|
820
|
+
path: `/v1/provenance${query}`
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Verify the integrity of an asset's provenance chain.
|
|
825
|
+
*
|
|
826
|
+
* Checks that all events are correctly linked via cryptographic hashes.
|
|
827
|
+
*/
|
|
828
|
+
async verifyChain(assetId) {
|
|
829
|
+
return this.request({
|
|
830
|
+
method: "POST",
|
|
831
|
+
path: `/v1/provenance/chain/${encodeURIComponent(assetId)}/verify`
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
buildQuery(params) {
|
|
835
|
+
const mapped = {
|
|
836
|
+
asset_id: params.assetId,
|
|
837
|
+
event_type: params.eventType,
|
|
838
|
+
from_date: params.from_date,
|
|
839
|
+
to_date: params.to_date,
|
|
840
|
+
page: params.page,
|
|
841
|
+
per_page: params.per_page
|
|
842
|
+
};
|
|
843
|
+
const entries = Object.entries(mapped).filter(([, v]) => v !== void 0);
|
|
844
|
+
if (entries.length === 0) return "";
|
|
845
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// src/resources/schemas.ts
|
|
850
|
+
function checkType(value, expected) {
|
|
851
|
+
switch (expected) {
|
|
852
|
+
case "string":
|
|
853
|
+
return typeof value === "string";
|
|
854
|
+
case "number":
|
|
855
|
+
return typeof value === "number" && !Number.isNaN(value);
|
|
856
|
+
case "boolean":
|
|
857
|
+
return typeof value === "boolean";
|
|
858
|
+
case "date":
|
|
859
|
+
return typeof value === "string";
|
|
860
|
+
// ISO 8601 string
|
|
861
|
+
case "array":
|
|
862
|
+
return Array.isArray(value);
|
|
863
|
+
default:
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
var SchemasResource = class {
|
|
868
|
+
constructor(request) {
|
|
869
|
+
this.request = request;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Register or update a vertical config schema.
|
|
873
|
+
* If a schema already exists for the verticalId, it will be updated.
|
|
874
|
+
*/
|
|
875
|
+
async create(params) {
|
|
876
|
+
const body = this.stripUndefined({
|
|
877
|
+
vertical_id: params.verticalId,
|
|
878
|
+
metadata_schema: params.metadataSchema,
|
|
879
|
+
version: params.version,
|
|
880
|
+
export_formats: params.exportFormats,
|
|
881
|
+
description: params.description
|
|
882
|
+
});
|
|
883
|
+
return this.request({ method: "POST", path: "/v1/schemas", body });
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* List registered vertical schemas with pagination.
|
|
887
|
+
*/
|
|
888
|
+
async list(params) {
|
|
889
|
+
const query = params ? this.buildQuery(params) : "";
|
|
890
|
+
return this.request({ method: "GET", path: `/v1/schemas${query}` });
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Get the active schema for a specific vertical.
|
|
894
|
+
*/
|
|
895
|
+
async get(verticalId) {
|
|
896
|
+
return this.request({
|
|
897
|
+
method: "GET",
|
|
898
|
+
path: `/v1/schemas/${encodeURIComponent(verticalId)}`
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Update an existing vertical schema.
|
|
903
|
+
*/
|
|
904
|
+
async update(verticalId, params) {
|
|
905
|
+
const body = this.stripUndefined({
|
|
906
|
+
version: params.version,
|
|
907
|
+
metadata_schema: params.metadataSchema,
|
|
908
|
+
export_formats: params.exportFormats,
|
|
909
|
+
description: params.description,
|
|
910
|
+
is_active: params.isActive
|
|
911
|
+
});
|
|
912
|
+
return this.request({
|
|
913
|
+
method: "PUT",
|
|
914
|
+
path: `/v1/schemas/${encodeURIComponent(verticalId)}`,
|
|
915
|
+
body
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Deactivate a vertical schema (soft delete).
|
|
920
|
+
*/
|
|
921
|
+
async delete(verticalId) {
|
|
922
|
+
await this.request({
|
|
923
|
+
method: "DELETE",
|
|
924
|
+
path: `/v1/schemas/${encodeURIComponent(verticalId)}`
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Pre-flight validation: check if assetConfig matches the registered schema.
|
|
929
|
+
*
|
|
930
|
+
* This is a client-side convenience that fetches the schema and validates locally.
|
|
931
|
+
* The server also validates on asset creation.
|
|
932
|
+
*/
|
|
933
|
+
async validate(verticalId, assetConfig) {
|
|
934
|
+
let schema;
|
|
935
|
+
try {
|
|
936
|
+
schema = await this.get(verticalId);
|
|
937
|
+
} catch {
|
|
938
|
+
return { valid: true, errors: [] };
|
|
939
|
+
}
|
|
940
|
+
const errors = [];
|
|
941
|
+
const metadataSchema = schema.metadataSchema ?? {};
|
|
942
|
+
for (const [fieldName, fieldDef] of Object.entries(metadataSchema)) {
|
|
943
|
+
if (typeof fieldDef !== "object" || fieldDef === null) continue;
|
|
944
|
+
const def = fieldDef;
|
|
945
|
+
const value = assetConfig[fieldName];
|
|
946
|
+
if (def.required && (value === void 0 || value === null || value === "")) {
|
|
947
|
+
const label = def.label ?? fieldName;
|
|
948
|
+
errors.push({ field: fieldName, message: `Required field "${label}" is missing` });
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
if (value === void 0 || value === null) continue;
|
|
952
|
+
const expectedType = def.type ?? "string";
|
|
953
|
+
if (!checkType(value, expectedType)) {
|
|
954
|
+
errors.push({
|
|
955
|
+
field: fieldName,
|
|
956
|
+
message: `"${def.label ?? fieldName}" must be a ${expectedType}`,
|
|
957
|
+
received: typeof value
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return { valid: errors.length === 0, errors };
|
|
962
|
+
}
|
|
963
|
+
buildQuery(params) {
|
|
964
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0);
|
|
965
|
+
if (entries.length === 0) return "";
|
|
966
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
967
|
+
}
|
|
968
|
+
stripUndefined(obj) {
|
|
969
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
|
|
521
973
|
// src/client.ts
|
|
522
974
|
var DEFAULT_BASE_URL = "https://api.optropic.com";
|
|
523
975
|
var DEFAULT_TIMEOUT = 3e4;
|
|
524
|
-
var SDK_VERSION = "2.
|
|
976
|
+
var SDK_VERSION = "2.2.0";
|
|
525
977
|
var SANDBOX_PREFIXES = ["optr_test_"];
|
|
526
978
|
var DEFAULT_RETRY_CONFIG = {
|
|
527
979
|
maxRetries: 3,
|
|
@@ -534,8 +986,13 @@ var OptropicClient = class {
|
|
|
534
986
|
retryConfig;
|
|
535
987
|
_sandbox;
|
|
536
988
|
assets;
|
|
989
|
+
audit;
|
|
990
|
+
compliance;
|
|
991
|
+
documents;
|
|
537
992
|
keys;
|
|
538
993
|
keysets;
|
|
994
|
+
provenance;
|
|
995
|
+
schemas;
|
|
539
996
|
constructor(config) {
|
|
540
997
|
if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
|
|
541
998
|
throw new AuthenticationError(
|
|
@@ -562,8 +1019,13 @@ var OptropicClient = class {
|
|
|
562
1019
|
};
|
|
563
1020
|
const boundRequest = this.request.bind(this);
|
|
564
1021
|
this.assets = new AssetsResource(boundRequest, this);
|
|
1022
|
+
this.audit = new AuditResource(boundRequest);
|
|
1023
|
+
this.compliance = new ComplianceResource(boundRequest);
|
|
1024
|
+
this.documents = new DocumentsResource(boundRequest);
|
|
565
1025
|
this.keys = new KeysResource(boundRequest);
|
|
566
1026
|
this.keysets = new KeysetsResource(boundRequest);
|
|
1027
|
+
this.provenance = new ProvenanceResource(boundRequest);
|
|
1028
|
+
this.schemas = new SchemasResource(boundRequest);
|
|
567
1029
|
}
|
|
568
1030
|
// ─────────────────────────────────────────────────────────────────────────
|
|
569
1031
|
// ENVIRONMENT DETECTION
|
|
@@ -587,7 +1049,7 @@ var OptropicClient = class {
|
|
|
587
1049
|
return /^optr_(live|test)_[a-zA-Z0-9_-]{20,}$/.test(apiKey);
|
|
588
1050
|
}
|
|
589
1051
|
async request(options) {
|
|
590
|
-
const { method, path, body, headers = {}, timeout = this.config.timeout } = options;
|
|
1052
|
+
const { method, path, body, headers = {}, timeout = this.config.timeout, idempotencyKey } = options;
|
|
591
1053
|
const url = `${this.baseUrl}${path}`;
|
|
592
1054
|
const requestHeaders = {
|
|
593
1055
|
"Content-Type": "application/json",
|
|
@@ -598,6 +1060,11 @@ var OptropicClient = class {
|
|
|
598
1060
|
...this.config.headers,
|
|
599
1061
|
...headers
|
|
600
1062
|
};
|
|
1063
|
+
if (idempotencyKey) {
|
|
1064
|
+
requestHeaders["Idempotency-Key"] = idempotencyKey;
|
|
1065
|
+
} else if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
1066
|
+
requestHeaders["Idempotency-Key"] = crypto.randomUUID();
|
|
1067
|
+
}
|
|
601
1068
|
let lastError = null;
|
|
602
1069
|
let attempt = 0;
|
|
603
1070
|
while (attempt <= this.retryConfig.maxRetries) {
|
|
@@ -618,10 +1085,12 @@ var OptropicClient = class {
|
|
|
618
1085
|
if (attempt >= this.retryConfig.maxRetries) {
|
|
619
1086
|
throw error;
|
|
620
1087
|
}
|
|
621
|
-
const
|
|
1088
|
+
const baseDelay = Math.min(
|
|
622
1089
|
this.retryConfig.baseDelay * Math.pow(2, attempt),
|
|
623
1090
|
this.retryConfig.maxDelay
|
|
624
1091
|
);
|
|
1092
|
+
const jitter = baseDelay * 0.5 * Math.random();
|
|
1093
|
+
const delay = baseDelay + jitter;
|
|
625
1094
|
if (error instanceof RateLimitedError) {
|
|
626
1095
|
await this.sleep(error.retryAfter * 1e3);
|
|
627
1096
|
} else {
|
|
@@ -709,6 +1178,203 @@ function createClient(config) {
|
|
|
709
1178
|
return new OptropicClient(config);
|
|
710
1179
|
}
|
|
711
1180
|
|
|
1181
|
+
// src/filter-verify.ts
|
|
1182
|
+
var HEADER_SIZE = 19;
|
|
1183
|
+
var SIGNATURE_SIZE = 64;
|
|
1184
|
+
var SLOTS_PER_BUCKET = 4;
|
|
1185
|
+
var StaleFilterError = class extends Error {
|
|
1186
|
+
code = "FILTER_STALE_CRITICAL";
|
|
1187
|
+
ageSeconds;
|
|
1188
|
+
trustWindowSeconds;
|
|
1189
|
+
constructor(ageSeconds, trustWindowSeconds) {
|
|
1190
|
+
super(`Filter age ${ageSeconds}s exceeds trust window ${trustWindowSeconds}s`);
|
|
1191
|
+
this.name = "StaleFilterError";
|
|
1192
|
+
this.ageSeconds = ageSeconds;
|
|
1193
|
+
this.trustWindowSeconds = trustWindowSeconds;
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
async function sha256(data) {
|
|
1197
|
+
const buf = new Uint8Array(data).buffer;
|
|
1198
|
+
const hash = await globalThis.crypto.subtle.digest("SHA-256", buf);
|
|
1199
|
+
return new Uint8Array(hash);
|
|
1200
|
+
}
|
|
1201
|
+
async function sha256Hex(input) {
|
|
1202
|
+
const encoded = new TextEncoder().encode(input);
|
|
1203
|
+
const hash = await sha256(encoded);
|
|
1204
|
+
return Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1205
|
+
}
|
|
1206
|
+
function uint32BE(buf, offset) {
|
|
1207
|
+
return (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
|
|
1208
|
+
}
|
|
1209
|
+
function uint16BE(buf, offset) {
|
|
1210
|
+
return buf[offset] << 8 | buf[offset + 1];
|
|
1211
|
+
}
|
|
1212
|
+
function int64BE(buf, offset) {
|
|
1213
|
+
const high = (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
|
|
1214
|
+
const low = (buf[offset + 4] << 24 | buf[offset + 5] << 16 | buf[offset + 6] << 8 | buf[offset + 7]) >>> 0;
|
|
1215
|
+
return high * 4294967296 + low;
|
|
1216
|
+
}
|
|
1217
|
+
async function fingerprint(item) {
|
|
1218
|
+
const encoded = new TextEncoder().encode(item);
|
|
1219
|
+
let digest = await sha256(encoded);
|
|
1220
|
+
let fp = uint16BE(digest, 4);
|
|
1221
|
+
while (fp === 0) {
|
|
1222
|
+
digest = await sha256(digest);
|
|
1223
|
+
fp = uint16BE(digest, 4);
|
|
1224
|
+
}
|
|
1225
|
+
return fp;
|
|
1226
|
+
}
|
|
1227
|
+
async function h1(item, capacity) {
|
|
1228
|
+
const encoded = new TextEncoder().encode(item);
|
|
1229
|
+
const digest = await sha256(encoded);
|
|
1230
|
+
return uint32BE(digest, 0) % capacity;
|
|
1231
|
+
}
|
|
1232
|
+
async function altIndex(index, fp, capacity) {
|
|
1233
|
+
const fpBuf = new Uint8Array(2);
|
|
1234
|
+
fpBuf[0] = fp >> 8 & 255;
|
|
1235
|
+
fpBuf[1] = fp & 255;
|
|
1236
|
+
const fpDigest = await sha256(fpBuf);
|
|
1237
|
+
return ((index ^ uint32BE(fpDigest, 0) % capacity) & 4294967295) >>> 0;
|
|
1238
|
+
}
|
|
1239
|
+
async function filterLookup(filterData, capacity, item) {
|
|
1240
|
+
const fp = await fingerprint(item);
|
|
1241
|
+
const i1 = await h1(item, capacity);
|
|
1242
|
+
const i2 = await altIndex(i1, fp, capacity);
|
|
1243
|
+
const b1Offset = i1 * SLOTS_PER_BUCKET * 2;
|
|
1244
|
+
for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
|
|
1245
|
+
const slotVal = uint16BE(filterData, b1Offset + s * 2);
|
|
1246
|
+
if (slotVal === fp) return true;
|
|
1247
|
+
}
|
|
1248
|
+
const b2Offset = i2 * SLOTS_PER_BUCKET * 2;
|
|
1249
|
+
for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
|
|
1250
|
+
const slotVal = uint16BE(filterData, b2Offset + s * 2);
|
|
1251
|
+
if (slotVal === fp) return true;
|
|
1252
|
+
}
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
function parseFilterHeader(buf) {
|
|
1256
|
+
if (buf.length < HEADER_SIZE) {
|
|
1257
|
+
throw new Error("Buffer too small for filter header");
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
version: buf[0],
|
|
1261
|
+
issuedAt: int64BE(buf, 1),
|
|
1262
|
+
itemCount: uint32BE(buf, 9),
|
|
1263
|
+
keyId: uint16BE(buf, 13),
|
|
1264
|
+
capacity: uint32BE(buf, 15)
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
function parseSaltsHeader(header) {
|
|
1268
|
+
const salts = /* @__PURE__ */ new Map();
|
|
1269
|
+
if (!header) return salts;
|
|
1270
|
+
for (const pair of header.split(",")) {
|
|
1271
|
+
const colonIdx = pair.indexOf(":");
|
|
1272
|
+
if (colonIdx === -1) continue;
|
|
1273
|
+
const tenantId = pair.slice(0, colonIdx).trim();
|
|
1274
|
+
const saltHex = pair.slice(colonIdx + 1).trim();
|
|
1275
|
+
if (tenantId && saltHex) {
|
|
1276
|
+
const bytes = new Uint8Array(saltHex.length / 2);
|
|
1277
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1278
|
+
bytes[i] = parseInt(saltHex.slice(i * 2, i * 2 + 2), 16);
|
|
1279
|
+
}
|
|
1280
|
+
salts.set(tenantId, bytes);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return salts;
|
|
1284
|
+
}
|
|
1285
|
+
function toHex(bytes) {
|
|
1286
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1287
|
+
}
|
|
1288
|
+
async function verifyOffline(options) {
|
|
1289
|
+
const {
|
|
1290
|
+
assetId,
|
|
1291
|
+
filterBytes,
|
|
1292
|
+
salts,
|
|
1293
|
+
filterPolicy = "permissive",
|
|
1294
|
+
trustWindowSeconds = 259200
|
|
1295
|
+
// 72 hours
|
|
1296
|
+
} = options;
|
|
1297
|
+
const header = parseFilterHeader(filterBytes);
|
|
1298
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
1299
|
+
const ageSeconds = nowSeconds - header.issuedAt;
|
|
1300
|
+
if (ageSeconds > trustWindowSeconds) {
|
|
1301
|
+
if (filterPolicy === "strict") {
|
|
1302
|
+
throw new StaleFilterError(ageSeconds, trustWindowSeconds);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const filterDataEnd = filterBytes.length - SIGNATURE_SIZE;
|
|
1306
|
+
const filterData = filterBytes.slice(HEADER_SIZE, filterDataEnd);
|
|
1307
|
+
const capacity = header.capacity;
|
|
1308
|
+
let revoked = false;
|
|
1309
|
+
for (const salt of salts.values()) {
|
|
1310
|
+
const saltHex = toHex(salt);
|
|
1311
|
+
const saltedInput = assetId + saltHex;
|
|
1312
|
+
const saltedHash = await sha256Hex(saltedInput);
|
|
1313
|
+
if (await filterLookup(filterData, capacity, saltedHash)) {
|
|
1314
|
+
revoked = true;
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const freshness = ageSeconds > trustWindowSeconds ? "stale" : "current";
|
|
1319
|
+
return {
|
|
1320
|
+
signatureValid: true,
|
|
1321
|
+
revocationStatus: revoked ? "revoked" : "clear",
|
|
1322
|
+
filterAgeSeconds: ageSeconds,
|
|
1323
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1324
|
+
verificationMode: "offline",
|
|
1325
|
+
freshness
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/dpp.ts
|
|
1330
|
+
function buildDPPConfig(metadata) {
|
|
1331
|
+
const config = {
|
|
1332
|
+
product_id: metadata.productId,
|
|
1333
|
+
product_name: metadata.productName,
|
|
1334
|
+
manufacturer: metadata.manufacturer,
|
|
1335
|
+
country_of_origin: metadata.countryOfOrigin,
|
|
1336
|
+
dpp_category: metadata.category
|
|
1337
|
+
};
|
|
1338
|
+
if (metadata.carbonFootprint !== void 0) config.carbon_footprint_kg_co2e = metadata.carbonFootprint;
|
|
1339
|
+
if (metadata.recycledContent !== void 0) config.recycled_content_percent = metadata.recycledContent;
|
|
1340
|
+
if (metadata.durabilityYears !== void 0) config.durability_years = metadata.durabilityYears;
|
|
1341
|
+
if (metadata.repairabilityScore !== void 0) config.repairability_score = metadata.repairabilityScore;
|
|
1342
|
+
if (metadata.substancesOfConcern) config.substances_of_concern = metadata.substancesOfConcern;
|
|
1343
|
+
if (metadata.dppRegistryId) config.dpp_registry_id = metadata.dppRegistryId;
|
|
1344
|
+
if (metadata.conformityDeclarations) config.conformity_declarations = metadata.conformityDeclarations;
|
|
1345
|
+
if (metadata.sectorData) config.sector_data = metadata.sectorData;
|
|
1346
|
+
return config;
|
|
1347
|
+
}
|
|
1348
|
+
function validateDPPMetadata(metadata) {
|
|
1349
|
+
const errors = [];
|
|
1350
|
+
if (!metadata.productId) errors.push("productId is required");
|
|
1351
|
+
if (!metadata.productName) errors.push("productName is required");
|
|
1352
|
+
if (!metadata.manufacturer) errors.push("manufacturer is required");
|
|
1353
|
+
if (!metadata.countryOfOrigin) errors.push("countryOfOrigin is required");
|
|
1354
|
+
if (metadata.countryOfOrigin && !/^[A-Z]{2}$/.test(metadata.countryOfOrigin)) {
|
|
1355
|
+
errors.push('countryOfOrigin must be ISO 3166-1 alpha-2 (e.g., "DE")');
|
|
1356
|
+
}
|
|
1357
|
+
if (metadata.recycledContent !== void 0 && (metadata.recycledContent < 0 || metadata.recycledContent > 100)) {
|
|
1358
|
+
errors.push("recycledContent must be between 0 and 100");
|
|
1359
|
+
}
|
|
1360
|
+
if (metadata.category === "battery" && metadata.sectorData) {
|
|
1361
|
+
const battery = metadata.sectorData;
|
|
1362
|
+
if (battery.type === "battery") {
|
|
1363
|
+
if (!battery.chemistry) errors.push("battery.chemistry is required for battery passports");
|
|
1364
|
+
if (!battery.capacityKwh) errors.push("battery.capacityKwh is required for battery passports");
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (metadata.category === "textile" && metadata.sectorData) {
|
|
1368
|
+
const textile = metadata.sectorData;
|
|
1369
|
+
if (textile.type === "textile") {
|
|
1370
|
+
if (!textile.fiberComposition || textile.fiberComposition.length === 0) {
|
|
1371
|
+
errors.push("textile.fiberComposition is required for textile passports");
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return { valid: errors.length === 0, errors };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
712
1378
|
// src/webhooks.ts
|
|
713
1379
|
async function computeHmacSha256(secret, message) {
|
|
714
1380
|
const encoder = new TextEncoder();
|
|
@@ -755,13 +1421,16 @@ async function verifyWebhookSignature(options) {
|
|
|
755
1421
|
}
|
|
756
1422
|
|
|
757
1423
|
// src/index.ts
|
|
758
|
-
var SDK_VERSION2 = "2.
|
|
1424
|
+
var SDK_VERSION2 = "2.2.0";
|
|
759
1425
|
// Annotate the CommonJS export names for ESM import in node:
|
|
760
1426
|
0 && (module.exports = {
|
|
761
1427
|
AssetsResource,
|
|
1428
|
+
AuditResource,
|
|
762
1429
|
AuthenticationError,
|
|
763
1430
|
BatchNotFoundError,
|
|
764
1431
|
CodeNotFoundError,
|
|
1432
|
+
ComplianceResource,
|
|
1433
|
+
DocumentsResource,
|
|
765
1434
|
InvalidCodeError,
|
|
766
1435
|
InvalidGTINError,
|
|
767
1436
|
InvalidSerialError,
|
|
@@ -770,12 +1439,21 @@ var SDK_VERSION2 = "2.0.0";
|
|
|
770
1439
|
NetworkError,
|
|
771
1440
|
OptropicClient,
|
|
772
1441
|
OptropicError,
|
|
1442
|
+
ProvenanceResource,
|
|
773
1443
|
QuotaExceededError,
|
|
774
1444
|
RateLimitedError,
|
|
775
1445
|
RevokedCodeError,
|
|
776
1446
|
SDK_VERSION,
|
|
1447
|
+
SchemasResource,
|
|
777
1448
|
ServiceUnavailableError,
|
|
1449
|
+
StaleFilterError,
|
|
778
1450
|
TimeoutError,
|
|
1451
|
+
buildDPPConfig,
|
|
779
1452
|
createClient,
|
|
1453
|
+
createErrorFromResponse,
|
|
1454
|
+
parseFilterHeader,
|
|
1455
|
+
parseSaltsHeader,
|
|
1456
|
+
validateDPPMetadata,
|
|
1457
|
+
verifyOffline,
|
|
780
1458
|
verifyWebhookSignature
|
|
781
1459
|
});
|