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.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
|
}
|
|
@@ -427,6 +452,209 @@ var AssetsResource = class {
|
|
|
427
452
|
}
|
|
428
453
|
};
|
|
429
454
|
|
|
455
|
+
// src/resources/audit.ts
|
|
456
|
+
var AuditResource = class {
|
|
457
|
+
constructor(request) {
|
|
458
|
+
this.request = request;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* List audit events with optional filtering and pagination.
|
|
462
|
+
*/
|
|
463
|
+
async list(params) {
|
|
464
|
+
const query = params ? this.buildQuery(params) : "";
|
|
465
|
+
return this.request({ method: "GET", path: `/v1/audit${query}` });
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Retrieve a single audit event by ID.
|
|
469
|
+
*/
|
|
470
|
+
async get(eventId) {
|
|
471
|
+
return this.request({
|
|
472
|
+
method: "GET",
|
|
473
|
+
path: `/v1/audit/${encodeURIComponent(eventId)}`
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Record a custom audit event.
|
|
478
|
+
*/
|
|
479
|
+
async create(params) {
|
|
480
|
+
return this.request({
|
|
481
|
+
method: "POST",
|
|
482
|
+
path: "/v1/audit",
|
|
483
|
+
body: {
|
|
484
|
+
event_type: params.eventType,
|
|
485
|
+
...params.resourceId !== void 0 && { resource_id: params.resourceId },
|
|
486
|
+
...params.resourceType !== void 0 && { resource_type: params.resourceType },
|
|
487
|
+
...params.details !== void 0 && { details: params.details }
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
buildQuery(params) {
|
|
492
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0);
|
|
493
|
+
if (entries.length === 0) return "";
|
|
494
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// src/resources/compliance.ts
|
|
499
|
+
var ComplianceResource = class {
|
|
500
|
+
constructor(request) {
|
|
501
|
+
this.request = request;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Verify the integrity of the full audit chain.
|
|
505
|
+
*/
|
|
506
|
+
async verifyChain() {
|
|
507
|
+
return this.request({
|
|
508
|
+
method: "POST",
|
|
509
|
+
path: "/v1/compliance/verify-chain"
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Return all Merkle roots.
|
|
514
|
+
*/
|
|
515
|
+
async listMerkleRoots() {
|
|
516
|
+
return this.request({
|
|
517
|
+
method: "GET",
|
|
518
|
+
path: "/v1/compliance/merkle-roots"
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Return a Merkle inclusion proof for a specific audit event.
|
|
523
|
+
*/
|
|
524
|
+
async getMerkleProof(eventId) {
|
|
525
|
+
return this.request({
|
|
526
|
+
method: "GET",
|
|
527
|
+
path: `/v1/compliance/merkle-proof/${encodeURIComponent(eventId)}`
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Export audit data as a signed CSV.
|
|
532
|
+
*/
|
|
533
|
+
async exportAudit(params) {
|
|
534
|
+
const query = params ? this.buildQuery(params) : "";
|
|
535
|
+
return this.request({
|
|
536
|
+
method: "GET",
|
|
537
|
+
path: `/v1/compliance/export${query}`
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Retrieve the current compliance configuration.
|
|
542
|
+
*/
|
|
543
|
+
async getConfig() {
|
|
544
|
+
return this.request({
|
|
545
|
+
method: "GET",
|
|
546
|
+
path: "/v1/compliance/config"
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Update the compliance mode.
|
|
551
|
+
*/
|
|
552
|
+
async updateConfig(mode) {
|
|
553
|
+
return this.request({
|
|
554
|
+
method: "POST",
|
|
555
|
+
path: "/v1/compliance/config",
|
|
556
|
+
body: { compliance_mode: mode }
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
buildQuery(params) {
|
|
560
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0);
|
|
561
|
+
if (entries.length === 0) return "";
|
|
562
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
563
|
+
}
|
|
564
|
+
};
|
|
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
|
+
|
|
430
658
|
// src/resources/keys.ts
|
|
431
659
|
var KeysResource = class {
|
|
432
660
|
constructor(request) {
|
|
@@ -463,10 +691,222 @@ var KeysetsResource = class {
|
|
|
463
691
|
}
|
|
464
692
|
};
|
|
465
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
|
+
|
|
782
|
+
// src/resources/schemas.ts
|
|
783
|
+
function checkType(value, expected) {
|
|
784
|
+
switch (expected) {
|
|
785
|
+
case "string":
|
|
786
|
+
return typeof value === "string";
|
|
787
|
+
case "number":
|
|
788
|
+
return typeof value === "number" && !Number.isNaN(value);
|
|
789
|
+
case "boolean":
|
|
790
|
+
return typeof value === "boolean";
|
|
791
|
+
case "date":
|
|
792
|
+
return typeof value === "string";
|
|
793
|
+
// ISO 8601 string
|
|
794
|
+
case "array":
|
|
795
|
+
return Array.isArray(value);
|
|
796
|
+
default:
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
var SchemasResource = class {
|
|
801
|
+
constructor(request) {
|
|
802
|
+
this.request = request;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Register or update a vertical config schema.
|
|
806
|
+
* If a schema already exists for the verticalId, it will be updated.
|
|
807
|
+
*/
|
|
808
|
+
async create(params) {
|
|
809
|
+
const body = this.stripUndefined({
|
|
810
|
+
vertical_id: params.verticalId,
|
|
811
|
+
metadata_schema: params.metadataSchema,
|
|
812
|
+
version: params.version,
|
|
813
|
+
export_formats: params.exportFormats,
|
|
814
|
+
description: params.description
|
|
815
|
+
});
|
|
816
|
+
return this.request({ method: "POST", path: "/v1/schemas", body });
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* List registered vertical schemas with pagination.
|
|
820
|
+
*/
|
|
821
|
+
async list(params) {
|
|
822
|
+
const query = params ? this.buildQuery(params) : "";
|
|
823
|
+
return this.request({ method: "GET", path: `/v1/schemas${query}` });
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Get the active schema for a specific vertical.
|
|
827
|
+
*/
|
|
828
|
+
async get(verticalId) {
|
|
829
|
+
return this.request({
|
|
830
|
+
method: "GET",
|
|
831
|
+
path: `/v1/schemas/${encodeURIComponent(verticalId)}`
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Update an existing vertical schema.
|
|
836
|
+
*/
|
|
837
|
+
async update(verticalId, params) {
|
|
838
|
+
const body = this.stripUndefined({
|
|
839
|
+
version: params.version,
|
|
840
|
+
metadata_schema: params.metadataSchema,
|
|
841
|
+
export_formats: params.exportFormats,
|
|
842
|
+
description: params.description,
|
|
843
|
+
is_active: params.isActive
|
|
844
|
+
});
|
|
845
|
+
return this.request({
|
|
846
|
+
method: "PUT",
|
|
847
|
+
path: `/v1/schemas/${encodeURIComponent(verticalId)}`,
|
|
848
|
+
body
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Deactivate a vertical schema (soft delete).
|
|
853
|
+
*/
|
|
854
|
+
async delete(verticalId) {
|
|
855
|
+
await this.request({
|
|
856
|
+
method: "DELETE",
|
|
857
|
+
path: `/v1/schemas/${encodeURIComponent(verticalId)}`
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Pre-flight validation: check if assetConfig matches the registered schema.
|
|
862
|
+
*
|
|
863
|
+
* This is a client-side convenience that fetches the schema and validates locally.
|
|
864
|
+
* The server also validates on asset creation.
|
|
865
|
+
*/
|
|
866
|
+
async validate(verticalId, assetConfig) {
|
|
867
|
+
let schema;
|
|
868
|
+
try {
|
|
869
|
+
schema = await this.get(verticalId);
|
|
870
|
+
} catch {
|
|
871
|
+
return { valid: true, errors: [] };
|
|
872
|
+
}
|
|
873
|
+
const errors = [];
|
|
874
|
+
const metadataSchema = schema.metadataSchema ?? {};
|
|
875
|
+
for (const [fieldName, fieldDef] of Object.entries(metadataSchema)) {
|
|
876
|
+
if (typeof fieldDef !== "object" || fieldDef === null) continue;
|
|
877
|
+
const def = fieldDef;
|
|
878
|
+
const value = assetConfig[fieldName];
|
|
879
|
+
if (def.required && (value === void 0 || value === null || value === "")) {
|
|
880
|
+
const label = def.label ?? fieldName;
|
|
881
|
+
errors.push({ field: fieldName, message: `Required field "${label}" is missing` });
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (value === void 0 || value === null) continue;
|
|
885
|
+
const expectedType = def.type ?? "string";
|
|
886
|
+
if (!checkType(value, expectedType)) {
|
|
887
|
+
errors.push({
|
|
888
|
+
field: fieldName,
|
|
889
|
+
message: `"${def.label ?? fieldName}" must be a ${expectedType}`,
|
|
890
|
+
received: typeof value
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return { valid: errors.length === 0, errors };
|
|
895
|
+
}
|
|
896
|
+
buildQuery(params) {
|
|
897
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0);
|
|
898
|
+
if (entries.length === 0) return "";
|
|
899
|
+
return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
900
|
+
}
|
|
901
|
+
stripUndefined(obj) {
|
|
902
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
|
|
466
906
|
// src/client.ts
|
|
467
907
|
var DEFAULT_BASE_URL = "https://api.optropic.com";
|
|
468
908
|
var DEFAULT_TIMEOUT = 3e4;
|
|
469
|
-
var SDK_VERSION = "2.
|
|
909
|
+
var SDK_VERSION = "2.2.0";
|
|
470
910
|
var SANDBOX_PREFIXES = ["optr_test_"];
|
|
471
911
|
var DEFAULT_RETRY_CONFIG = {
|
|
472
912
|
maxRetries: 3,
|
|
@@ -479,8 +919,13 @@ var OptropicClient = class {
|
|
|
479
919
|
retryConfig;
|
|
480
920
|
_sandbox;
|
|
481
921
|
assets;
|
|
922
|
+
audit;
|
|
923
|
+
compliance;
|
|
924
|
+
documents;
|
|
482
925
|
keys;
|
|
483
926
|
keysets;
|
|
927
|
+
provenance;
|
|
928
|
+
schemas;
|
|
484
929
|
constructor(config) {
|
|
485
930
|
if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
|
|
486
931
|
throw new AuthenticationError(
|
|
@@ -507,8 +952,13 @@ var OptropicClient = class {
|
|
|
507
952
|
};
|
|
508
953
|
const boundRequest = this.request.bind(this);
|
|
509
954
|
this.assets = new AssetsResource(boundRequest, this);
|
|
955
|
+
this.audit = new AuditResource(boundRequest);
|
|
956
|
+
this.compliance = new ComplianceResource(boundRequest);
|
|
957
|
+
this.documents = new DocumentsResource(boundRequest);
|
|
510
958
|
this.keys = new KeysResource(boundRequest);
|
|
511
959
|
this.keysets = new KeysetsResource(boundRequest);
|
|
960
|
+
this.provenance = new ProvenanceResource(boundRequest);
|
|
961
|
+
this.schemas = new SchemasResource(boundRequest);
|
|
512
962
|
}
|
|
513
963
|
// ─────────────────────────────────────────────────────────────────────────
|
|
514
964
|
// ENVIRONMENT DETECTION
|
|
@@ -532,7 +982,7 @@ var OptropicClient = class {
|
|
|
532
982
|
return /^optr_(live|test)_[a-zA-Z0-9_-]{20,}$/.test(apiKey);
|
|
533
983
|
}
|
|
534
984
|
async request(options) {
|
|
535
|
-
const { method, path, body, headers = {}, timeout = this.config.timeout } = options;
|
|
985
|
+
const { method, path, body, headers = {}, timeout = this.config.timeout, idempotencyKey } = options;
|
|
536
986
|
const url = `${this.baseUrl}${path}`;
|
|
537
987
|
const requestHeaders = {
|
|
538
988
|
"Content-Type": "application/json",
|
|
@@ -543,6 +993,11 @@ var OptropicClient = class {
|
|
|
543
993
|
...this.config.headers,
|
|
544
994
|
...headers
|
|
545
995
|
};
|
|
996
|
+
if (idempotencyKey) {
|
|
997
|
+
requestHeaders["Idempotency-Key"] = idempotencyKey;
|
|
998
|
+
} else if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
999
|
+
requestHeaders["Idempotency-Key"] = crypto.randomUUID();
|
|
1000
|
+
}
|
|
546
1001
|
let lastError = null;
|
|
547
1002
|
let attempt = 0;
|
|
548
1003
|
while (attempt <= this.retryConfig.maxRetries) {
|
|
@@ -563,10 +1018,12 @@ var OptropicClient = class {
|
|
|
563
1018
|
if (attempt >= this.retryConfig.maxRetries) {
|
|
564
1019
|
throw error;
|
|
565
1020
|
}
|
|
566
|
-
const
|
|
1021
|
+
const baseDelay = Math.min(
|
|
567
1022
|
this.retryConfig.baseDelay * Math.pow(2, attempt),
|
|
568
1023
|
this.retryConfig.maxDelay
|
|
569
1024
|
);
|
|
1025
|
+
const jitter = baseDelay * 0.5 * Math.random();
|
|
1026
|
+
const delay = baseDelay + jitter;
|
|
570
1027
|
if (error instanceof RateLimitedError) {
|
|
571
1028
|
await this.sleep(error.retryAfter * 1e3);
|
|
572
1029
|
} else {
|
|
@@ -654,6 +1111,203 @@ function createClient(config) {
|
|
|
654
1111
|
return new OptropicClient(config);
|
|
655
1112
|
}
|
|
656
1113
|
|
|
1114
|
+
// src/filter-verify.ts
|
|
1115
|
+
var HEADER_SIZE = 19;
|
|
1116
|
+
var SIGNATURE_SIZE = 64;
|
|
1117
|
+
var SLOTS_PER_BUCKET = 4;
|
|
1118
|
+
var StaleFilterError = class extends Error {
|
|
1119
|
+
code = "FILTER_STALE_CRITICAL";
|
|
1120
|
+
ageSeconds;
|
|
1121
|
+
trustWindowSeconds;
|
|
1122
|
+
constructor(ageSeconds, trustWindowSeconds) {
|
|
1123
|
+
super(`Filter age ${ageSeconds}s exceeds trust window ${trustWindowSeconds}s`);
|
|
1124
|
+
this.name = "StaleFilterError";
|
|
1125
|
+
this.ageSeconds = ageSeconds;
|
|
1126
|
+
this.trustWindowSeconds = trustWindowSeconds;
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
async function sha256(data) {
|
|
1130
|
+
const buf = new Uint8Array(data).buffer;
|
|
1131
|
+
const hash = await globalThis.crypto.subtle.digest("SHA-256", buf);
|
|
1132
|
+
return new Uint8Array(hash);
|
|
1133
|
+
}
|
|
1134
|
+
async function sha256Hex(input) {
|
|
1135
|
+
const encoded = new TextEncoder().encode(input);
|
|
1136
|
+
const hash = await sha256(encoded);
|
|
1137
|
+
return Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1138
|
+
}
|
|
1139
|
+
function uint32BE(buf, offset) {
|
|
1140
|
+
return (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
|
|
1141
|
+
}
|
|
1142
|
+
function uint16BE(buf, offset) {
|
|
1143
|
+
return buf[offset] << 8 | buf[offset + 1];
|
|
1144
|
+
}
|
|
1145
|
+
function int64BE(buf, offset) {
|
|
1146
|
+
const high = (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
|
|
1147
|
+
const low = (buf[offset + 4] << 24 | buf[offset + 5] << 16 | buf[offset + 6] << 8 | buf[offset + 7]) >>> 0;
|
|
1148
|
+
return high * 4294967296 + low;
|
|
1149
|
+
}
|
|
1150
|
+
async function fingerprint(item) {
|
|
1151
|
+
const encoded = new TextEncoder().encode(item);
|
|
1152
|
+
let digest = await sha256(encoded);
|
|
1153
|
+
let fp = uint16BE(digest, 4);
|
|
1154
|
+
while (fp === 0) {
|
|
1155
|
+
digest = await sha256(digest);
|
|
1156
|
+
fp = uint16BE(digest, 4);
|
|
1157
|
+
}
|
|
1158
|
+
return fp;
|
|
1159
|
+
}
|
|
1160
|
+
async function h1(item, capacity) {
|
|
1161
|
+
const encoded = new TextEncoder().encode(item);
|
|
1162
|
+
const digest = await sha256(encoded);
|
|
1163
|
+
return uint32BE(digest, 0) % capacity;
|
|
1164
|
+
}
|
|
1165
|
+
async function altIndex(index, fp, capacity) {
|
|
1166
|
+
const fpBuf = new Uint8Array(2);
|
|
1167
|
+
fpBuf[0] = fp >> 8 & 255;
|
|
1168
|
+
fpBuf[1] = fp & 255;
|
|
1169
|
+
const fpDigest = await sha256(fpBuf);
|
|
1170
|
+
return ((index ^ uint32BE(fpDigest, 0) % capacity) & 4294967295) >>> 0;
|
|
1171
|
+
}
|
|
1172
|
+
async function filterLookup(filterData, capacity, item) {
|
|
1173
|
+
const fp = await fingerprint(item);
|
|
1174
|
+
const i1 = await h1(item, capacity);
|
|
1175
|
+
const i2 = await altIndex(i1, fp, capacity);
|
|
1176
|
+
const b1Offset = i1 * SLOTS_PER_BUCKET * 2;
|
|
1177
|
+
for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
|
|
1178
|
+
const slotVal = uint16BE(filterData, b1Offset + s * 2);
|
|
1179
|
+
if (slotVal === fp) return true;
|
|
1180
|
+
}
|
|
1181
|
+
const b2Offset = i2 * SLOTS_PER_BUCKET * 2;
|
|
1182
|
+
for (let s = 0; s < SLOTS_PER_BUCKET; s++) {
|
|
1183
|
+
const slotVal = uint16BE(filterData, b2Offset + s * 2);
|
|
1184
|
+
if (slotVal === fp) return true;
|
|
1185
|
+
}
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
function parseFilterHeader(buf) {
|
|
1189
|
+
if (buf.length < HEADER_SIZE) {
|
|
1190
|
+
throw new Error("Buffer too small for filter header");
|
|
1191
|
+
}
|
|
1192
|
+
return {
|
|
1193
|
+
version: buf[0],
|
|
1194
|
+
issuedAt: int64BE(buf, 1),
|
|
1195
|
+
itemCount: uint32BE(buf, 9),
|
|
1196
|
+
keyId: uint16BE(buf, 13),
|
|
1197
|
+
capacity: uint32BE(buf, 15)
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function parseSaltsHeader(header) {
|
|
1201
|
+
const salts = /* @__PURE__ */ new Map();
|
|
1202
|
+
if (!header) return salts;
|
|
1203
|
+
for (const pair of header.split(",")) {
|
|
1204
|
+
const colonIdx = pair.indexOf(":");
|
|
1205
|
+
if (colonIdx === -1) continue;
|
|
1206
|
+
const tenantId = pair.slice(0, colonIdx).trim();
|
|
1207
|
+
const saltHex = pair.slice(colonIdx + 1).trim();
|
|
1208
|
+
if (tenantId && saltHex) {
|
|
1209
|
+
const bytes = new Uint8Array(saltHex.length / 2);
|
|
1210
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1211
|
+
bytes[i] = parseInt(saltHex.slice(i * 2, i * 2 + 2), 16);
|
|
1212
|
+
}
|
|
1213
|
+
salts.set(tenantId, bytes);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return salts;
|
|
1217
|
+
}
|
|
1218
|
+
function toHex(bytes) {
|
|
1219
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1220
|
+
}
|
|
1221
|
+
async function verifyOffline(options) {
|
|
1222
|
+
const {
|
|
1223
|
+
assetId,
|
|
1224
|
+
filterBytes,
|
|
1225
|
+
salts,
|
|
1226
|
+
filterPolicy = "permissive",
|
|
1227
|
+
trustWindowSeconds = 259200
|
|
1228
|
+
// 72 hours
|
|
1229
|
+
} = options;
|
|
1230
|
+
const header = parseFilterHeader(filterBytes);
|
|
1231
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
1232
|
+
const ageSeconds = nowSeconds - header.issuedAt;
|
|
1233
|
+
if (ageSeconds > trustWindowSeconds) {
|
|
1234
|
+
if (filterPolicy === "strict") {
|
|
1235
|
+
throw new StaleFilterError(ageSeconds, trustWindowSeconds);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const filterDataEnd = filterBytes.length - SIGNATURE_SIZE;
|
|
1239
|
+
const filterData = filterBytes.slice(HEADER_SIZE, filterDataEnd);
|
|
1240
|
+
const capacity = header.capacity;
|
|
1241
|
+
let revoked = false;
|
|
1242
|
+
for (const salt of salts.values()) {
|
|
1243
|
+
const saltHex = toHex(salt);
|
|
1244
|
+
const saltedInput = assetId + saltHex;
|
|
1245
|
+
const saltedHash = await sha256Hex(saltedInput);
|
|
1246
|
+
if (await filterLookup(filterData, capacity, saltedHash)) {
|
|
1247
|
+
revoked = true;
|
|
1248
|
+
break;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const freshness = ageSeconds > trustWindowSeconds ? "stale" : "current";
|
|
1252
|
+
return {
|
|
1253
|
+
signatureValid: true,
|
|
1254
|
+
revocationStatus: revoked ? "revoked" : "clear",
|
|
1255
|
+
filterAgeSeconds: ageSeconds,
|
|
1256
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1257
|
+
verificationMode: "offline",
|
|
1258
|
+
freshness
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// src/dpp.ts
|
|
1263
|
+
function buildDPPConfig(metadata) {
|
|
1264
|
+
const config = {
|
|
1265
|
+
product_id: metadata.productId,
|
|
1266
|
+
product_name: metadata.productName,
|
|
1267
|
+
manufacturer: metadata.manufacturer,
|
|
1268
|
+
country_of_origin: metadata.countryOfOrigin,
|
|
1269
|
+
dpp_category: metadata.category
|
|
1270
|
+
};
|
|
1271
|
+
if (metadata.carbonFootprint !== void 0) config.carbon_footprint_kg_co2e = metadata.carbonFootprint;
|
|
1272
|
+
if (metadata.recycledContent !== void 0) config.recycled_content_percent = metadata.recycledContent;
|
|
1273
|
+
if (metadata.durabilityYears !== void 0) config.durability_years = metadata.durabilityYears;
|
|
1274
|
+
if (metadata.repairabilityScore !== void 0) config.repairability_score = metadata.repairabilityScore;
|
|
1275
|
+
if (metadata.substancesOfConcern) config.substances_of_concern = metadata.substancesOfConcern;
|
|
1276
|
+
if (metadata.dppRegistryId) config.dpp_registry_id = metadata.dppRegistryId;
|
|
1277
|
+
if (metadata.conformityDeclarations) config.conformity_declarations = metadata.conformityDeclarations;
|
|
1278
|
+
if (metadata.sectorData) config.sector_data = metadata.sectorData;
|
|
1279
|
+
return config;
|
|
1280
|
+
}
|
|
1281
|
+
function validateDPPMetadata(metadata) {
|
|
1282
|
+
const errors = [];
|
|
1283
|
+
if (!metadata.productId) errors.push("productId is required");
|
|
1284
|
+
if (!metadata.productName) errors.push("productName is required");
|
|
1285
|
+
if (!metadata.manufacturer) errors.push("manufacturer is required");
|
|
1286
|
+
if (!metadata.countryOfOrigin) errors.push("countryOfOrigin is required");
|
|
1287
|
+
if (metadata.countryOfOrigin && !/^[A-Z]{2}$/.test(metadata.countryOfOrigin)) {
|
|
1288
|
+
errors.push('countryOfOrigin must be ISO 3166-1 alpha-2 (e.g., "DE")');
|
|
1289
|
+
}
|
|
1290
|
+
if (metadata.recycledContent !== void 0 && (metadata.recycledContent < 0 || metadata.recycledContent > 100)) {
|
|
1291
|
+
errors.push("recycledContent must be between 0 and 100");
|
|
1292
|
+
}
|
|
1293
|
+
if (metadata.category === "battery" && metadata.sectorData) {
|
|
1294
|
+
const battery = metadata.sectorData;
|
|
1295
|
+
if (battery.type === "battery") {
|
|
1296
|
+
if (!battery.chemistry) errors.push("battery.chemistry is required for battery passports");
|
|
1297
|
+
if (!battery.capacityKwh) errors.push("battery.capacityKwh is required for battery passports");
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (metadata.category === "textile" && metadata.sectorData) {
|
|
1301
|
+
const textile = metadata.sectorData;
|
|
1302
|
+
if (textile.type === "textile") {
|
|
1303
|
+
if (!textile.fiberComposition || textile.fiberComposition.length === 0) {
|
|
1304
|
+
errors.push("textile.fiberComposition is required for textile passports");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return { valid: errors.length === 0, errors };
|
|
1309
|
+
}
|
|
1310
|
+
|
|
657
1311
|
// src/webhooks.ts
|
|
658
1312
|
async function computeHmacSha256(secret, message) {
|
|
659
1313
|
const encoder = new TextEncoder();
|
|
@@ -700,12 +1354,15 @@ async function verifyWebhookSignature(options) {
|
|
|
700
1354
|
}
|
|
701
1355
|
|
|
702
1356
|
// src/index.ts
|
|
703
|
-
var SDK_VERSION2 = "2.
|
|
1357
|
+
var SDK_VERSION2 = "2.2.0";
|
|
704
1358
|
export {
|
|
705
1359
|
AssetsResource,
|
|
1360
|
+
AuditResource,
|
|
706
1361
|
AuthenticationError,
|
|
707
1362
|
BatchNotFoundError,
|
|
708
1363
|
CodeNotFoundError,
|
|
1364
|
+
ComplianceResource,
|
|
1365
|
+
DocumentsResource,
|
|
709
1366
|
InvalidCodeError,
|
|
710
1367
|
InvalidGTINError,
|
|
711
1368
|
InvalidSerialError,
|
|
@@ -714,12 +1371,21 @@ export {
|
|
|
714
1371
|
NetworkError,
|
|
715
1372
|
OptropicClient,
|
|
716
1373
|
OptropicError,
|
|
1374
|
+
ProvenanceResource,
|
|
717
1375
|
QuotaExceededError,
|
|
718
1376
|
RateLimitedError,
|
|
719
1377
|
RevokedCodeError,
|
|
720
1378
|
SDK_VERSION2 as SDK_VERSION,
|
|
1379
|
+
SchemasResource,
|
|
721
1380
|
ServiceUnavailableError,
|
|
1381
|
+
StaleFilterError,
|
|
722
1382
|
TimeoutError,
|
|
1383
|
+
buildDPPConfig,
|
|
723
1384
|
createClient,
|
|
1385
|
+
createErrorFromResponse,
|
|
1386
|
+
parseFilterHeader,
|
|
1387
|
+
parseSaltsHeader,
|
|
1388
|
+
validateDPPMetadata,
|
|
1389
|
+
verifyOffline,
|
|
724
1390
|
verifyWebhookSignature
|
|
725
1391
|
};
|