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.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,19 +17,30 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
23
33
  AssetsResource: () => AssetsResource,
34
+ AuditResource: () => AuditResource,
24
35
  AuthenticationError: () => AuthenticationError,
25
36
  BatchNotFoundError: () => BatchNotFoundError,
26
37
  CodeNotFoundError: () => CodeNotFoundError,
38
+ ComplianceResource: () => ComplianceResource,
27
39
  InvalidCodeError: () => InvalidCodeError,
28
40
  InvalidGTINError: () => InvalidGTINError,
29
41
  InvalidSerialError: () => InvalidSerialError,
30
42
  KeysResource: () => KeysResource,
43
+ KeysetsResource: () => KeysetsResource,
31
44
  NetworkError: () => NetworkError,
32
45
  OptropicClient: () => OptropicClient,
33
46
  OptropicError: () => OptropicError,
@@ -35,9 +48,11 @@ __export(index_exports, {
35
48
  RateLimitedError: () => RateLimitedError,
36
49
  RevokedCodeError: () => RevokedCodeError,
37
50
  SDK_VERSION: () => SDK_VERSION2,
51
+ SchemasResource: () => SchemasResource,
38
52
  ServiceUnavailableError: () => ServiceUnavailableError,
39
53
  TimeoutError: () => TimeoutError,
40
- createClient: () => createClient
54
+ createClient: () => createClient,
55
+ verifyWebhookSignature: () => verifyWebhookSignature
41
56
  });
42
57
  module.exports = __toCommonJS(index_exports);
43
58
 
@@ -425,14 +440,26 @@ function createErrorFromResponse(statusCode, body) {
425
440
 
426
441
  // src/resources/assets.ts
427
442
  var AssetsResource = class {
428
- constructor(request) {
443
+ constructor(request, client) {
429
444
  this.request = request;
445
+ this.client = client;
430
446
  }
431
447
  async create(params) {
432
448
  return this.request({ method: "POST", path: "/v1/assets", body: params });
433
449
  }
450
+ /**
451
+ * List assets with optional filtering and pagination.
452
+ *
453
+ * When the client uses a sandbox/test API key, `is_sandbox` is
454
+ * automatically set to `true` so sandbox clients only see sandbox
455
+ * assets. Pass an explicit `is_sandbox` value to override.
456
+ */
434
457
  async list(params) {
435
- const query = params ? this.buildQuery(params) : "";
458
+ let effectiveParams = params;
459
+ if (this.client.isSandbox && (!params || params.is_sandbox === void 0)) {
460
+ effectiveParams = { ...params, is_sandbox: true };
461
+ }
462
+ const query = effectiveParams ? this.buildQuery(effectiveParams) : "";
436
463
  return this.request({ method: "GET", path: `/v1/assets${query}` });
437
464
  }
438
465
  async get(assetId) {
@@ -458,6 +485,117 @@ var AssetsResource = class {
458
485
  }
459
486
  };
460
487
 
488
+ // src/resources/audit.ts
489
+ var AuditResource = class {
490
+ constructor(request) {
491
+ this.request = request;
492
+ }
493
+ /**
494
+ * List audit events with optional filtering and pagination.
495
+ */
496
+ async list(params) {
497
+ const query = params ? this.buildQuery(params) : "";
498
+ return this.request({ method: "GET", path: `/v1/audit${query}` });
499
+ }
500
+ /**
501
+ * Retrieve a single audit event by ID.
502
+ */
503
+ async get(eventId) {
504
+ return this.request({
505
+ method: "GET",
506
+ path: `/v1/audit/${encodeURIComponent(eventId)}`
507
+ });
508
+ }
509
+ /**
510
+ * Record a custom audit event.
511
+ */
512
+ async create(params) {
513
+ return this.request({
514
+ method: "POST",
515
+ path: "/v1/audit",
516
+ body: {
517
+ event_type: params.eventType,
518
+ ...params.resourceId !== void 0 && { resource_id: params.resourceId },
519
+ ...params.resourceType !== void 0 && { resource_type: params.resourceType },
520
+ ...params.details !== void 0 && { details: params.details }
521
+ }
522
+ });
523
+ }
524
+ buildQuery(params) {
525
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
526
+ if (entries.length === 0) return "";
527
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
528
+ }
529
+ };
530
+
531
+ // src/resources/compliance.ts
532
+ var ComplianceResource = class {
533
+ constructor(request) {
534
+ this.request = request;
535
+ }
536
+ /**
537
+ * Verify the integrity of the full audit chain.
538
+ */
539
+ async verifyChain() {
540
+ return this.request({
541
+ method: "POST",
542
+ path: "/v1/compliance/verify-chain"
543
+ });
544
+ }
545
+ /**
546
+ * Return all Merkle roots.
547
+ */
548
+ async listMerkleRoots() {
549
+ return this.request({
550
+ method: "GET",
551
+ path: "/v1/compliance/merkle-roots"
552
+ });
553
+ }
554
+ /**
555
+ * Return a Merkle inclusion proof for a specific audit event.
556
+ */
557
+ async getMerkleProof(eventId) {
558
+ return this.request({
559
+ method: "GET",
560
+ path: `/v1/compliance/merkle-proof/${encodeURIComponent(eventId)}`
561
+ });
562
+ }
563
+ /**
564
+ * Export audit data as a signed CSV.
565
+ */
566
+ async exportAudit(params) {
567
+ const query = params ? this.buildQuery(params) : "";
568
+ return this.request({
569
+ method: "GET",
570
+ path: `/v1/compliance/export${query}`
571
+ });
572
+ }
573
+ /**
574
+ * Retrieve the current compliance configuration.
575
+ */
576
+ async getConfig() {
577
+ return this.request({
578
+ method: "GET",
579
+ path: "/v1/compliance/config"
580
+ });
581
+ }
582
+ /**
583
+ * Update the compliance mode.
584
+ */
585
+ async updateConfig(mode) {
586
+ return this.request({
587
+ method: "POST",
588
+ path: "/v1/compliance/config",
589
+ body: { compliance_mode: mode }
590
+ });
591
+ }
592
+ buildQuery(params) {
593
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
594
+ if (entries.length === 0) return "";
595
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
596
+ }
597
+ };
598
+
461
599
  // src/resources/keys.ts
462
600
  var KeysResource = class {
463
601
  constructor(request) {
@@ -467,17 +605,162 @@ var KeysResource = class {
467
605
  return this.request({ method: "POST", path: "/v1/keys", body: params });
468
606
  }
469
607
  async list() {
470
- return this.request({ method: "GET", path: "/v1/keys" });
608
+ const result = await this.request({ method: "GET", path: "/v1/keys" });
609
+ return result.data;
471
610
  }
472
611
  async revoke(keyId) {
473
612
  await this.request({ method: "DELETE", path: `/v1/keys/${encodeURIComponent(keyId)}` });
474
613
  }
475
614
  };
476
615
 
616
+ // src/resources/keysets.ts
617
+ var KeysetsResource = class {
618
+ constructor(request) {
619
+ this.request = request;
620
+ }
621
+ async create(params) {
622
+ return this.request({ method: "POST", path: "/v1/keysets", body: params });
623
+ }
624
+ async list(params) {
625
+ const query = params ? this.buildQuery(params) : "";
626
+ return this.request({ method: "GET", path: `/v1/keysets${query}` });
627
+ }
628
+ buildQuery(params) {
629
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
630
+ if (entries.length === 0) return "";
631
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
632
+ }
633
+ };
634
+
635
+ // src/resources/schemas.ts
636
+ function checkType(value, expected) {
637
+ switch (expected) {
638
+ case "string":
639
+ return typeof value === "string";
640
+ case "number":
641
+ return typeof value === "number" && !Number.isNaN(value);
642
+ case "boolean":
643
+ return typeof value === "boolean";
644
+ case "date":
645
+ return typeof value === "string";
646
+ // ISO 8601 string
647
+ case "array":
648
+ return Array.isArray(value);
649
+ default:
650
+ return true;
651
+ }
652
+ }
653
+ var SchemasResource = class {
654
+ constructor(request) {
655
+ this.request = request;
656
+ }
657
+ /**
658
+ * Register or update a vertical config schema.
659
+ * If a schema already exists for the verticalId, it will be updated.
660
+ */
661
+ async create(params) {
662
+ const body = this.stripUndefined({
663
+ vertical_id: params.verticalId,
664
+ metadata_schema: params.metadataSchema,
665
+ version: params.version,
666
+ export_formats: params.exportFormats,
667
+ description: params.description
668
+ });
669
+ return this.request({ method: "POST", path: "/v1/schemas", body });
670
+ }
671
+ /**
672
+ * List registered vertical schemas with pagination.
673
+ */
674
+ async list(params) {
675
+ const query = params ? this.buildQuery(params) : "";
676
+ return this.request({ method: "GET", path: `/v1/schemas${query}` });
677
+ }
678
+ /**
679
+ * Get the active schema for a specific vertical.
680
+ */
681
+ async get(verticalId) {
682
+ return this.request({
683
+ method: "GET",
684
+ path: `/v1/schemas/${encodeURIComponent(verticalId)}`
685
+ });
686
+ }
687
+ /**
688
+ * Update an existing vertical schema.
689
+ */
690
+ async update(verticalId, params) {
691
+ const body = this.stripUndefined({
692
+ version: params.version,
693
+ metadata_schema: params.metadataSchema,
694
+ export_formats: params.exportFormats,
695
+ description: params.description,
696
+ is_active: params.isActive
697
+ });
698
+ return this.request({
699
+ method: "PUT",
700
+ path: `/v1/schemas/${encodeURIComponent(verticalId)}`,
701
+ body
702
+ });
703
+ }
704
+ /**
705
+ * Deactivate a vertical schema (soft delete).
706
+ */
707
+ async delete(verticalId) {
708
+ await this.request({
709
+ method: "DELETE",
710
+ path: `/v1/schemas/${encodeURIComponent(verticalId)}`
711
+ });
712
+ }
713
+ /**
714
+ * Pre-flight validation: check if assetConfig matches the registered schema.
715
+ *
716
+ * This is a client-side convenience that fetches the schema and validates locally.
717
+ * The server also validates on asset creation.
718
+ */
719
+ async validate(verticalId, assetConfig) {
720
+ let schema;
721
+ try {
722
+ schema = await this.get(verticalId);
723
+ } catch {
724
+ return { valid: true, errors: [] };
725
+ }
726
+ const errors = [];
727
+ const metadataSchema = schema.metadataSchema ?? {};
728
+ for (const [fieldName, fieldDef] of Object.entries(metadataSchema)) {
729
+ if (typeof fieldDef !== "object" || fieldDef === null) continue;
730
+ const def = fieldDef;
731
+ const value = assetConfig[fieldName];
732
+ if (def.required && (value === void 0 || value === null || value === "")) {
733
+ const label = def.label ?? fieldName;
734
+ errors.push({ field: fieldName, message: `Required field "${label}" is missing` });
735
+ continue;
736
+ }
737
+ if (value === void 0 || value === null) continue;
738
+ const expectedType = def.type ?? "string";
739
+ if (!checkType(value, expectedType)) {
740
+ errors.push({
741
+ field: fieldName,
742
+ message: `"${def.label ?? fieldName}" must be a ${expectedType}`,
743
+ received: typeof value
744
+ });
745
+ }
746
+ }
747
+ return { valid: errors.length === 0, errors };
748
+ }
749
+ buildQuery(params) {
750
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
751
+ if (entries.length === 0) return "";
752
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
753
+ }
754
+ stripUndefined(obj) {
755
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
756
+ }
757
+ };
758
+
477
759
  // src/client.ts
478
760
  var DEFAULT_BASE_URL = "https://api.optropic.com";
479
761
  var DEFAULT_TIMEOUT = 3e4;
480
- var SDK_VERSION = "1.0.0";
762
+ var SDK_VERSION = "2.0.0";
763
+ var SANDBOX_PREFIXES = ["optr_test_"];
481
764
  var DEFAULT_RETRY_CONFIG = {
482
765
  maxRetries: 3,
483
766
  baseDelay: 1e3,
@@ -487,8 +770,13 @@ var OptropicClient = class {
487
770
  config;
488
771
  baseUrl;
489
772
  retryConfig;
773
+ _sandbox;
490
774
  assets;
775
+ audit;
776
+ compliance;
491
777
  keys;
778
+ keysets;
779
+ schemas;
492
780
  constructor(config) {
493
781
  if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
494
782
  throw new AuthenticationError(
@@ -499,6 +787,11 @@ var OptropicClient = class {
499
787
  ...config,
500
788
  timeout: config.timeout ?? DEFAULT_TIMEOUT
501
789
  };
790
+ if (config.sandbox !== void 0) {
791
+ this._sandbox = config.sandbox;
792
+ } else {
793
+ this._sandbox = SANDBOX_PREFIXES.some((p) => config.apiKey.startsWith(p));
794
+ }
502
795
  if (config.baseUrl) {
503
796
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
504
797
  } else {
@@ -509,8 +802,27 @@ var OptropicClient = class {
509
802
  ...config.retry
510
803
  };
511
804
  const boundRequest = this.request.bind(this);
512
- this.assets = new AssetsResource(boundRequest);
805
+ this.assets = new AssetsResource(boundRequest, this);
806
+ this.audit = new AuditResource(boundRequest);
807
+ this.compliance = new ComplianceResource(boundRequest);
513
808
  this.keys = new KeysResource(boundRequest);
809
+ this.keysets = new KeysetsResource(boundRequest);
810
+ this.schemas = new SchemasResource(boundRequest);
811
+ }
812
+ // ─────────────────────────────────────────────────────────────────────────
813
+ // ENVIRONMENT DETECTION
814
+ // ─────────────────────────────────────────────────────────────────────────
815
+ /** True when the client is in sandbox mode (test API key or explicit override). */
816
+ get isSandbox() {
817
+ return this._sandbox;
818
+ }
819
+ /** True when the client is in live/production mode. */
820
+ get isLive() {
821
+ return !this._sandbox;
822
+ }
823
+ /** Returns 'sandbox' or 'live'. */
824
+ get environment() {
825
+ return this._sandbox ? "sandbox" : "live";
514
826
  }
515
827
  // ─────────────────────────────────────────────────────────────────────────
516
828
  // PRIVATE METHODS
@@ -601,18 +913,22 @@ var OptropicClient = class {
601
913
  requestId
602
914
  });
603
915
  }
916
+ if (response.status === 204) {
917
+ return void 0;
918
+ }
604
919
  const json = await response.json();
605
- if (json.error) {
920
+ if (json && typeof json === "object" && "error" in json && json.error) {
921
+ const err = json.error;
606
922
  throw createErrorFromResponse(response.status, {
607
923
  // Justified: Error code string from API may not match SDK's ErrorCode enum exactly
608
924
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
609
- code: json.error.code,
610
- message: json.error.message,
611
- details: json.error.details,
925
+ code: err.code ?? "UNKNOWN_ERROR",
926
+ message: err.message ?? "Unknown error",
927
+ details: err.details,
612
928
  requestId: json.requestId
613
929
  });
614
930
  }
615
- return json.data;
931
+ return json;
616
932
  } catch (error) {
617
933
  clearTimeout(timeoutId);
618
934
  if (error instanceof OptropicError) {
@@ -637,18 +953,66 @@ function createClient(config) {
637
953
  return new OptropicClient(config);
638
954
  }
639
955
 
956
+ // src/webhooks.ts
957
+ async function computeHmacSha256(secret, message) {
958
+ const encoder = new TextEncoder();
959
+ if (typeof globalThis.crypto?.subtle !== "undefined") {
960
+ const key = await globalThis.crypto.subtle.importKey(
961
+ "raw",
962
+ encoder.encode(secret),
963
+ // OPSEC: Web Crypto API requires this exact algorithm identifier
964
+ { name: "HMAC", hash: "SHA-256" },
965
+ false,
966
+ ["sign"]
967
+ );
968
+ const sig = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(message));
969
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
970
+ }
971
+ const { createHmac } = await import("crypto");
972
+ return createHmac("sha256", secret).update(message).digest("hex");
973
+ }
974
+ function timingSafeEqual(a, b) {
975
+ if (a.length !== b.length) return false;
976
+ let result = 0;
977
+ for (let i = 0; i < a.length; i++) {
978
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
979
+ }
980
+ return result === 0;
981
+ }
982
+ async function verifyWebhookSignature(options) {
983
+ const { payload, signature, timestamp, secret, tolerance = 300 } = options;
984
+ const ts = parseInt(timestamp, 10);
985
+ if (isNaN(ts)) {
986
+ return { valid: false, reason: "Invalid timestamp" };
987
+ }
988
+ const age = Math.abs(Math.floor(Date.now() / 1e3) - ts);
989
+ if (age > tolerance) {
990
+ return { valid: false, reason: `Timestamp too old (${age}s > ${tolerance}s tolerance)` };
991
+ }
992
+ const signedPayload = `${timestamp}.${payload}`;
993
+ const expectedHex = await computeHmacSha256(secret, signedPayload);
994
+ const expected = `sha256=${expectedHex}`;
995
+ if (!timingSafeEqual(expected, signature)) {
996
+ return { valid: false, reason: "Signature mismatch" };
997
+ }
998
+ return { valid: true };
999
+ }
1000
+
640
1001
  // src/index.ts
641
- var SDK_VERSION2 = "1.0.0";
1002
+ var SDK_VERSION2 = "2.0.0";
642
1003
  // Annotate the CommonJS export names for ESM import in node:
643
1004
  0 && (module.exports = {
644
1005
  AssetsResource,
1006
+ AuditResource,
645
1007
  AuthenticationError,
646
1008
  BatchNotFoundError,
647
1009
  CodeNotFoundError,
1010
+ ComplianceResource,
648
1011
  InvalidCodeError,
649
1012
  InvalidGTINError,
650
1013
  InvalidSerialError,
651
1014
  KeysResource,
1015
+ KeysetsResource,
652
1016
  NetworkError,
653
1017
  OptropicClient,
654
1018
  OptropicError,
@@ -656,7 +1020,9 @@ var SDK_VERSION2 = "1.0.0";
656
1020
  RateLimitedError,
657
1021
  RevokedCodeError,
658
1022
  SDK_VERSION,
1023
+ SchemasResource,
659
1024
  ServiceUnavailableError,
660
1025
  TimeoutError,
661
- createClient
1026
+ createClient,
1027
+ verifyWebhookSignature
662
1028
  });