optropic 1.0.0 → 2.1.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.js CHANGED
@@ -382,14 +382,26 @@ function createErrorFromResponse(statusCode, body) {
382
382
 
383
383
  // src/resources/assets.ts
384
384
  var AssetsResource = class {
385
- constructor(request) {
385
+ constructor(request, client) {
386
386
  this.request = request;
387
+ this.client = client;
387
388
  }
388
389
  async create(params) {
389
390
  return this.request({ method: "POST", path: "/v1/assets", body: params });
390
391
  }
392
+ /**
393
+ * List assets with optional filtering and pagination.
394
+ *
395
+ * When the client uses a sandbox/test API key, `is_sandbox` is
396
+ * automatically set to `true` so sandbox clients only see sandbox
397
+ * assets. Pass an explicit `is_sandbox` value to override.
398
+ */
391
399
  async list(params) {
392
- const query = params ? this.buildQuery(params) : "";
400
+ let effectiveParams = params;
401
+ if (this.client.isSandbox && (!params || params.is_sandbox === void 0)) {
402
+ effectiveParams = { ...params, is_sandbox: true };
403
+ }
404
+ const query = effectiveParams ? this.buildQuery(effectiveParams) : "";
393
405
  return this.request({ method: "GET", path: `/v1/assets${query}` });
394
406
  }
395
407
  async get(assetId) {
@@ -415,6 +427,117 @@ var AssetsResource = class {
415
427
  }
416
428
  };
417
429
 
430
+ // src/resources/audit.ts
431
+ var AuditResource = class {
432
+ constructor(request) {
433
+ this.request = request;
434
+ }
435
+ /**
436
+ * List audit events with optional filtering and pagination.
437
+ */
438
+ async list(params) {
439
+ const query = params ? this.buildQuery(params) : "";
440
+ return this.request({ method: "GET", path: `/v1/audit${query}` });
441
+ }
442
+ /**
443
+ * Retrieve a single audit event by ID.
444
+ */
445
+ async get(eventId) {
446
+ return this.request({
447
+ method: "GET",
448
+ path: `/v1/audit/${encodeURIComponent(eventId)}`
449
+ });
450
+ }
451
+ /**
452
+ * Record a custom audit event.
453
+ */
454
+ async create(params) {
455
+ return this.request({
456
+ method: "POST",
457
+ path: "/v1/audit",
458
+ body: {
459
+ event_type: params.eventType,
460
+ ...params.resourceId !== void 0 && { resource_id: params.resourceId },
461
+ ...params.resourceType !== void 0 && { resource_type: params.resourceType },
462
+ ...params.details !== void 0 && { details: params.details }
463
+ }
464
+ });
465
+ }
466
+ buildQuery(params) {
467
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
468
+ if (entries.length === 0) return "";
469
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
470
+ }
471
+ };
472
+
473
+ // src/resources/compliance.ts
474
+ var ComplianceResource = class {
475
+ constructor(request) {
476
+ this.request = request;
477
+ }
478
+ /**
479
+ * Verify the integrity of the full audit chain.
480
+ */
481
+ async verifyChain() {
482
+ return this.request({
483
+ method: "POST",
484
+ path: "/v1/compliance/verify-chain"
485
+ });
486
+ }
487
+ /**
488
+ * Return all Merkle roots.
489
+ */
490
+ async listMerkleRoots() {
491
+ return this.request({
492
+ method: "GET",
493
+ path: "/v1/compliance/merkle-roots"
494
+ });
495
+ }
496
+ /**
497
+ * Return a Merkle inclusion proof for a specific audit event.
498
+ */
499
+ async getMerkleProof(eventId) {
500
+ return this.request({
501
+ method: "GET",
502
+ path: `/v1/compliance/merkle-proof/${encodeURIComponent(eventId)}`
503
+ });
504
+ }
505
+ /**
506
+ * Export audit data as a signed CSV.
507
+ */
508
+ async exportAudit(params) {
509
+ const query = params ? this.buildQuery(params) : "";
510
+ return this.request({
511
+ method: "GET",
512
+ path: `/v1/compliance/export${query}`
513
+ });
514
+ }
515
+ /**
516
+ * Retrieve the current compliance configuration.
517
+ */
518
+ async getConfig() {
519
+ return this.request({
520
+ method: "GET",
521
+ path: "/v1/compliance/config"
522
+ });
523
+ }
524
+ /**
525
+ * Update the compliance mode.
526
+ */
527
+ async updateConfig(mode) {
528
+ return this.request({
529
+ method: "POST",
530
+ path: "/v1/compliance/config",
531
+ body: { compliance_mode: mode }
532
+ });
533
+ }
534
+ buildQuery(params) {
535
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
536
+ if (entries.length === 0) return "";
537
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
538
+ }
539
+ };
540
+
418
541
  // src/resources/keys.ts
419
542
  var KeysResource = class {
420
543
  constructor(request) {
@@ -424,17 +547,162 @@ var KeysResource = class {
424
547
  return this.request({ method: "POST", path: "/v1/keys", body: params });
425
548
  }
426
549
  async list() {
427
- return this.request({ method: "GET", path: "/v1/keys" });
550
+ const result = await this.request({ method: "GET", path: "/v1/keys" });
551
+ return result.data;
428
552
  }
429
553
  async revoke(keyId) {
430
554
  await this.request({ method: "DELETE", path: `/v1/keys/${encodeURIComponent(keyId)}` });
431
555
  }
432
556
  };
433
557
 
558
+ // src/resources/keysets.ts
559
+ var KeysetsResource = class {
560
+ constructor(request) {
561
+ this.request = request;
562
+ }
563
+ async create(params) {
564
+ return this.request({ method: "POST", path: "/v1/keysets", body: params });
565
+ }
566
+ async list(params) {
567
+ const query = params ? this.buildQuery(params) : "";
568
+ return this.request({ method: "GET", path: `/v1/keysets${query}` });
569
+ }
570
+ buildQuery(params) {
571
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
572
+ if (entries.length === 0) return "";
573
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
574
+ }
575
+ };
576
+
577
+ // src/resources/schemas.ts
578
+ function checkType(value, expected) {
579
+ switch (expected) {
580
+ case "string":
581
+ return typeof value === "string";
582
+ case "number":
583
+ return typeof value === "number" && !Number.isNaN(value);
584
+ case "boolean":
585
+ return typeof value === "boolean";
586
+ case "date":
587
+ return typeof value === "string";
588
+ // ISO 8601 string
589
+ case "array":
590
+ return Array.isArray(value);
591
+ default:
592
+ return true;
593
+ }
594
+ }
595
+ var SchemasResource = class {
596
+ constructor(request) {
597
+ this.request = request;
598
+ }
599
+ /**
600
+ * Register or update a vertical config schema.
601
+ * If a schema already exists for the verticalId, it will be updated.
602
+ */
603
+ async create(params) {
604
+ const body = this.stripUndefined({
605
+ vertical_id: params.verticalId,
606
+ metadata_schema: params.metadataSchema,
607
+ version: params.version,
608
+ export_formats: params.exportFormats,
609
+ description: params.description
610
+ });
611
+ return this.request({ method: "POST", path: "/v1/schemas", body });
612
+ }
613
+ /**
614
+ * List registered vertical schemas with pagination.
615
+ */
616
+ async list(params) {
617
+ const query = params ? this.buildQuery(params) : "";
618
+ return this.request({ method: "GET", path: `/v1/schemas${query}` });
619
+ }
620
+ /**
621
+ * Get the active schema for a specific vertical.
622
+ */
623
+ async get(verticalId) {
624
+ return this.request({
625
+ method: "GET",
626
+ path: `/v1/schemas/${encodeURIComponent(verticalId)}`
627
+ });
628
+ }
629
+ /**
630
+ * Update an existing vertical schema.
631
+ */
632
+ async update(verticalId, params) {
633
+ const body = this.stripUndefined({
634
+ version: params.version,
635
+ metadata_schema: params.metadataSchema,
636
+ export_formats: params.exportFormats,
637
+ description: params.description,
638
+ is_active: params.isActive
639
+ });
640
+ return this.request({
641
+ method: "PUT",
642
+ path: `/v1/schemas/${encodeURIComponent(verticalId)}`,
643
+ body
644
+ });
645
+ }
646
+ /**
647
+ * Deactivate a vertical schema (soft delete).
648
+ */
649
+ async delete(verticalId) {
650
+ await this.request({
651
+ method: "DELETE",
652
+ path: `/v1/schemas/${encodeURIComponent(verticalId)}`
653
+ });
654
+ }
655
+ /**
656
+ * Pre-flight validation: check if assetConfig matches the registered schema.
657
+ *
658
+ * This is a client-side convenience that fetches the schema and validates locally.
659
+ * The server also validates on asset creation.
660
+ */
661
+ async validate(verticalId, assetConfig) {
662
+ let schema;
663
+ try {
664
+ schema = await this.get(verticalId);
665
+ } catch {
666
+ return { valid: true, errors: [] };
667
+ }
668
+ const errors = [];
669
+ const metadataSchema = schema.metadataSchema ?? {};
670
+ for (const [fieldName, fieldDef] of Object.entries(metadataSchema)) {
671
+ if (typeof fieldDef !== "object" || fieldDef === null) continue;
672
+ const def = fieldDef;
673
+ const value = assetConfig[fieldName];
674
+ if (def.required && (value === void 0 || value === null || value === "")) {
675
+ const label = def.label ?? fieldName;
676
+ errors.push({ field: fieldName, message: `Required field "${label}" is missing` });
677
+ continue;
678
+ }
679
+ if (value === void 0 || value === null) continue;
680
+ const expectedType = def.type ?? "string";
681
+ if (!checkType(value, expectedType)) {
682
+ errors.push({
683
+ field: fieldName,
684
+ message: `"${def.label ?? fieldName}" must be a ${expectedType}`,
685
+ received: typeof value
686
+ });
687
+ }
688
+ }
689
+ return { valid: errors.length === 0, errors };
690
+ }
691
+ buildQuery(params) {
692
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
693
+ if (entries.length === 0) return "";
694
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
695
+ }
696
+ stripUndefined(obj) {
697
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
698
+ }
699
+ };
700
+
434
701
  // src/client.ts
435
702
  var DEFAULT_BASE_URL = "https://api.optropic.com";
436
703
  var DEFAULT_TIMEOUT = 3e4;
437
- var SDK_VERSION = "1.0.0";
704
+ var SDK_VERSION = "2.0.0";
705
+ var SANDBOX_PREFIXES = ["optr_test_"];
438
706
  var DEFAULT_RETRY_CONFIG = {
439
707
  maxRetries: 3,
440
708
  baseDelay: 1e3,
@@ -444,8 +712,13 @@ var OptropicClient = class {
444
712
  config;
445
713
  baseUrl;
446
714
  retryConfig;
715
+ _sandbox;
447
716
  assets;
717
+ audit;
718
+ compliance;
448
719
  keys;
720
+ keysets;
721
+ schemas;
449
722
  constructor(config) {
450
723
  if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
451
724
  throw new AuthenticationError(
@@ -456,6 +729,11 @@ var OptropicClient = class {
456
729
  ...config,
457
730
  timeout: config.timeout ?? DEFAULT_TIMEOUT
458
731
  };
732
+ if (config.sandbox !== void 0) {
733
+ this._sandbox = config.sandbox;
734
+ } else {
735
+ this._sandbox = SANDBOX_PREFIXES.some((p) => config.apiKey.startsWith(p));
736
+ }
459
737
  if (config.baseUrl) {
460
738
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
461
739
  } else {
@@ -466,8 +744,27 @@ var OptropicClient = class {
466
744
  ...config.retry
467
745
  };
468
746
  const boundRequest = this.request.bind(this);
469
- this.assets = new AssetsResource(boundRequest);
747
+ this.assets = new AssetsResource(boundRequest, this);
748
+ this.audit = new AuditResource(boundRequest);
749
+ this.compliance = new ComplianceResource(boundRequest);
470
750
  this.keys = new KeysResource(boundRequest);
751
+ this.keysets = new KeysetsResource(boundRequest);
752
+ this.schemas = new SchemasResource(boundRequest);
753
+ }
754
+ // ─────────────────────────────────────────────────────────────────────────
755
+ // ENVIRONMENT DETECTION
756
+ // ─────────────────────────────────────────────────────────────────────────
757
+ /** True when the client is in sandbox mode (test API key or explicit override). */
758
+ get isSandbox() {
759
+ return this._sandbox;
760
+ }
761
+ /** True when the client is in live/production mode. */
762
+ get isLive() {
763
+ return !this._sandbox;
764
+ }
765
+ /** Returns 'sandbox' or 'live'. */
766
+ get environment() {
767
+ return this._sandbox ? "sandbox" : "live";
471
768
  }
472
769
  // ─────────────────────────────────────────────────────────────────────────
473
770
  // PRIVATE METHODS
@@ -558,18 +855,22 @@ var OptropicClient = class {
558
855
  requestId
559
856
  });
560
857
  }
858
+ if (response.status === 204) {
859
+ return void 0;
860
+ }
561
861
  const json = await response.json();
562
- if (json.error) {
862
+ if (json && typeof json === "object" && "error" in json && json.error) {
863
+ const err = json.error;
563
864
  throw createErrorFromResponse(response.status, {
564
865
  // Justified: Error code string from API may not match SDK's ErrorCode enum exactly
565
866
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
566
- code: json.error.code,
567
- message: json.error.message,
568
- details: json.error.details,
867
+ code: err.code ?? "UNKNOWN_ERROR",
868
+ message: err.message ?? "Unknown error",
869
+ details: err.details,
569
870
  requestId: json.requestId
570
871
  });
571
872
  }
572
- return json.data;
873
+ return json;
573
874
  } catch (error) {
574
875
  clearTimeout(timeoutId);
575
876
  if (error instanceof OptropicError) {
@@ -594,17 +895,65 @@ function createClient(config) {
594
895
  return new OptropicClient(config);
595
896
  }
596
897
 
898
+ // src/webhooks.ts
899
+ async function computeHmacSha256(secret, message) {
900
+ const encoder = new TextEncoder();
901
+ if (typeof globalThis.crypto?.subtle !== "undefined") {
902
+ const key = await globalThis.crypto.subtle.importKey(
903
+ "raw",
904
+ encoder.encode(secret),
905
+ // OPSEC: Web Crypto API requires this exact algorithm identifier
906
+ { name: "HMAC", hash: "SHA-256" },
907
+ false,
908
+ ["sign"]
909
+ );
910
+ const sig = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(message));
911
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
912
+ }
913
+ const { createHmac } = await import("crypto");
914
+ return createHmac("sha256", secret).update(message).digest("hex");
915
+ }
916
+ function timingSafeEqual(a, b) {
917
+ if (a.length !== b.length) return false;
918
+ let result = 0;
919
+ for (let i = 0; i < a.length; i++) {
920
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
921
+ }
922
+ return result === 0;
923
+ }
924
+ async function verifyWebhookSignature(options) {
925
+ const { payload, signature, timestamp, secret, tolerance = 300 } = options;
926
+ const ts = parseInt(timestamp, 10);
927
+ if (isNaN(ts)) {
928
+ return { valid: false, reason: "Invalid timestamp" };
929
+ }
930
+ const age = Math.abs(Math.floor(Date.now() / 1e3) - ts);
931
+ if (age > tolerance) {
932
+ return { valid: false, reason: `Timestamp too old (${age}s > ${tolerance}s tolerance)` };
933
+ }
934
+ const signedPayload = `${timestamp}.${payload}`;
935
+ const expectedHex = await computeHmacSha256(secret, signedPayload);
936
+ const expected = `sha256=${expectedHex}`;
937
+ if (!timingSafeEqual(expected, signature)) {
938
+ return { valid: false, reason: "Signature mismatch" };
939
+ }
940
+ return { valid: true };
941
+ }
942
+
597
943
  // src/index.ts
598
- var SDK_VERSION2 = "1.0.0";
944
+ var SDK_VERSION2 = "2.0.0";
599
945
  export {
600
946
  AssetsResource,
947
+ AuditResource,
601
948
  AuthenticationError,
602
949
  BatchNotFoundError,
603
950
  CodeNotFoundError,
951
+ ComplianceResource,
604
952
  InvalidCodeError,
605
953
  InvalidGTINError,
606
954
  InvalidSerialError,
607
955
  KeysResource,
956
+ KeysetsResource,
608
957
  NetworkError,
609
958
  OptropicClient,
610
959
  OptropicError,
@@ -612,7 +961,9 @@ export {
612
961
  RateLimitedError,
613
962
  RevokedCodeError,
614
963
  SDK_VERSION2 as SDK_VERSION,
964
+ SchemasResource,
615
965
  ServiceUnavailableError,
616
966
  TimeoutError,
617
- createClient
967
+ createClient,
968
+ verifyWebhookSignature
618
969
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "optropic",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Official Optropic SDK for TypeScript and JavaScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,7 +32,10 @@
32
32
  "gtin",
33
33
  "serialization"
34
34
  ],
35
- "author": "Optropic GmbH",
35
+ "author": {
36
+ "name": "Virtrex GmbH",
37
+ "url": "https://optropic.com"
38
+ },
36
39
  "license": "MIT",
37
40
  "repository": {
38
41
  "type": "git",