optropic 1.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.js ADDED
@@ -0,0 +1,618 @@
1
+ // src/errors.ts
2
+ var OptropicError = class extends Error {
3
+ /**
4
+ * Error code for programmatic handling.
5
+ */
6
+ code;
7
+ /**
8
+ * HTTP status code (if applicable).
9
+ */
10
+ statusCode;
11
+ /**
12
+ * Whether this error can be retried.
13
+ */
14
+ retryable;
15
+ /**
16
+ * Additional details about the error.
17
+ */
18
+ details;
19
+ /**
20
+ * Request ID for support reference.
21
+ */
22
+ requestId;
23
+ constructor(code, message, options) {
24
+ super(message, { cause: options?.cause });
25
+ this.name = "OptropicError";
26
+ this.code = code;
27
+ this.statusCode = options?.statusCode;
28
+ this.retryable = options?.retryable ?? false;
29
+ this.details = options?.details;
30
+ this.requestId = options?.requestId;
31
+ if (Error.captureStackTrace) {
32
+ Error.captureStackTrace(this, this.constructor);
33
+ }
34
+ }
35
+ /**
36
+ * Create a JSON-serializable representation.
37
+ */
38
+ toJSON() {
39
+ return {
40
+ name: this.name,
41
+ code: this.code,
42
+ message: this.message,
43
+ statusCode: this.statusCode,
44
+ retryable: this.retryable,
45
+ details: this.details,
46
+ requestId: this.requestId
47
+ };
48
+ }
49
+ };
50
+ var AuthenticationError = class extends OptropicError {
51
+ constructor(message = "Authentication failed. Please check your API key.", options) {
52
+ super(options?.code ?? "INVALID_API_KEY", message, {
53
+ statusCode: 401,
54
+ retryable: false,
55
+ details: options?.details,
56
+ requestId: options?.requestId
57
+ });
58
+ this.name = "AuthenticationError";
59
+ }
60
+ };
61
+ var InvalidSerialError = class extends OptropicError {
62
+ /**
63
+ * The invalid serial that was provided.
64
+ */
65
+ serial;
66
+ constructor(serial, message, options) {
67
+ super(
68
+ "INVALID_SERIAL",
69
+ message ?? `Invalid serial number: "${serial}". Serial must be 1-20 alphanumeric characters.`,
70
+ {
71
+ statusCode: 400,
72
+ retryable: false,
73
+ details: { serial, ...options?.details },
74
+ requestId: options?.requestId
75
+ }
76
+ );
77
+ this.name = "InvalidSerialError";
78
+ this.serial = serial;
79
+ }
80
+ };
81
+ var InvalidGTINError = class extends OptropicError {
82
+ /**
83
+ * The invalid GTIN that was provided.
84
+ */
85
+ gtin;
86
+ constructor(gtin, message, options) {
87
+ super(
88
+ "INVALID_GTIN",
89
+ message ?? `Invalid GTIN: "${gtin}". GTIN must be 8, 12, 13, or 14 digits with valid check digit.`,
90
+ {
91
+ statusCode: 400,
92
+ retryable: false,
93
+ details: { gtin, ...options?.details },
94
+ requestId: options?.requestId
95
+ }
96
+ );
97
+ this.name = "InvalidGTINError";
98
+ this.gtin = gtin;
99
+ }
100
+ };
101
+ var InvalidCodeError = class extends OptropicError {
102
+ /**
103
+ * The invalid code that was provided.
104
+ */
105
+ invalidCode;
106
+ constructor(invalidCode, message, options) {
107
+ super(
108
+ "INVALID_CODE_FORMAT",
109
+ message ?? `Invalid code format: "${invalidCode}". Expected a valid Optropic verification URL or code ID.`,
110
+ {
111
+ statusCode: 400,
112
+ retryable: false,
113
+ details: { code: invalidCode, ...options?.details },
114
+ requestId: options?.requestId
115
+ }
116
+ );
117
+ this.name = "InvalidCodeError";
118
+ this.invalidCode = invalidCode;
119
+ }
120
+ };
121
+ var RevokedCodeError = class extends OptropicError {
122
+ /**
123
+ * The revoked code.
124
+ */
125
+ revokedCode;
126
+ /**
127
+ * Reason for revocation.
128
+ */
129
+ reason;
130
+ /**
131
+ * When the code was revoked.
132
+ */
133
+ revokedAt;
134
+ constructor(code, reason, revokedAt, options) {
135
+ super(
136
+ "CODE_REVOKED",
137
+ `Code has been revoked: ${reason}`,
138
+ {
139
+ statusCode: 410,
140
+ retryable: false,
141
+ details: { code, reason, revokedAt, ...options?.details },
142
+ requestId: options?.requestId
143
+ }
144
+ );
145
+ this.name = "RevokedCodeError";
146
+ this.revokedCode = code;
147
+ this.reason = reason;
148
+ this.revokedAt = revokedAt;
149
+ }
150
+ };
151
+ var CodeNotFoundError = class extends OptropicError {
152
+ /**
153
+ * The code that was not found.
154
+ */
155
+ searchedCode;
156
+ constructor(code, options) {
157
+ super(
158
+ "CODE_NOT_FOUND",
159
+ `Code not found: "${code}". The code may not exist or may have been entered incorrectly.`,
160
+ {
161
+ statusCode: 404,
162
+ retryable: false,
163
+ details: { code, ...options?.details },
164
+ requestId: options?.requestId
165
+ }
166
+ );
167
+ this.name = "CodeNotFoundError";
168
+ this.searchedCode = code;
169
+ }
170
+ };
171
+ var BatchNotFoundError = class extends OptropicError {
172
+ /**
173
+ * The batch ID that was not found.
174
+ */
175
+ batchId;
176
+ constructor(batchId, options) {
177
+ super(
178
+ "BATCH_NOT_FOUND",
179
+ `Batch not found: "${batchId}".`,
180
+ {
181
+ statusCode: 404,
182
+ retryable: false,
183
+ details: { batchId, ...options?.details },
184
+ requestId: options?.requestId
185
+ }
186
+ );
187
+ this.name = "BatchNotFoundError";
188
+ this.batchId = batchId;
189
+ }
190
+ };
191
+ var RateLimitedError = class extends OptropicError {
192
+ /**
193
+ * Seconds until rate limit resets.
194
+ */
195
+ retryAfter;
196
+ /**
197
+ * Current rate limit (requests per period).
198
+ */
199
+ limit;
200
+ /**
201
+ * Remaining requests in current period.
202
+ */
203
+ remaining;
204
+ constructor(retryAfter, options) {
205
+ super(
206
+ "RATE_LIMITED",
207
+ `Rate limit exceeded. Please retry after ${retryAfter} seconds.`,
208
+ {
209
+ statusCode: 429,
210
+ retryable: true,
211
+ details: {
212
+ retryAfter,
213
+ limit: options?.limit,
214
+ remaining: options?.remaining,
215
+ ...options?.details
216
+ },
217
+ requestId: options?.requestId
218
+ }
219
+ );
220
+ this.name = "RateLimitedError";
221
+ this.retryAfter = retryAfter;
222
+ this.limit = options?.limit ?? 0;
223
+ this.remaining = options?.remaining ?? 0;
224
+ }
225
+ };
226
+ var QuotaExceededError = class extends OptropicError {
227
+ /**
228
+ * Current quota limit.
229
+ */
230
+ quotaLimit;
231
+ /**
232
+ * Quota used.
233
+ */
234
+ quotaUsed;
235
+ /**
236
+ * When quota resets (ISO 8601).
237
+ */
238
+ resetsAt;
239
+ constructor(quotaLimit, quotaUsed, resetsAt, options) {
240
+ super(
241
+ "QUOTA_EXCEEDED",
242
+ `Monthly quota exceeded (${quotaUsed}/${quotaLimit}). Resets at ${resetsAt}.`,
243
+ {
244
+ statusCode: 402,
245
+ retryable: false,
246
+ details: { quotaLimit, quotaUsed, resetsAt, ...options?.details },
247
+ requestId: options?.requestId
248
+ }
249
+ );
250
+ this.name = "QuotaExceededError";
251
+ this.quotaLimit = quotaLimit;
252
+ this.quotaUsed = quotaUsed;
253
+ this.resetsAt = resetsAt;
254
+ }
255
+ };
256
+ var NetworkError = class extends OptropicError {
257
+ constructor(message = "Network error occurred. Please check your connection.", options) {
258
+ super("NETWORK_ERROR", message, {
259
+ statusCode: void 0,
260
+ retryable: true,
261
+ details: options?.details,
262
+ requestId: options?.requestId,
263
+ cause: options?.cause
264
+ });
265
+ this.name = "NetworkError";
266
+ }
267
+ };
268
+ var TimeoutError = class extends OptropicError {
269
+ /**
270
+ * Timeout duration in milliseconds.
271
+ */
272
+ timeoutMs;
273
+ constructor(timeoutMs, options) {
274
+ super(
275
+ "TIMEOUT",
276
+ `Request timed out after ${timeoutMs}ms. Please try again.`,
277
+ {
278
+ statusCode: 408,
279
+ retryable: true,
280
+ details: { timeoutMs, ...options?.details },
281
+ requestId: options?.requestId
282
+ }
283
+ );
284
+ this.name = "TimeoutError";
285
+ this.timeoutMs = timeoutMs;
286
+ }
287
+ };
288
+ var ServiceUnavailableError = class extends OptropicError {
289
+ constructor(message = "Service temporarily unavailable. Please try again later.", options) {
290
+ super("SERVICE_UNAVAILABLE", message, {
291
+ statusCode: 503,
292
+ retryable: true,
293
+ details: { retryAfter: options?.retryAfter, ...options?.details },
294
+ requestId: options?.requestId
295
+ });
296
+ this.name = "ServiceUnavailableError";
297
+ }
298
+ };
299
+ function createErrorFromResponse(statusCode, body) {
300
+ const code = body.code ?? "UNKNOWN_ERROR";
301
+ const message = body.message ?? "An unknown error occurred";
302
+ const { details, requestId } = body;
303
+ switch (code) {
304
+ case "INVALID_API_KEY":
305
+ case "EXPIRED_API_KEY":
306
+ case "INSUFFICIENT_PERMISSIONS":
307
+ return new AuthenticationError(message, { code, details, requestId });
308
+ case "INVALID_SERIAL":
309
+ return new InvalidSerialError(
310
+ details?.serial ?? "",
311
+ message,
312
+ { details, requestId }
313
+ );
314
+ case "INVALID_GTIN":
315
+ return new InvalidGTINError(
316
+ details?.gtin ?? "",
317
+ message,
318
+ { details, requestId }
319
+ );
320
+ case "INVALID_CODE_FORMAT":
321
+ return new InvalidCodeError(
322
+ details?.code ?? "",
323
+ message,
324
+ { details, requestId }
325
+ );
326
+ case "CODE_NOT_FOUND":
327
+ return new CodeNotFoundError(
328
+ details?.code ?? "",
329
+ { details, requestId }
330
+ );
331
+ case "BATCH_NOT_FOUND":
332
+ return new BatchNotFoundError(
333
+ details?.batchId ?? "",
334
+ { details, requestId }
335
+ );
336
+ case "CODE_REVOKED":
337
+ return new RevokedCodeError(
338
+ details?.code ?? "",
339
+ details?.reason ?? "Unknown reason",
340
+ details?.revokedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
341
+ { details, requestId }
342
+ );
343
+ case "RATE_LIMITED":
344
+ return new RateLimitedError(
345
+ details?.retryAfter ?? 60,
346
+ {
347
+ limit: details?.limit,
348
+ remaining: details?.remaining,
349
+ details,
350
+ requestId
351
+ }
352
+ );
353
+ case "QUOTA_EXCEEDED":
354
+ return new QuotaExceededError(
355
+ details?.quotaLimit ?? 0,
356
+ details?.quotaUsed ?? 0,
357
+ details?.resetsAt ?? "",
358
+ { details, requestId }
359
+ );
360
+ case "NETWORK_ERROR":
361
+ return new NetworkError(message, { details, requestId });
362
+ case "TIMEOUT":
363
+ return new TimeoutError(
364
+ details?.timeoutMs ?? 3e4,
365
+ { details, requestId }
366
+ );
367
+ case "SERVICE_UNAVAILABLE":
368
+ return new ServiceUnavailableError(message, {
369
+ retryAfter: details?.retryAfter,
370
+ details,
371
+ requestId
372
+ });
373
+ default:
374
+ return new OptropicError(code, message, {
375
+ statusCode,
376
+ retryable: statusCode >= 500,
377
+ details,
378
+ requestId
379
+ });
380
+ }
381
+ }
382
+
383
+ // src/resources/assets.ts
384
+ var AssetsResource = class {
385
+ constructor(request) {
386
+ this.request = request;
387
+ }
388
+ async create(params) {
389
+ return this.request({ method: "POST", path: "/v1/assets", body: params });
390
+ }
391
+ async list(params) {
392
+ const query = params ? this.buildQuery(params) : "";
393
+ return this.request({ method: "GET", path: `/v1/assets${query}` });
394
+ }
395
+ async get(assetId) {
396
+ return this.request({ method: "GET", path: `/v1/assets/${encodeURIComponent(assetId)}` });
397
+ }
398
+ async verify(assetId) {
399
+ return this.request({ method: "GET", path: `/v1/assets/${encodeURIComponent(assetId)}/verify` });
400
+ }
401
+ async revoke(assetId, reason) {
402
+ return this.request({
403
+ method: "POST",
404
+ path: `/v1/assets/${encodeURIComponent(assetId)}/revoke`,
405
+ body: reason ? { reason } : void 0
406
+ });
407
+ }
408
+ async batchCreate(params) {
409
+ return this.request({ method: "POST", path: "/v1/assets/batch", body: params });
410
+ }
411
+ buildQuery(params) {
412
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
413
+ if (entries.length === 0) return "";
414
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
415
+ }
416
+ };
417
+
418
+ // src/resources/keys.ts
419
+ var KeysResource = class {
420
+ constructor(request) {
421
+ this.request = request;
422
+ }
423
+ async create(params) {
424
+ return this.request({ method: "POST", path: "/v1/keys", body: params });
425
+ }
426
+ async list() {
427
+ return this.request({ method: "GET", path: "/v1/keys" });
428
+ }
429
+ async revoke(keyId) {
430
+ await this.request({ method: "DELETE", path: `/v1/keys/${encodeURIComponent(keyId)}` });
431
+ }
432
+ };
433
+
434
+ // src/client.ts
435
+ var DEFAULT_BASE_URL = "https://api.optropic.com";
436
+ var DEFAULT_TIMEOUT = 3e4;
437
+ var SDK_VERSION = "1.0.0";
438
+ var DEFAULT_RETRY_CONFIG = {
439
+ maxRetries: 3,
440
+ baseDelay: 1e3,
441
+ maxDelay: 1e4
442
+ };
443
+ var OptropicClient = class {
444
+ config;
445
+ baseUrl;
446
+ retryConfig;
447
+ assets;
448
+ keys;
449
+ constructor(config) {
450
+ if (!config.apiKey || !this.isValidApiKey(config.apiKey)) {
451
+ throw new AuthenticationError(
452
+ "Invalid API key format. Expected format: optr_live_xxx or optr_test_xxx"
453
+ );
454
+ }
455
+ this.config = {
456
+ ...config,
457
+ timeout: config.timeout ?? DEFAULT_TIMEOUT
458
+ };
459
+ if (config.baseUrl) {
460
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
461
+ } else {
462
+ this.baseUrl = DEFAULT_BASE_URL;
463
+ }
464
+ this.retryConfig = {
465
+ ...DEFAULT_RETRY_CONFIG,
466
+ ...config.retry
467
+ };
468
+ const boundRequest = this.request.bind(this);
469
+ this.assets = new AssetsResource(boundRequest);
470
+ this.keys = new KeysResource(boundRequest);
471
+ }
472
+ // ─────────────────────────────────────────────────────────────────────────
473
+ // PRIVATE METHODS
474
+ // ─────────────────────────────────────────────────────────────────────────
475
+ isValidApiKey(apiKey) {
476
+ return /^optr_(live|test)_[a-zA-Z0-9_-]{20,}$/.test(apiKey);
477
+ }
478
+ async request(options) {
479
+ const { method, path, body, headers = {}, timeout = this.config.timeout } = options;
480
+ const url = `${this.baseUrl}${path}`;
481
+ const requestHeaders = {
482
+ "Content-Type": "application/json",
483
+ "Accept": "application/json",
484
+ "x-api-key": this.config.apiKey,
485
+ "X-SDK-Version": SDK_VERSION,
486
+ "X-SDK-Language": "typescript",
487
+ ...this.config.headers,
488
+ ...headers
489
+ };
490
+ let lastError = null;
491
+ let attempt = 0;
492
+ while (attempt <= this.retryConfig.maxRetries) {
493
+ try {
494
+ const response = await this.executeRequest(
495
+ url,
496
+ method,
497
+ requestHeaders,
498
+ body,
499
+ timeout
500
+ );
501
+ return response;
502
+ } catch (error) {
503
+ lastError = error;
504
+ if (error instanceof OptropicError && !error.retryable) {
505
+ throw error;
506
+ }
507
+ if (attempt >= this.retryConfig.maxRetries) {
508
+ throw error;
509
+ }
510
+ const delay = Math.min(
511
+ this.retryConfig.baseDelay * Math.pow(2, attempt),
512
+ this.retryConfig.maxDelay
513
+ );
514
+ if (error instanceof RateLimitedError) {
515
+ await this.sleep(error.retryAfter * 1e3);
516
+ } else {
517
+ await this.sleep(delay);
518
+ }
519
+ attempt++;
520
+ }
521
+ }
522
+ throw lastError ?? new OptropicError("UNKNOWN_ERROR", "Request failed");
523
+ }
524
+ async executeRequest(url, method, headers, body, timeout) {
525
+ const controller = new AbortController();
526
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
527
+ try {
528
+ const response = await fetch(url, {
529
+ method,
530
+ headers,
531
+ body: body ? JSON.stringify(body) : void 0,
532
+ signal: controller.signal
533
+ });
534
+ clearTimeout(timeoutId);
535
+ const requestId = response.headers.get("x-request-id") ?? "";
536
+ if (!response.ok) {
537
+ let errorBody;
538
+ try {
539
+ const json2 = await response.json();
540
+ const parsed = json2.error ?? json2;
541
+ errorBody = {
542
+ code: parsed?.code ?? "UNKNOWN_ERROR",
543
+ message: parsed?.message ?? `HTTP ${response.status}: ${response.statusText}`,
544
+ details: parsed?.details
545
+ };
546
+ } catch {
547
+ errorBody = {
548
+ code: "UNKNOWN_ERROR",
549
+ message: `HTTP ${response.status}: ${response.statusText}`
550
+ };
551
+ }
552
+ throw createErrorFromResponse(response.status, {
553
+ // Justified: Error code string from API may not match SDK's ErrorCode enum exactly
554
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
555
+ code: errorBody.code,
556
+ message: errorBody.message,
557
+ details: errorBody.details,
558
+ requestId
559
+ });
560
+ }
561
+ const json = await response.json();
562
+ if (json.error) {
563
+ throw createErrorFromResponse(response.status, {
564
+ // Justified: Error code string from API may not match SDK's ErrorCode enum exactly
565
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
566
+ code: json.error.code,
567
+ message: json.error.message,
568
+ details: json.error.details,
569
+ requestId: json.requestId
570
+ });
571
+ }
572
+ return json.data;
573
+ } catch (error) {
574
+ clearTimeout(timeoutId);
575
+ if (error instanceof OptropicError) {
576
+ throw error;
577
+ }
578
+ if (error instanceof Error) {
579
+ if (error.name === "AbortError") {
580
+ throw new TimeoutError(timeout);
581
+ }
582
+ throw new NetworkError(error.message, { cause: error });
583
+ }
584
+ throw new OptropicError("UNKNOWN_ERROR", "An unexpected error occurred", {
585
+ retryable: false
586
+ });
587
+ }
588
+ }
589
+ sleep(ms) {
590
+ return new Promise((resolve) => setTimeout(resolve, ms));
591
+ }
592
+ };
593
+ function createClient(config) {
594
+ return new OptropicClient(config);
595
+ }
596
+
597
+ // src/index.ts
598
+ var SDK_VERSION2 = "1.0.0";
599
+ export {
600
+ AssetsResource,
601
+ AuthenticationError,
602
+ BatchNotFoundError,
603
+ CodeNotFoundError,
604
+ InvalidCodeError,
605
+ InvalidGTINError,
606
+ InvalidSerialError,
607
+ KeysResource,
608
+ NetworkError,
609
+ OptropicClient,
610
+ OptropicError,
611
+ QuotaExceededError,
612
+ RateLimitedError,
613
+ RevokedCodeError,
614
+ SDK_VERSION2 as SDK_VERSION,
615
+ ServiceUnavailableError,
616
+ TimeoutError,
617
+ createClient
618
+ };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "optropic",
3
+ "version": "1.0.0",
4
+ "description": "Official Optropic SDK for TypeScript and JavaScript",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
18
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
19
+ "typecheck": "tsc --noEmit",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "clean": "rm -rf dist",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "optropic",
27
+ "authentication",
28
+ "anti-counterfeit",
29
+ "product-verification",
30
+ "qr-code",
31
+ "gs1",
32
+ "gtin",
33
+ "serialization"
34
+ ],
35
+ "author": "Optropic GmbH",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/optropic/optropic-node.git"
40
+ },
41
+ "homepage": "https://docs.optropic.com/sdk/typescript",
42
+ "bugs": {
43
+ "url": "https://github.com/optropic/optropic-node/issues"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "dependencies": {},
49
+ "devDependencies": {
50
+ "tsup": "^8.0.0",
51
+ "typescript": "^5.3.3",
52
+ "vitest": "^3.0.0"
53
+ },
54
+ "peerDependencies": {},
55
+ "files": [
56
+ "dist",
57
+ "README.md",
58
+ "LICENSE"
59
+ ],
60
+ "sideEffects": false
61
+ }