optropic 1.0.0 → 2.0.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,6 +17,14 @@ 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
@@ -28,6 +38,7 @@ __export(index_exports, {
28
38
  InvalidGTINError: () => InvalidGTINError,
29
39
  InvalidSerialError: () => InvalidSerialError,
30
40
  KeysResource: () => KeysResource,
41
+ KeysetsResource: () => KeysetsResource,
31
42
  NetworkError: () => NetworkError,
32
43
  OptropicClient: () => OptropicClient,
33
44
  OptropicError: () => OptropicError,
@@ -37,7 +48,8 @@ __export(index_exports, {
37
48
  SDK_VERSION: () => SDK_VERSION2,
38
49
  ServiceUnavailableError: () => ServiceUnavailableError,
39
50
  TimeoutError: () => TimeoutError,
40
- createClient: () => createClient
51
+ createClient: () => createClient,
52
+ verifyWebhookSignature: () => verifyWebhookSignature
41
53
  });
42
54
  module.exports = __toCommonJS(index_exports);
43
55
 
@@ -425,14 +437,26 @@ function createErrorFromResponse(statusCode, body) {
425
437
 
426
438
  // src/resources/assets.ts
427
439
  var AssetsResource = class {
428
- constructor(request) {
440
+ constructor(request, client) {
429
441
  this.request = request;
442
+ this.client = client;
430
443
  }
431
444
  async create(params) {
432
445
  return this.request({ method: "POST", path: "/v1/assets", body: params });
433
446
  }
447
+ /**
448
+ * List assets with optional filtering and pagination.
449
+ *
450
+ * When the client uses a sandbox/test API key, `is_sandbox` is
451
+ * automatically set to `true` so sandbox clients only see sandbox
452
+ * assets. Pass an explicit `is_sandbox` value to override.
453
+ */
434
454
  async list(params) {
435
- const query = params ? this.buildQuery(params) : "";
455
+ let effectiveParams = params;
456
+ if (this.client.isSandbox && (!params || params.is_sandbox === void 0)) {
457
+ effectiveParams = { ...params, is_sandbox: true };
458
+ }
459
+ const query = effectiveParams ? this.buildQuery(effectiveParams) : "";
436
460
  return this.request({ method: "GET", path: `/v1/assets${query}` });
437
461
  }
438
462
  async get(assetId) {
@@ -467,17 +491,38 @@ var KeysResource = class {
467
491
  return this.request({ method: "POST", path: "/v1/keys", body: params });
468
492
  }
469
493
  async list() {
470
- return this.request({ method: "GET", path: "/v1/keys" });
494
+ const result = await this.request({ method: "GET", path: "/v1/keys" });
495
+ return result.data;
471
496
  }
472
497
  async revoke(keyId) {
473
498
  await this.request({ method: "DELETE", path: `/v1/keys/${encodeURIComponent(keyId)}` });
474
499
  }
475
500
  };
476
501
 
502
+ // src/resources/keysets.ts
503
+ var KeysetsResource = class {
504
+ constructor(request) {
505
+ this.request = request;
506
+ }
507
+ async create(params) {
508
+ return this.request({ method: "POST", path: "/v1/keysets", body: params });
509
+ }
510
+ async list(params) {
511
+ const query = params ? this.buildQuery(params) : "";
512
+ return this.request({ method: "GET", path: `/v1/keysets${query}` });
513
+ }
514
+ buildQuery(params) {
515
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
516
+ if (entries.length === 0) return "";
517
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
518
+ }
519
+ };
520
+
477
521
  // src/client.ts
478
522
  var DEFAULT_BASE_URL = "https://api.optropic.com";
479
523
  var DEFAULT_TIMEOUT = 3e4;
480
- var SDK_VERSION = "1.0.0";
524
+ var SDK_VERSION = "2.0.0";
525
+ var SANDBOX_PREFIXES = ["optr_test_"];
481
526
  var DEFAULT_RETRY_CONFIG = {
482
527
  maxRetries: 3,
483
528
  baseDelay: 1e3,
@@ -487,8 +532,10 @@ var OptropicClient = class {
487
532
  config;
488
533
  baseUrl;
489
534
  retryConfig;
535
+ _sandbox;
490
536
  assets;
491
537
  keys;
538
+ keysets;
492
539
  constructor(config) {
493
540
  if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
494
541
  throw new AuthenticationError(
@@ -499,6 +546,11 @@ var OptropicClient = class {
499
546
  ...config,
500
547
  timeout: config.timeout ?? DEFAULT_TIMEOUT
501
548
  };
549
+ if (config.sandbox !== void 0) {
550
+ this._sandbox = config.sandbox;
551
+ } else {
552
+ this._sandbox = SANDBOX_PREFIXES.some((p) => config.apiKey.startsWith(p));
553
+ }
502
554
  if (config.baseUrl) {
503
555
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
504
556
  } else {
@@ -509,8 +561,24 @@ var OptropicClient = class {
509
561
  ...config.retry
510
562
  };
511
563
  const boundRequest = this.request.bind(this);
512
- this.assets = new AssetsResource(boundRequest);
564
+ this.assets = new AssetsResource(boundRequest, this);
513
565
  this.keys = new KeysResource(boundRequest);
566
+ this.keysets = new KeysetsResource(boundRequest);
567
+ }
568
+ // ─────────────────────────────────────────────────────────────────────────
569
+ // ENVIRONMENT DETECTION
570
+ // ─────────────────────────────────────────────────────────────────────────
571
+ /** True when the client is in sandbox mode (test API key or explicit override). */
572
+ get isSandbox() {
573
+ return this._sandbox;
574
+ }
575
+ /** True when the client is in live/production mode. */
576
+ get isLive() {
577
+ return !this._sandbox;
578
+ }
579
+ /** Returns 'sandbox' or 'live'. */
580
+ get environment() {
581
+ return this._sandbox ? "sandbox" : "live";
514
582
  }
515
583
  // ─────────────────────────────────────────────────────────────────────────
516
584
  // PRIVATE METHODS
@@ -601,18 +669,22 @@ var OptropicClient = class {
601
669
  requestId
602
670
  });
603
671
  }
672
+ if (response.status === 204) {
673
+ return void 0;
674
+ }
604
675
  const json = await response.json();
605
- if (json.error) {
676
+ if (json && typeof json === "object" && "error" in json && json.error) {
677
+ const err = json.error;
606
678
  throw createErrorFromResponse(response.status, {
607
679
  // Justified: Error code string from API may not match SDK's ErrorCode enum exactly
608
680
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
609
- code: json.error.code,
610
- message: json.error.message,
611
- details: json.error.details,
681
+ code: err.code ?? "UNKNOWN_ERROR",
682
+ message: err.message ?? "Unknown error",
683
+ details: err.details,
612
684
  requestId: json.requestId
613
685
  });
614
686
  }
615
- return json.data;
687
+ return json;
616
688
  } catch (error) {
617
689
  clearTimeout(timeoutId);
618
690
  if (error instanceof OptropicError) {
@@ -637,8 +709,53 @@ function createClient(config) {
637
709
  return new OptropicClient(config);
638
710
  }
639
711
 
712
+ // src/webhooks.ts
713
+ async function computeHmacSha256(secret, message) {
714
+ const encoder = new TextEncoder();
715
+ if (typeof globalThis.crypto?.subtle !== "undefined") {
716
+ const key = await globalThis.crypto.subtle.importKey(
717
+ "raw",
718
+ encoder.encode(secret),
719
+ // OPSEC: Web Crypto API requires this exact algorithm identifier
720
+ { name: "HMAC", hash: "SHA-256" },
721
+ false,
722
+ ["sign"]
723
+ );
724
+ const sig = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(message));
725
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
726
+ }
727
+ const { createHmac } = await import("crypto");
728
+ return createHmac("sha256", secret).update(message).digest("hex");
729
+ }
730
+ function timingSafeEqual(a, b) {
731
+ if (a.length !== b.length) return false;
732
+ let result = 0;
733
+ for (let i = 0; i < a.length; i++) {
734
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
735
+ }
736
+ return result === 0;
737
+ }
738
+ async function verifyWebhookSignature(options) {
739
+ const { payload, signature, timestamp, secret, tolerance = 300 } = options;
740
+ const ts = parseInt(timestamp, 10);
741
+ if (isNaN(ts)) {
742
+ return { valid: false, reason: "Invalid timestamp" };
743
+ }
744
+ const age = Math.abs(Math.floor(Date.now() / 1e3) - ts);
745
+ if (age > tolerance) {
746
+ return { valid: false, reason: `Timestamp too old (${age}s > ${tolerance}s tolerance)` };
747
+ }
748
+ const signedPayload = `${timestamp}.${payload}`;
749
+ const expectedHex = await computeHmacSha256(secret, signedPayload);
750
+ const expected = `sha256=${expectedHex}`;
751
+ if (!timingSafeEqual(expected, signature)) {
752
+ return { valid: false, reason: "Signature mismatch" };
753
+ }
754
+ return { valid: true };
755
+ }
756
+
640
757
  // src/index.ts
641
- var SDK_VERSION2 = "1.0.0";
758
+ var SDK_VERSION2 = "2.0.0";
642
759
  // Annotate the CommonJS export names for ESM import in node:
643
760
  0 && (module.exports = {
644
761
  AssetsResource,
@@ -649,6 +766,7 @@ var SDK_VERSION2 = "1.0.0";
649
766
  InvalidGTINError,
650
767
  InvalidSerialError,
651
768
  KeysResource,
769
+ KeysetsResource,
652
770
  NetworkError,
653
771
  OptropicClient,
654
772
  OptropicError,
@@ -658,5 +776,6 @@ var SDK_VERSION2 = "1.0.0";
658
776
  SDK_VERSION,
659
777
  ServiceUnavailableError,
660
778
  TimeoutError,
661
- createClient
779
+ createClient,
780
+ verifyWebhookSignature
662
781
  });
package/dist/index.d.cts CHANGED
@@ -21,6 +21,11 @@ interface OptropicConfig {
21
21
  * Format: optr_live_xxx (production) or optr_test_xxx (test)
22
22
  */
23
23
  readonly apiKey: string;
24
+ /**
25
+ * Explicitly set sandbox mode. When omitted, auto-detected from
26
+ * the API key prefix: `optr_test_*` → sandbox, `optr_live_*` → live.
27
+ */
28
+ readonly sandbox?: boolean;
24
29
  /**
25
30
  * Base URL override (for self-hosted deployments).
26
31
  * If not provided, uses Optropic's managed infrastructure.
@@ -67,60 +72,93 @@ interface RetryConfig {
67
72
  */
68
73
  type ErrorCode = 'INVALID_API_KEY' | 'EXPIRED_API_KEY' | 'INSUFFICIENT_PERMISSIONS' | 'INVALID_GTIN' | 'INVALID_SERIAL' | 'INVALID_CODE_FORMAT' | 'INVALID_BATCH_CONFIG' | 'CODE_NOT_FOUND' | 'BATCH_NOT_FOUND' | 'PRODUCT_NOT_FOUND' | 'CODE_REVOKED' | 'CODE_EXPIRED' | 'BATCH_ALREADY_EXISTS' | 'RATE_LIMITED' | 'QUOTA_EXCEEDED' | 'NETWORK_ERROR' | 'TIMEOUT' | 'SERVICE_UNAVAILABLE' | 'INTERNAL_ERROR' | 'UNKNOWN_ERROR';
69
74
 
75
+ /**
76
+ * Assets Resource
77
+ *
78
+ * CRUD operations for digitally signed assets.
79
+ */
80
+
70
81
  interface Asset {
71
82
  readonly id: string;
72
- readonly short_id: string;
73
- readonly gtin: string;
74
- readonly serial: string;
83
+ readonly shortId: string;
84
+ readonly tenantId: string;
85
+ readonly keysetId: string;
86
+ readonly externalId: string | null;
87
+ readonly vertical: string;
88
+ readonly securityLevel: 'signed' | 'provenance';
89
+ readonly assetConfig: Record<string, unknown>;
90
+ readonly signatureHex: string;
75
91
  readonly status: 'active' | 'revoked';
76
- readonly security_level: 'signed' | 'provenance';
77
- readonly metadata?: Record<string, string>;
78
- readonly created_at: string;
79
- readonly revoked_at?: string;
92
+ readonly isSandbox: boolean;
93
+ readonly verificationCount: number;
94
+ readonly metadata: Record<string, unknown> | null;
95
+ readonly createdAt: string;
96
+ readonly updatedAt: string;
80
97
  }
81
98
  interface CreateAssetParams {
82
- readonly gtin: string;
83
- readonly serial: string;
84
- readonly metadata?: Record<string, string>;
99
+ readonly keysetId: string;
100
+ readonly externalId?: string;
101
+ readonly vertical?: string;
102
+ readonly securityLevel?: 'signed' | 'provenance';
103
+ readonly assetConfig?: Record<string, unknown>;
104
+ readonly metadata?: Record<string, unknown>;
85
105
  }
86
106
  interface ListAssetsParams {
87
- readonly limit?: number;
88
- readonly offset?: number;
107
+ readonly page?: number;
108
+ readonly per_page?: number;
89
109
  readonly status?: 'active' | 'revoked';
90
- readonly gtin?: string;
110
+ readonly vertical?: string;
111
+ /** Filter by sandbox status. Auto-set when using a test API key. */
112
+ readonly is_sandbox?: boolean;
113
+ }
114
+ interface ListAssetsResponse {
115
+ readonly data: Asset[];
116
+ readonly pagination: {
117
+ readonly total: number;
118
+ readonly page: number;
119
+ readonly perPage: number;
120
+ readonly totalPages: number;
121
+ };
91
122
  }
92
123
  interface VerifyResult {
93
124
  readonly id: string;
94
125
  readonly status: string;
95
- readonly security_level: string;
96
- readonly signature_valid: boolean;
97
- readonly verification_count: number;
98
- readonly last_verified_at: string;
99
- readonly revocation_status: 'active' | 'revoked';
100
- readonly verification_mode: 'online' | 'offline';
101
- readonly provenance_valid?: boolean;
102
- readonly evidence?: VerificationEvidence;
103
- }
104
- interface VerificationEvidence {
105
- readonly signature_valid: boolean;
106
- readonly revocation_status: string;
107
- readonly verification_mode: string;
108
- readonly provenance_valid?: boolean;
109
- readonly security_level?: string;
110
- readonly verification_count?: number;
126
+ readonly securityLevel: string;
127
+ readonly signatureValid: boolean;
128
+ readonly provenanceValid?: boolean;
129
+ readonly revocationStatus: 'active' | 'revoked';
130
+ readonly verificationMode: 'online' | 'offline';
131
+ readonly verificationCount: number;
132
+ readonly lastVerifiedAt: string;
111
133
  }
112
134
  interface BatchCreateParams {
113
- readonly assets: CreateAssetParams[];
135
+ readonly keysetId: string;
136
+ readonly assets: Array<{
137
+ readonly externalId?: string;
138
+ readonly vertical?: string;
139
+ readonly securityLevel?: 'signed' | 'provenance';
140
+ readonly assetConfig?: Record<string, unknown>;
141
+ readonly metadata?: Record<string, unknown>;
142
+ }>;
114
143
  }
115
144
  interface BatchCreateResult {
116
145
  readonly created: number;
117
- readonly asset_ids: string[];
146
+ readonly requested: number;
147
+ readonly assets: Asset[];
118
148
  }
119
149
  declare class AssetsResource {
120
150
  private readonly request;
121
- constructor(request: RequestFn);
151
+ private readonly client;
152
+ constructor(request: RequestFn, client: OptropicClient);
122
153
  create(params: CreateAssetParams): Promise<Asset>;
123
- list(params?: ListAssetsParams): Promise<Asset[]>;
154
+ /**
155
+ * List assets with optional filtering and pagination.
156
+ *
157
+ * When the client uses a sandbox/test API key, `is_sandbox` is
158
+ * automatically set to `true` so sandbox clients only see sandbox
159
+ * assets. Pass an explicit `is_sandbox` value to override.
160
+ */
161
+ list(params?: ListAssetsParams): Promise<ListAssetsResponse>;
124
162
  get(assetId: string): Promise<Asset>;
125
163
  verify(assetId: string): Promise<VerifyResult>;
126
164
  revoke(assetId: string, reason?: string): Promise<Asset>;
@@ -128,21 +166,31 @@ declare class AssetsResource {
128
166
  private buildQuery;
129
167
  }
130
168
 
169
+ /**
170
+ * Keys Resource
171
+ *
172
+ * API key management operations.
173
+ */
174
+
131
175
  interface ApiKey {
132
176
  readonly id: string;
133
- readonly name: string;
134
- readonly key_prefix: string;
177
+ readonly label: string;
178
+ readonly prefix: string;
135
179
  readonly environment: string;
136
- readonly permissions: string[];
137
180
  readonly created_at: string;
138
181
  readonly last_used_at?: string;
139
182
  }
140
183
  interface CreateKeyParams {
141
- readonly name?: string;
142
- readonly environment?: 'production' | 'test';
184
+ readonly label?: string;
185
+ readonly environment?: 'live' | 'test';
143
186
  }
144
- interface CreateKeyResult extends ApiKey {
187
+ interface CreateKeyResult {
145
188
  readonly key: string;
189
+ readonly prefix: string;
190
+ readonly id: string;
191
+ readonly label: string;
192
+ readonly environment: string;
193
+ readonly created_at: string;
146
194
  }
147
195
  declare class KeysResource {
148
196
  private readonly request;
@@ -152,6 +200,44 @@ declare class KeysResource {
152
200
  revoke(keyId: string): Promise<void>;
153
201
  }
154
202
 
203
+ /**
204
+ * Keysets Resource
205
+ *
206
+ * Signing keyset management operations.
207
+ */
208
+
209
+ interface Keyset {
210
+ readonly id: string;
211
+ readonly name: string;
212
+ readonly publicKey: string;
213
+ readonly tenantId: string;
214
+ readonly isActive: boolean;
215
+ readonly createdAt: string;
216
+ }
217
+ interface CreateKeysetParams {
218
+ readonly name?: string;
219
+ }
220
+ interface ListKeysetsParams {
221
+ readonly page?: number;
222
+ readonly per_page?: number;
223
+ }
224
+ interface ListKeysetsResponse {
225
+ readonly data: Keyset[];
226
+ readonly pagination: {
227
+ readonly total: number;
228
+ readonly page: number;
229
+ readonly perPage: number;
230
+ readonly totalPages: number;
231
+ };
232
+ }
233
+ declare class KeysetsResource {
234
+ private readonly request;
235
+ constructor(request: RequestFn);
236
+ create(params?: CreateKeysetParams): Promise<Keyset>;
237
+ list(params?: ListKeysetsParams): Promise<ListKeysetsResponse>;
238
+ private buildQuery;
239
+ }
240
+
155
241
  /**
156
242
  * optropic - OptropicClient
157
243
  *
@@ -193,9 +279,17 @@ declare class OptropicClient {
193
279
  private readonly config;
194
280
  private readonly baseUrl;
195
281
  private readonly retryConfig;
282
+ private readonly _sandbox;
196
283
  readonly assets: AssetsResource;
197
284
  readonly keys: KeysResource;
285
+ readonly keysets: KeysetsResource;
198
286
  constructor(config: OptropicConfig);
287
+ /** True when the client is in sandbox mode (test API key or explicit override). */
288
+ get isSandbox(): boolean;
289
+ /** True when the client is in live/production mode. */
290
+ get isLive(): boolean;
291
+ /** Returns 'sandbox' or 'live'. */
292
+ get environment(): 'sandbox' | 'live';
199
293
  private isValidApiKey;
200
294
  private request;
201
295
  private executeRequest;
@@ -218,6 +312,51 @@ declare class OptropicClient {
218
312
  */
219
313
  declare function createClient(config: OptropicConfig): OptropicClient;
220
314
 
315
+ /**
316
+ * Webhook Signature Verification
317
+ *
318
+ * Verifies that incoming webhook payloads were signed by Optropic.
319
+ * Uses the endpoint secret provided when the webhook was registered.
320
+ *
321
+ * @module optropic/webhooks
322
+ */
323
+ interface WebhookVerifyOptions {
324
+ /** Raw request body (string). Must not be parsed JSON. */
325
+ readonly payload: string;
326
+ /** Value of the `X-Optropic-Signature` header. Format: `sha256=<hex>` */
327
+ readonly signature: string;
328
+ /** Value of the `X-Optropic-Timestamp` header (unix seconds). */
329
+ readonly timestamp: string;
330
+ /** The endpoint secret from webhook registration. */
331
+ readonly secret: string;
332
+ /** Maximum allowed age of the timestamp in seconds. @default 300 (5 minutes) */
333
+ readonly tolerance?: number;
334
+ }
335
+ interface WebhookVerifyResult {
336
+ readonly valid: boolean;
337
+ readonly reason?: string;
338
+ }
339
+ /**
340
+ * Verify a webhook payload signature.
341
+ *
342
+ * @example
343
+ * ```typescript
344
+ * import { verifyWebhookSignature } from 'optropic';
345
+ *
346
+ * const result = await verifyWebhookSignature({
347
+ * payload: req.body, // raw string body
348
+ * signature: req.headers['x-optropic-signature'],
349
+ * timestamp: req.headers['x-optropic-timestamp'],
350
+ * secret: process.env.WEBHOOK_SECRET!,
351
+ * });
352
+ *
353
+ * if (!result.valid) {
354
+ * return res.status(401).json({ error: result.reason });
355
+ * }
356
+ * ```
357
+ */
358
+ declare function verifyWebhookSignature(options: WebhookVerifyOptions): Promise<WebhookVerifyResult>;
359
+
221
360
  /**
222
361
  * optropic - Error Classes
223
362
  *
@@ -473,6 +612,6 @@ declare class ServiceUnavailableError extends OptropicError {
473
612
  });
474
613
  }
475
614
 
476
- declare const SDK_VERSION = "1.0.0";
615
+ declare const SDK_VERSION = "2.0.0";
477
616
 
478
- export { type ApiKey, type Asset, AssetsResource, AuthenticationError, type BatchCreateParams, type BatchCreateResult, BatchNotFoundError, CodeNotFoundError, type CreateAssetParams, type CreateKeyParams, type CreateKeyResult, type ErrorCode, InvalidCodeError, InvalidGTINError, InvalidSerialError, KeysResource, type ListAssetsParams, NetworkError, OptropicClient, type OptropicConfig, OptropicError, QuotaExceededError, RateLimitedError, type RequestFn, type RetryConfig, RevokedCodeError, SDK_VERSION, ServiceUnavailableError, TimeoutError, type VerificationEvidence, type VerifyResult, createClient };
617
+ export { type ApiKey, type Asset, AssetsResource, AuthenticationError, type BatchCreateParams, type BatchCreateResult, BatchNotFoundError, CodeNotFoundError, type CreateAssetParams, type CreateKeyParams, type CreateKeyResult, type CreateKeysetParams, type ErrorCode, InvalidCodeError, InvalidGTINError, InvalidSerialError, KeysResource, type Keyset, KeysetsResource, type ListAssetsParams, type ListAssetsResponse, type ListKeysetsParams, type ListKeysetsResponse, NetworkError, OptropicClient, type OptropicConfig, OptropicError, QuotaExceededError, RateLimitedError, type RequestFn, type RetryConfig, RevokedCodeError, SDK_VERSION, ServiceUnavailableError, TimeoutError, type VerifyResult, type WebhookVerifyOptions, type WebhookVerifyResult, createClient, verifyWebhookSignature };
package/dist/index.d.ts CHANGED
@@ -21,6 +21,11 @@ interface OptropicConfig {
21
21
  * Format: optr_live_xxx (production) or optr_test_xxx (test)
22
22
  */
23
23
  readonly apiKey: string;
24
+ /**
25
+ * Explicitly set sandbox mode. When omitted, auto-detected from
26
+ * the API key prefix: `optr_test_*` → sandbox, `optr_live_*` → live.
27
+ */
28
+ readonly sandbox?: boolean;
24
29
  /**
25
30
  * Base URL override (for self-hosted deployments).
26
31
  * If not provided, uses Optropic's managed infrastructure.
@@ -67,60 +72,93 @@ interface RetryConfig {
67
72
  */
68
73
  type ErrorCode = 'INVALID_API_KEY' | 'EXPIRED_API_KEY' | 'INSUFFICIENT_PERMISSIONS' | 'INVALID_GTIN' | 'INVALID_SERIAL' | 'INVALID_CODE_FORMAT' | 'INVALID_BATCH_CONFIG' | 'CODE_NOT_FOUND' | 'BATCH_NOT_FOUND' | 'PRODUCT_NOT_FOUND' | 'CODE_REVOKED' | 'CODE_EXPIRED' | 'BATCH_ALREADY_EXISTS' | 'RATE_LIMITED' | 'QUOTA_EXCEEDED' | 'NETWORK_ERROR' | 'TIMEOUT' | 'SERVICE_UNAVAILABLE' | 'INTERNAL_ERROR' | 'UNKNOWN_ERROR';
69
74
 
75
+ /**
76
+ * Assets Resource
77
+ *
78
+ * CRUD operations for digitally signed assets.
79
+ */
80
+
70
81
  interface Asset {
71
82
  readonly id: string;
72
- readonly short_id: string;
73
- readonly gtin: string;
74
- readonly serial: string;
83
+ readonly shortId: string;
84
+ readonly tenantId: string;
85
+ readonly keysetId: string;
86
+ readonly externalId: string | null;
87
+ readonly vertical: string;
88
+ readonly securityLevel: 'signed' | 'provenance';
89
+ readonly assetConfig: Record<string, unknown>;
90
+ readonly signatureHex: string;
75
91
  readonly status: 'active' | 'revoked';
76
- readonly security_level: 'signed' | 'provenance';
77
- readonly metadata?: Record<string, string>;
78
- readonly created_at: string;
79
- readonly revoked_at?: string;
92
+ readonly isSandbox: boolean;
93
+ readonly verificationCount: number;
94
+ readonly metadata: Record<string, unknown> | null;
95
+ readonly createdAt: string;
96
+ readonly updatedAt: string;
80
97
  }
81
98
  interface CreateAssetParams {
82
- readonly gtin: string;
83
- readonly serial: string;
84
- readonly metadata?: Record<string, string>;
99
+ readonly keysetId: string;
100
+ readonly externalId?: string;
101
+ readonly vertical?: string;
102
+ readonly securityLevel?: 'signed' | 'provenance';
103
+ readonly assetConfig?: Record<string, unknown>;
104
+ readonly metadata?: Record<string, unknown>;
85
105
  }
86
106
  interface ListAssetsParams {
87
- readonly limit?: number;
88
- readonly offset?: number;
107
+ readonly page?: number;
108
+ readonly per_page?: number;
89
109
  readonly status?: 'active' | 'revoked';
90
- readonly gtin?: string;
110
+ readonly vertical?: string;
111
+ /** Filter by sandbox status. Auto-set when using a test API key. */
112
+ readonly is_sandbox?: boolean;
113
+ }
114
+ interface ListAssetsResponse {
115
+ readonly data: Asset[];
116
+ readonly pagination: {
117
+ readonly total: number;
118
+ readonly page: number;
119
+ readonly perPage: number;
120
+ readonly totalPages: number;
121
+ };
91
122
  }
92
123
  interface VerifyResult {
93
124
  readonly id: string;
94
125
  readonly status: string;
95
- readonly security_level: string;
96
- readonly signature_valid: boolean;
97
- readonly verification_count: number;
98
- readonly last_verified_at: string;
99
- readonly revocation_status: 'active' | 'revoked';
100
- readonly verification_mode: 'online' | 'offline';
101
- readonly provenance_valid?: boolean;
102
- readonly evidence?: VerificationEvidence;
103
- }
104
- interface VerificationEvidence {
105
- readonly signature_valid: boolean;
106
- readonly revocation_status: string;
107
- readonly verification_mode: string;
108
- readonly provenance_valid?: boolean;
109
- readonly security_level?: string;
110
- readonly verification_count?: number;
126
+ readonly securityLevel: string;
127
+ readonly signatureValid: boolean;
128
+ readonly provenanceValid?: boolean;
129
+ readonly revocationStatus: 'active' | 'revoked';
130
+ readonly verificationMode: 'online' | 'offline';
131
+ readonly verificationCount: number;
132
+ readonly lastVerifiedAt: string;
111
133
  }
112
134
  interface BatchCreateParams {
113
- readonly assets: CreateAssetParams[];
135
+ readonly keysetId: string;
136
+ readonly assets: Array<{
137
+ readonly externalId?: string;
138
+ readonly vertical?: string;
139
+ readonly securityLevel?: 'signed' | 'provenance';
140
+ readonly assetConfig?: Record<string, unknown>;
141
+ readonly metadata?: Record<string, unknown>;
142
+ }>;
114
143
  }
115
144
  interface BatchCreateResult {
116
145
  readonly created: number;
117
- readonly asset_ids: string[];
146
+ readonly requested: number;
147
+ readonly assets: Asset[];
118
148
  }
119
149
  declare class AssetsResource {
120
150
  private readonly request;
121
- constructor(request: RequestFn);
151
+ private readonly client;
152
+ constructor(request: RequestFn, client: OptropicClient);
122
153
  create(params: CreateAssetParams): Promise<Asset>;
123
- list(params?: ListAssetsParams): Promise<Asset[]>;
154
+ /**
155
+ * List assets with optional filtering and pagination.
156
+ *
157
+ * When the client uses a sandbox/test API key, `is_sandbox` is
158
+ * automatically set to `true` so sandbox clients only see sandbox
159
+ * assets. Pass an explicit `is_sandbox` value to override.
160
+ */
161
+ list(params?: ListAssetsParams): Promise<ListAssetsResponse>;
124
162
  get(assetId: string): Promise<Asset>;
125
163
  verify(assetId: string): Promise<VerifyResult>;
126
164
  revoke(assetId: string, reason?: string): Promise<Asset>;
@@ -128,21 +166,31 @@ declare class AssetsResource {
128
166
  private buildQuery;
129
167
  }
130
168
 
169
+ /**
170
+ * Keys Resource
171
+ *
172
+ * API key management operations.
173
+ */
174
+
131
175
  interface ApiKey {
132
176
  readonly id: string;
133
- readonly name: string;
134
- readonly key_prefix: string;
177
+ readonly label: string;
178
+ readonly prefix: string;
135
179
  readonly environment: string;
136
- readonly permissions: string[];
137
180
  readonly created_at: string;
138
181
  readonly last_used_at?: string;
139
182
  }
140
183
  interface CreateKeyParams {
141
- readonly name?: string;
142
- readonly environment?: 'production' | 'test';
184
+ readonly label?: string;
185
+ readonly environment?: 'live' | 'test';
143
186
  }
144
- interface CreateKeyResult extends ApiKey {
187
+ interface CreateKeyResult {
145
188
  readonly key: string;
189
+ readonly prefix: string;
190
+ readonly id: string;
191
+ readonly label: string;
192
+ readonly environment: string;
193
+ readonly created_at: string;
146
194
  }
147
195
  declare class KeysResource {
148
196
  private readonly request;
@@ -152,6 +200,44 @@ declare class KeysResource {
152
200
  revoke(keyId: string): Promise<void>;
153
201
  }
154
202
 
203
+ /**
204
+ * Keysets Resource
205
+ *
206
+ * Signing keyset management operations.
207
+ */
208
+
209
+ interface Keyset {
210
+ readonly id: string;
211
+ readonly name: string;
212
+ readonly publicKey: string;
213
+ readonly tenantId: string;
214
+ readonly isActive: boolean;
215
+ readonly createdAt: string;
216
+ }
217
+ interface CreateKeysetParams {
218
+ readonly name?: string;
219
+ }
220
+ interface ListKeysetsParams {
221
+ readonly page?: number;
222
+ readonly per_page?: number;
223
+ }
224
+ interface ListKeysetsResponse {
225
+ readonly data: Keyset[];
226
+ readonly pagination: {
227
+ readonly total: number;
228
+ readonly page: number;
229
+ readonly perPage: number;
230
+ readonly totalPages: number;
231
+ };
232
+ }
233
+ declare class KeysetsResource {
234
+ private readonly request;
235
+ constructor(request: RequestFn);
236
+ create(params?: CreateKeysetParams): Promise<Keyset>;
237
+ list(params?: ListKeysetsParams): Promise<ListKeysetsResponse>;
238
+ private buildQuery;
239
+ }
240
+
155
241
  /**
156
242
  * optropic - OptropicClient
157
243
  *
@@ -193,9 +279,17 @@ declare class OptropicClient {
193
279
  private readonly config;
194
280
  private readonly baseUrl;
195
281
  private readonly retryConfig;
282
+ private readonly _sandbox;
196
283
  readonly assets: AssetsResource;
197
284
  readonly keys: KeysResource;
285
+ readonly keysets: KeysetsResource;
198
286
  constructor(config: OptropicConfig);
287
+ /** True when the client is in sandbox mode (test API key or explicit override). */
288
+ get isSandbox(): boolean;
289
+ /** True when the client is in live/production mode. */
290
+ get isLive(): boolean;
291
+ /** Returns 'sandbox' or 'live'. */
292
+ get environment(): 'sandbox' | 'live';
199
293
  private isValidApiKey;
200
294
  private request;
201
295
  private executeRequest;
@@ -218,6 +312,51 @@ declare class OptropicClient {
218
312
  */
219
313
  declare function createClient(config: OptropicConfig): OptropicClient;
220
314
 
315
+ /**
316
+ * Webhook Signature Verification
317
+ *
318
+ * Verifies that incoming webhook payloads were signed by Optropic.
319
+ * Uses the endpoint secret provided when the webhook was registered.
320
+ *
321
+ * @module optropic/webhooks
322
+ */
323
+ interface WebhookVerifyOptions {
324
+ /** Raw request body (string). Must not be parsed JSON. */
325
+ readonly payload: string;
326
+ /** Value of the `X-Optropic-Signature` header. Format: `sha256=<hex>` */
327
+ readonly signature: string;
328
+ /** Value of the `X-Optropic-Timestamp` header (unix seconds). */
329
+ readonly timestamp: string;
330
+ /** The endpoint secret from webhook registration. */
331
+ readonly secret: string;
332
+ /** Maximum allowed age of the timestamp in seconds. @default 300 (5 minutes) */
333
+ readonly tolerance?: number;
334
+ }
335
+ interface WebhookVerifyResult {
336
+ readonly valid: boolean;
337
+ readonly reason?: string;
338
+ }
339
+ /**
340
+ * Verify a webhook payload signature.
341
+ *
342
+ * @example
343
+ * ```typescript
344
+ * import { verifyWebhookSignature } from 'optropic';
345
+ *
346
+ * const result = await verifyWebhookSignature({
347
+ * payload: req.body, // raw string body
348
+ * signature: req.headers['x-optropic-signature'],
349
+ * timestamp: req.headers['x-optropic-timestamp'],
350
+ * secret: process.env.WEBHOOK_SECRET!,
351
+ * });
352
+ *
353
+ * if (!result.valid) {
354
+ * return res.status(401).json({ error: result.reason });
355
+ * }
356
+ * ```
357
+ */
358
+ declare function verifyWebhookSignature(options: WebhookVerifyOptions): Promise<WebhookVerifyResult>;
359
+
221
360
  /**
222
361
  * optropic - Error Classes
223
362
  *
@@ -473,6 +612,6 @@ declare class ServiceUnavailableError extends OptropicError {
473
612
  });
474
613
  }
475
614
 
476
- declare const SDK_VERSION = "1.0.0";
615
+ declare const SDK_VERSION = "2.0.0";
477
616
 
478
- export { type ApiKey, type Asset, AssetsResource, AuthenticationError, type BatchCreateParams, type BatchCreateResult, BatchNotFoundError, CodeNotFoundError, type CreateAssetParams, type CreateKeyParams, type CreateKeyResult, type ErrorCode, InvalidCodeError, InvalidGTINError, InvalidSerialError, KeysResource, type ListAssetsParams, NetworkError, OptropicClient, type OptropicConfig, OptropicError, QuotaExceededError, RateLimitedError, type RequestFn, type RetryConfig, RevokedCodeError, SDK_VERSION, ServiceUnavailableError, TimeoutError, type VerificationEvidence, type VerifyResult, createClient };
617
+ export { type ApiKey, type Asset, AssetsResource, AuthenticationError, type BatchCreateParams, type BatchCreateResult, BatchNotFoundError, CodeNotFoundError, type CreateAssetParams, type CreateKeyParams, type CreateKeyResult, type CreateKeysetParams, type ErrorCode, InvalidCodeError, InvalidGTINError, InvalidSerialError, KeysResource, type Keyset, KeysetsResource, type ListAssetsParams, type ListAssetsResponse, type ListKeysetsParams, type ListKeysetsResponse, NetworkError, OptropicClient, type OptropicConfig, OptropicError, QuotaExceededError, RateLimitedError, type RequestFn, type RetryConfig, RevokedCodeError, SDK_VERSION, ServiceUnavailableError, TimeoutError, type VerifyResult, type WebhookVerifyOptions, type WebhookVerifyResult, createClient, verifyWebhookSignature };
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) {
@@ -424,17 +436,38 @@ var KeysResource = class {
424
436
  return this.request({ method: "POST", path: "/v1/keys", body: params });
425
437
  }
426
438
  async list() {
427
- return this.request({ method: "GET", path: "/v1/keys" });
439
+ const result = await this.request({ method: "GET", path: "/v1/keys" });
440
+ return result.data;
428
441
  }
429
442
  async revoke(keyId) {
430
443
  await this.request({ method: "DELETE", path: `/v1/keys/${encodeURIComponent(keyId)}` });
431
444
  }
432
445
  };
433
446
 
447
+ // src/resources/keysets.ts
448
+ var KeysetsResource = class {
449
+ constructor(request) {
450
+ this.request = request;
451
+ }
452
+ async create(params) {
453
+ return this.request({ method: "POST", path: "/v1/keysets", body: params });
454
+ }
455
+ async list(params) {
456
+ const query = params ? this.buildQuery(params) : "";
457
+ return this.request({ method: "GET", path: `/v1/keysets${query}` });
458
+ }
459
+ buildQuery(params) {
460
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
461
+ if (entries.length === 0) return "";
462
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
463
+ }
464
+ };
465
+
434
466
  // src/client.ts
435
467
  var DEFAULT_BASE_URL = "https://api.optropic.com";
436
468
  var DEFAULT_TIMEOUT = 3e4;
437
- var SDK_VERSION = "1.0.0";
469
+ var SDK_VERSION = "2.0.0";
470
+ var SANDBOX_PREFIXES = ["optr_test_"];
438
471
  var DEFAULT_RETRY_CONFIG = {
439
472
  maxRetries: 3,
440
473
  baseDelay: 1e3,
@@ -444,8 +477,10 @@ var OptropicClient = class {
444
477
  config;
445
478
  baseUrl;
446
479
  retryConfig;
480
+ _sandbox;
447
481
  assets;
448
482
  keys;
483
+ keysets;
449
484
  constructor(config) {
450
485
  if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
451
486
  throw new AuthenticationError(
@@ -456,6 +491,11 @@ var OptropicClient = class {
456
491
  ...config,
457
492
  timeout: config.timeout ?? DEFAULT_TIMEOUT
458
493
  };
494
+ if (config.sandbox !== void 0) {
495
+ this._sandbox = config.sandbox;
496
+ } else {
497
+ this._sandbox = SANDBOX_PREFIXES.some((p) => config.apiKey.startsWith(p));
498
+ }
459
499
  if (config.baseUrl) {
460
500
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
461
501
  } else {
@@ -466,8 +506,24 @@ var OptropicClient = class {
466
506
  ...config.retry
467
507
  };
468
508
  const boundRequest = this.request.bind(this);
469
- this.assets = new AssetsResource(boundRequest);
509
+ this.assets = new AssetsResource(boundRequest, this);
470
510
  this.keys = new KeysResource(boundRequest);
511
+ this.keysets = new KeysetsResource(boundRequest);
512
+ }
513
+ // ─────────────────────────────────────────────────────────────────────────
514
+ // ENVIRONMENT DETECTION
515
+ // ─────────────────────────────────────────────────────────────────────────
516
+ /** True when the client is in sandbox mode (test API key or explicit override). */
517
+ get isSandbox() {
518
+ return this._sandbox;
519
+ }
520
+ /** True when the client is in live/production mode. */
521
+ get isLive() {
522
+ return !this._sandbox;
523
+ }
524
+ /** Returns 'sandbox' or 'live'. */
525
+ get environment() {
526
+ return this._sandbox ? "sandbox" : "live";
471
527
  }
472
528
  // ─────────────────────────────────────────────────────────────────────────
473
529
  // PRIVATE METHODS
@@ -558,18 +614,22 @@ var OptropicClient = class {
558
614
  requestId
559
615
  });
560
616
  }
617
+ if (response.status === 204) {
618
+ return void 0;
619
+ }
561
620
  const json = await response.json();
562
- if (json.error) {
621
+ if (json && typeof json === "object" && "error" in json && json.error) {
622
+ const err = json.error;
563
623
  throw createErrorFromResponse(response.status, {
564
624
  // Justified: Error code string from API may not match SDK's ErrorCode enum exactly
565
625
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
566
- code: json.error.code,
567
- message: json.error.message,
568
- details: json.error.details,
626
+ code: err.code ?? "UNKNOWN_ERROR",
627
+ message: err.message ?? "Unknown error",
628
+ details: err.details,
569
629
  requestId: json.requestId
570
630
  });
571
631
  }
572
- return json.data;
632
+ return json;
573
633
  } catch (error) {
574
634
  clearTimeout(timeoutId);
575
635
  if (error instanceof OptropicError) {
@@ -594,8 +654,53 @@ function createClient(config) {
594
654
  return new OptropicClient(config);
595
655
  }
596
656
 
657
+ // src/webhooks.ts
658
+ async function computeHmacSha256(secret, message) {
659
+ const encoder = new TextEncoder();
660
+ if (typeof globalThis.crypto?.subtle !== "undefined") {
661
+ const key = await globalThis.crypto.subtle.importKey(
662
+ "raw",
663
+ encoder.encode(secret),
664
+ // OPSEC: Web Crypto API requires this exact algorithm identifier
665
+ { name: "HMAC", hash: "SHA-256" },
666
+ false,
667
+ ["sign"]
668
+ );
669
+ const sig = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(message));
670
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
671
+ }
672
+ const { createHmac } = await import("crypto");
673
+ return createHmac("sha256", secret).update(message).digest("hex");
674
+ }
675
+ function timingSafeEqual(a, b) {
676
+ if (a.length !== b.length) return false;
677
+ let result = 0;
678
+ for (let i = 0; i < a.length; i++) {
679
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
680
+ }
681
+ return result === 0;
682
+ }
683
+ async function verifyWebhookSignature(options) {
684
+ const { payload, signature, timestamp, secret, tolerance = 300 } = options;
685
+ const ts = parseInt(timestamp, 10);
686
+ if (isNaN(ts)) {
687
+ return { valid: false, reason: "Invalid timestamp" };
688
+ }
689
+ const age = Math.abs(Math.floor(Date.now() / 1e3) - ts);
690
+ if (age > tolerance) {
691
+ return { valid: false, reason: `Timestamp too old (${age}s > ${tolerance}s tolerance)` };
692
+ }
693
+ const signedPayload = `${timestamp}.${payload}`;
694
+ const expectedHex = await computeHmacSha256(secret, signedPayload);
695
+ const expected = `sha256=${expectedHex}`;
696
+ if (!timingSafeEqual(expected, signature)) {
697
+ return { valid: false, reason: "Signature mismatch" };
698
+ }
699
+ return { valid: true };
700
+ }
701
+
597
702
  // src/index.ts
598
- var SDK_VERSION2 = "1.0.0";
703
+ var SDK_VERSION2 = "2.0.0";
599
704
  export {
600
705
  AssetsResource,
601
706
  AuthenticationError,
@@ -605,6 +710,7 @@ export {
605
710
  InvalidGTINError,
606
711
  InvalidSerialError,
607
712
  KeysResource,
713
+ KeysetsResource,
608
714
  NetworkError,
609
715
  OptropicClient,
610
716
  OptropicError,
@@ -614,5 +720,6 @@ export {
614
720
  SDK_VERSION2 as SDK_VERSION,
615
721
  ServiceUnavailableError,
616
722
  TimeoutError,
617
- createClient
723
+ createClient,
724
+ verifyWebhookSignature
618
725
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "optropic",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Official Optropic SDK for TypeScript and JavaScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",