apick-api 3.0.0 → 3.4.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/src/index.cjs CHANGED
@@ -1,623 +1,805 @@
1
- 'use strict';
2
-
3
- const DEFAULT_BASE_URL = 'https://apick.app';
4
- const DEFAULT_TIMEOUT_MS = 30_000;
5
- const MAX_OCR_BYTES = 50 * 1024 * 1024;
6
- const MAX_IMAGE_AI_BYTES = 50 * 1024 * 1024;
7
- const IMAGE_AI_SIZES = Object.freeze(['1024x1024', '1536x1024', '1024x1536', '1152x864', '864x1152']);
8
- const IMAGE_AI_SIZE_SET = new Set(IMAGE_AI_SIZES);
9
- const TTS_VOICE_IDS = Object.freeze([
10
- 'narrator_m_01', 'narrator_m_02', 'narrator_m_03', 'narrator_m_04', 'narrator_m_05',
11
- 'narrator_f_10s_01', 'narrator_f_10s_02', 'narrator_f_10s_03',
12
- 'narrator_m_20s_01', 'narrator_f_20s_01', 'narrator_f_20s_02',
13
- 'narrator_f_20s_03', 'narrator_f_20s_04', 'narrator_m_30s_01',
14
- 'narrator_m_30s_02', 'narrator_m_40s_01', 'narrator_m_80s_01'
15
- ]);
16
- const TTS_VOICE_ID_SET = new Set(TTS_VOICE_IDS);
17
-
18
- const SERVICE_DEFINITIONS = Object.freeze({
19
- businessDetails: { endpoint: '/rest/biz_detail', timeoutMs: 50_000, output: 'json' },
20
- ventureBusiness: { endpoint: '/rest/venture_biz_info', timeoutMs: 50_000, output: 'json' },
21
- trackParcel: { endpoint: '/rest/parcel_tracking', timeoutMs: 30_000, output: 'json' },
22
- trackParcelAuto: { endpoint: '/rest/parcel_tracking_auto', timeoutMs: 30_000, output: 'json' },
23
- validateEmail: { endpoint: '/rest/check_email_valid', timeoutMs: 20_000, output: 'json' },
24
- validatePhone: { endpoint: '/rest/check_phone_valid', timeoutMs: 20_000, output: 'json' },
25
- holidays: { endpoint: '/rest/holiday_info', timeoutMs: 35_000, output: 'json' },
26
- searchAddress: { endpoint: '/rest/search_juso', timeoutMs: 20_000, output: 'json' },
27
- ocr: { endpoint: '/rest/ocr', timeoutMs: 35_000, output: 'json' },
28
- maskResidentNumber: { endpoint: '/rest/hide_rrn', timeoutMs: 35_000, output: 'binary', filename: 'masked.png' },
29
- maskResidenceCard: { endpoint: '/rest/identity_document_residence_card', timeoutMs: 35_000, output: 'json' },
30
- maskPassport: { endpoint: '/rest/identity_document_passport', timeoutMs: 35_000, output: 'json' },
31
- maskIdCard: { endpoint: '/rest/identity_document_id_card', timeoutMs: 35_000, output: 'json' },
32
- maskDriverLicense: { endpoint: '/rest/identity_document_driver_license', timeoutMs: 35_000, output: 'json' },
33
- dnsLookup: { endpoint: '/rest/nslookup', timeoutMs: 16_000, output: 'json' },
34
- geolocate: { endpoint: '/rest/location', timeoutMs: 35_000, output: 'json' },
35
- whois: { endpoint: '/rest/whois', timeoutMs: 35_000, output: 'json' },
36
- googleSearch: { endpoint: '/rest/google_search', timeoutMs: 35_000, output: 'json' },
37
- googleImageSearch: { endpoint: '/rest/google_image_search', timeoutMs: 35_000, output: 'json' },
38
- screenshot: { endpoint: '/rest/url_screenshot', timeoutMs: 75_000, output: 'binary', filename: 'screenshot.jpeg' },
39
- createTtsJob: { endpoint: '/rest/tts/jobs', timeoutMs: 35_000, output: 'json' },
40
- htmlToPdf: { endpoint: '/rest/html_to_pdf', timeoutMs: 25_000, output: 'binary', filename: 'document.pdf' },
41
- jsonToExcel: { endpoint: '/rest/json_to_excel', timeoutMs: 45_000, output: 'binary', filename: 'data.xlsx' },
42
- summarize: { endpoint: '/rest/llm/text_summary', timeoutMs: 75_000, output: 'json' },
43
- polish: { endpoint: '/rest/llm/text_polish', timeoutMs: 105_000, output: 'json' },
44
- generateImages: { endpoint: '/rest/image-generation/generate', timeoutMs: 190_000, output: 'json' }
45
- });
46
-
47
- const SERVICES = Object.freeze(Object.fromEntries(
48
- Object.entries(SERVICE_DEFINITIONS).map(([name, definition]) => [
49
- name,
50
- Object.freeze({ endpoint: definition.endpoint, output: definition.output })
51
- ])
52
- ));
53
-
54
- function redact(value, apiKey) {
55
- let text = String(value || '');
56
- if (apiKey) text = text.split(apiKey).join('***');
57
- return text.replace(/(CL_AUTH_KEY\s*[:=]\s*)\S+/gi, '$1***');
58
- }
59
-
60
- function requiredString(name, value, maxLength) {
61
- if (typeof value !== 'string' || !value.trim()) {
62
- throw new TypeError(`${name} must be a non-empty string.`);
63
- }
64
- const normalized = value.trim();
65
- if (maxLength && normalized.length > maxLength) {
66
- throw new RangeError(`${name} must not exceed ${maxLength} characters.`);
67
- }
68
- return normalized;
69
- }
70
-
71
- function positiveInteger(name, value, defaultValue) {
72
- if (value === undefined || value === null || value === '') return defaultValue;
73
- const number = Number(value);
74
- if (!Number.isInteger(number) || number < 1) {
75
- throw new RangeError(`${name} must be a positive integer.`);
76
- }
77
- return number;
78
- }
79
-
80
- function normalizeBusinessNumber(value) {
81
- const normalized = requiredString('businessNumber', value).replace(/-/g, '');
82
- if (!/^\d{10}$/.test(normalized)) {
83
- throw new TypeError('businessNumber must contain exactly 10 digits.');
84
- }
85
- return normalized;
86
- }
87
-
88
- function normalizeUrl(value) {
89
- const input = requiredString('url', value);
90
- let parsed;
91
- try {
92
- parsed = new URL(input);
93
- } catch {
94
- throw new TypeError('url must be a valid HTTP or HTTPS URL.');
95
- }
96
- if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
97
- throw new TypeError('url must use HTTP or HTTPS.');
98
- }
99
- return parsed.toString();
100
- }
101
-
102
- function normalizeTtsJobId(value) {
103
- const jobId = requiredString('jobId', value);
104
- if (!/^[a-f0-9]{32}$/.test(jobId)) throw new TypeError('jobId must be a 32-character lowercase hexadecimal string.');
105
- return jobId;
106
- }
107
-
108
- function normalizeTtsVoice(value) {
109
- const voiceId = requiredString('voiceId', value);
110
- if (!TTS_VOICE_ID_SET.has(voiceId)) throw new RangeError('voiceId must be one of the supported TTS voice IDs.');
111
- return voiceId;
112
- }
113
-
114
- function numberOrNull(value) {
115
- if (value === undefined || value === null || value === '') return null;
116
- const number = Number(value);
117
- return Number.isFinite(number) ? number : null;
118
- }
119
-
120
- function responseMeta(api, headers) {
121
- const metadata = api && typeof api === 'object' ? api : {};
122
- const readHeader = (name) => headers && typeof headers.get === 'function' ? headers.get(name) : null;
123
- return Object.freeze({
124
- cost: numberOrNull(metadata.cost ?? readHeader('cost')),
125
- durationMs: numberOrNull(metadata.ms ?? readHeader('ms'))
126
- });
127
- }
128
-
129
- function publicErrorMessage(body, fallback) {
130
- if (!body || typeof body !== 'object') return fallback;
131
- const candidates = [
132
- body.data && body.data.error,
133
- body.result && body.result.error,
134
- body.error,
135
- body.message,
136
- body.msg
137
- ];
138
- for (const candidate of candidates) {
139
- if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();
140
- }
141
- return fallback;
142
- }
143
-
144
- function parseFilename(headerValue, fallback) {
145
- if (typeof headerValue !== 'string') return fallback;
146
- const encoded = /filename\*=UTF-8''([^;]+)/i.exec(headerValue);
147
- if (encoded) {
148
- try { return decodeURIComponent(encoded[1]).replace(/[\\/]/g, '_'); } catch { /* use fallback parser */ }
149
- }
150
- const plain = /filename="?([^";]+)"?/i.exec(headerValue);
151
- return plain ? plain[1].trim().replace(/[\\/]/g, '_') : fallback;
152
- }
153
-
154
- function inferImageType(filename) {
155
- const lower = String(filename || '').toLowerCase();
156
- if (lower.endsWith('.png')) return 'image/png';
157
- if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
158
- if (lower.endsWith('.webp')) return 'image/webp';
159
- return '';
160
- }
161
-
162
- async function normalizeImage(image, options) {
163
- const config = options || {};
164
- let blob;
165
- let filename = config.filename || '';
166
-
167
- if (typeof image === 'string') {
168
- const [{ readFile }, path] = await Promise.all([
169
- import('node:fs/promises'),
170
- import('node:path')
171
- ]);
172
- const bytes = await readFile(image);
173
- filename = filename || path.basename(image);
174
- blob = new Blob([bytes], { type: config.contentType || inferImageType(filename) });
175
- } else if (typeof Blob !== 'undefined' && image instanceof Blob) {
176
- blob = image;
177
- filename = filename || (typeof image.name === 'string' ? image.name : 'image.png');
178
- } else if (image && typeof image.arrayBuffer === 'function') {
179
- const bytes = await image.arrayBuffer();
180
- filename = filename || (typeof image.name === 'string' ? image.name : 'image.png');
181
- blob = new Blob([bytes], { type: config.contentType || image.type || inferImageType(filename) });
182
- } else if (image instanceof ArrayBuffer || ArrayBuffer.isView(image)) {
183
- filename = filename || 'image.png';
184
- blob = new Blob([image], { type: config.contentType || inferImageType(filename) });
185
- } else {
186
- throw new TypeError('image must be a file path, Blob, ArrayBuffer, or typed array.');
187
- }
188
-
189
- const contentType = config.contentType || blob.type || inferImageType(filename);
190
- const allowedTypes = config.allowedTypes || ['image/png', 'image/jpeg'];
191
- if (!allowedTypes.includes(contentType)) {
192
- throw new TypeError(config.typeError || 'OCR supports PNG and JPEG images only.');
193
- }
194
- if (blob.size > (config.maxBytes || MAX_OCR_BYTES)) {
195
- throw new RangeError('image must not exceed 50 MB.');
196
- }
197
- return { blob, filename, contentType };
198
- }
199
-
200
- class ApickApiError extends Error {
201
- constructor(message, options) {
202
- super(message);
203
- this.name = 'ApickApiError';
204
- this.status = options && options.status || 0;
205
- this.code = options && options.code || 'APICK_API_ERROR';
206
- this.serviceCode = options && options.serviceCode || undefined;
207
- }
208
-
209
- toJSON() {
210
- return {
211
- name: this.name,
212
- message: this.message,
213
- status: this.status,
214
- code: this.code,
215
- serviceCode: this.serviceCode
216
- };
217
- }
218
- }
219
-
220
- class ApickBinaryResult {
221
- constructor(bytes, options) {
222
- this.bytes = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
223
- this.contentType = options.contentType || 'application/octet-stream';
224
- this.filename = options.filename || 'result.bin';
225
- this.meta = options.meta;
226
- Object.freeze(this.meta);
227
- }
228
-
229
- get size() {
230
- return this.bytes.byteLength;
231
- }
232
-
233
- toArrayBuffer() {
234
- return this.bytes.buffer.slice(this.bytes.byteOffset, this.bytes.byteOffset + this.bytes.byteLength);
235
- }
236
-
237
- toBlob() {
238
- return new Blob([this.bytes], { type: this.contentType });
239
- }
240
-
241
- async save(filePath) {
242
- const target = requiredString('filePath', filePath);
243
- const { writeFile } = await import('node:fs/promises');
244
- await writeFile(target, this.bytes);
245
- return target;
246
- }
247
- }
248
-
249
- class ApickClient {
250
- #apiKey;
251
- #fetch;
252
- #baseUrl;
253
- #timeoutMs;
254
-
255
- constructor(apiKeyOrOptions) {
256
- const options = typeof apiKeyOrOptions === 'string'
257
- ? { apiKey: apiKeyOrOptions }
258
- : (apiKeyOrOptions || {});
259
-
260
- this.#apiKey = requiredString('apiKey', options.apiKey);
261
- this.#fetch = options.fetch || globalThis.fetch;
262
- if (typeof this.#fetch !== 'function') {
263
- throw new TypeError('A Fetch API implementation is required. Use Node.js 18+ or pass options.fetch.');
264
- }
265
-
266
- let baseUrl;
267
- try {
268
- baseUrl = new URL(options.baseUrl || DEFAULT_BASE_URL);
269
- } catch {
270
- throw new TypeError('baseUrl must be a valid HTTPS URL.');
271
- }
272
- if (baseUrl.protocol !== 'https:' || baseUrl.username || baseUrl.password) {
273
- throw new TypeError('baseUrl must be an HTTPS URL without embedded credentials.');
274
- }
275
- this.#baseUrl = baseUrl.toString().replace(/\/+$/, '');
276
- this.#timeoutMs = options.timeoutMs === undefined
277
- ? null
278
- : positiveInteger('timeoutMs', options.timeoutMs);
279
- }
280
-
281
- async _call(serviceName, payload, formData, requestOptions) {
282
- const baseDefinition = SERVICE_DEFINITIONS[serviceName];
283
- if (!baseDefinition) throw new TypeError(`Unknown APICK service: ${serviceName}`);
284
- const definition = Object.assign({}, baseDefinition, requestOptions || {});
285
-
286
- const controller = new AbortController();
287
- const timeoutMs = this.#timeoutMs || definition.timeoutMs || DEFAULT_TIMEOUT_MS;
288
- const timer = setTimeout(() => controller.abort(), timeoutMs);
289
- const headers = {
290
- Accept: definition.output === 'binary' ? '*/*' : 'application/json',
291
- CL_AUTH_KEY: this.#apiKey
292
- };
293
- const method = definition.method || 'POST';
294
- const request = {
295
- method,
296
- headers,
297
- signal: controller.signal,
298
- redirect: 'error'
299
- };
300
- if (method !== 'GET') {
301
- request.body = formData || JSON.stringify(payload || {});
302
- if (!formData) headers['Content-Type'] = 'application/json';
303
- }
304
-
305
- let response;
306
- try {
307
- response = await this.#fetch(this.#baseUrl + definition.endpoint, request);
308
- } catch (error) {
309
- const timedOut = controller.signal.aborted || error && error.name === 'AbortError';
310
- throw new ApickApiError(
311
- timedOut ? `APICK request timed out after ${timeoutMs} ms.` : redact(error && error.message || 'APICK network request failed.', this.#apiKey),
312
- { code: timedOut ? 'APICK_TIMEOUT' : 'APICK_NETWORK_ERROR' }
313
- );
314
- } finally {
315
- clearTimeout(timer);
316
- }
317
-
318
- const contentType = String(response.headers.get('content-type') || '').toLowerCase();
319
- const isJson = contentType.includes('application/json') || contentType.includes('+json');
320
-
321
- if (definition.output === 'binary' && !isJson && response.ok) {
322
- const bytes = new Uint8Array(await response.arrayBuffer());
323
- return new ApickBinaryResult(bytes, {
324
- contentType: contentType.split(';')[0] || 'application/octet-stream',
325
- filename: parseFilename(response.headers.get('content-disposition'), definition.filename),
326
- meta: responseMeta(null, response.headers)
327
- });
328
- }
329
-
330
- const raw = await response.text();
331
- let body;
332
- try {
333
- body = raw ? JSON.parse(raw) : null;
334
- } catch {
335
- throw new ApickApiError(`APICK returned an unexpected response (HTTP ${response.status}).`, {
336
- status: response.status,
337
- code: 'APICK_INVALID_RESPONSE'
338
- });
339
- }
340
-
341
- const failureMessage = publicErrorMessage(body, 'APICK request failed.');
342
- const bodyFailed = Boolean(
343
- body && body.data && body.data.error
344
- || body && body.result && body.result.error
345
- || body && body.api && body.api.success === false
346
- );
347
- if (!response.ok || bodyFailed || definition.output === 'binary') {
348
- throw new ApickApiError(failureMessage, {
349
- status: response.status,
350
- code: response.status === 401 ? 'APICK_AUTH_ERROR' : 'APICK_API_ERROR',
351
- serviceCode: body && body.data && typeof (body.data.code || body.data.error_code) === 'string' ? (body.data.code || body.data.error_code) : undefined
352
- });
353
- }
354
-
355
- return Object.freeze({
356
- data: body && Object.prototype.hasOwnProperty.call(body, 'data') ? body.data : body,
357
- meta: responseMeta(body && body.api, response.headers)
358
- });
359
- }
360
-
361
- businessDetails(businessNumber) {
362
- return this._call('businessDetails', { biz_no: normalizeBusinessNumber(businessNumber) });
363
- }
364
-
365
- ventureBusiness(businessNumber) {
366
- return this._call('ventureBusiness', { biz_no: normalizeBusinessNumber(businessNumber) });
367
- }
368
-
369
- trackParcel(carrier, trackingNumber) {
370
- return this._call('trackParcel', {
371
- carrier: requiredString('carrier', carrier),
372
- trackingNumber: requiredString('trackingNumber', trackingNumber)
373
- });
374
- }
375
-
376
- trackParcelAuto(trackingNumber) {
377
- return this._call('trackParcelAuto', { trackingNumber: requiredString('trackingNumber', trackingNumber) });
378
- }
379
-
380
- validateEmail(email) {
381
- return this._call('validateEmail', { email: requiredString('email', email) });
382
- }
383
-
384
- validatePhone(number) {
385
- return this._call('validatePhone', { number: requiredString('number', number) });
386
- }
387
-
388
- holidays(year, month) {
389
- const normalizedYear = Number(year);
390
- const normalizedMonth = Number(month);
391
- if (!Number.isInteger(normalizedYear) || normalizedYear < 1900 || normalizedYear > 2200) {
392
- throw new RangeError('year must be an integer from 1900 through 2200.');
393
- }
394
- if (!Number.isInteger(normalizedMonth) || normalizedMonth < 1 || normalizedMonth > 12) {
395
- throw new RangeError('month must be an integer from 1 through 12.');
396
- }
397
- return this._call('holidays', {
398
- year: String(normalizedYear),
399
- month: String(normalizedMonth).padStart(2, '0')
400
- });
401
- }
402
-
403
- searchAddress(query, options) {
404
- const config = options || {};
405
- return this._call('searchAddress', {
406
- juso: requiredString('query', query),
407
- page: String(positiveInteger('page', config.page, 1))
408
- });
409
- }
410
-
411
- async ocr(image, options) {
412
- const upload = await normalizeImage(image, options);
413
- const form = new FormData();
414
- form.append('image', upload.blob, upload.filename);
415
- return this._call('ocr', null, form);
416
- }
417
-
418
- async _maskImage(serviceName, image, options, type) {
419
- const upload = await normalizeImage(image, options);
420
- const form = new FormData();
421
- form.append('image', upload.blob, upload.filename);
422
- if (type !== undefined) form.append('type', String(type));
423
- return this._call(serviceName, null, form);
424
- }
425
-
426
- maskResidentNumber(image, options) {
427
- const config = options || {};
428
- const type = Number(config.type);
429
- if (![1, 2, 3].includes(type)) throw new RangeError('type must be one of: 1, 2, 3.');
430
- return this._maskImage('maskResidentNumber', image, config, type);
431
- }
432
-
433
- maskResidenceCard(image, options) { return this._maskImage('maskResidenceCard', image, options); }
434
- maskPassport(image, options) { return this._maskImage('maskPassport', image, options); }
435
- maskIdCard(image, options) { return this._maskImage('maskIdCard', image, options); }
436
- maskDriverLicense(image, options) { return this._maskImage('maskDriverLicense', image, options); }
437
-
438
- dnsLookup(domain) {
439
- return this._call('dnsLookup', { domain: requiredString('domain', domain) });
440
- }
441
-
442
- geolocate(address) {
443
- return this._call('geolocate', { address: requiredString('address', address) });
444
- }
445
-
446
- whois(address) {
447
- return this._call('whois', { address: requiredString('address', address) });
448
- }
449
-
450
- googleSearch(keyword, options) {
451
- const config = options || {};
452
- return this._call('googleSearch', {
453
- keyword: requiredString('keyword', keyword),
454
- page: String(positiveInteger('page', config.page, 1))
455
- });
456
- }
457
-
458
- googleImageSearch(keyword, options) {
459
- const config = options || {};
460
- return this._call('googleImageSearch', {
461
- keyword: requiredString('keyword', keyword),
462
- page: String(positiveInteger('page', config.page, 1))
463
- });
464
- }
465
-
466
- screenshot(url) {
467
- return this._call('screenshot', { url: normalizeUrl(url) });
468
- }
469
-
470
- createTtsJob(text, options) {
471
- const config = options || {};
472
- return this._call('createTtsJob', {
473
- voice_id: normalizeTtsVoice(config.voiceId || 'narrator_m_03'),
474
- text: requiredString('text', text, 800)
475
- });
476
- }
477
-
478
- getTtsJob(jobId) {
479
- const id = normalizeTtsJobId(jobId);
480
- return this._call('createTtsJob', null, null, { endpoint: '/rest/tts/jobs/' + id, method: 'GET' });
481
- }
482
-
483
- cancelTtsJob(jobId) {
484
- const id = normalizeTtsJobId(jobId);
485
- return this._call('createTtsJob', null, null, { endpoint: '/rest/tts/jobs/' + id + '/cancel' });
486
- }
487
-
488
- downloadTtsResult(jobId) {
489
- const id = normalizeTtsJobId(jobId);
490
- return this._call('createTtsJob', null, null, {
491
- endpoint: '/rest/tts/jobs/' + id + '/result',
492
- method: 'GET',
493
- output: 'binary',
494
- filename: id + '.mp3'
495
- });
496
- }
497
-
498
- downloadTtsSubtitles(jobId) {
499
- const id = normalizeTtsJobId(jobId);
500
- return this._call('createTtsJob', null, null, {
501
- endpoint: '/rest/tts/jobs/' + id + '/subtitles',
502
- method: 'GET',
503
- output: 'binary',
504
- filename: id + '.ass'
505
- });
506
- }
507
-
508
- htmlToPdf(html, options) {
509
- const config = options || {};
510
- return this._call('htmlToPdf', {
511
- html: requiredString('html', html),
512
- pagination: config.pagination ? 1 : 0
513
- });
514
- }
515
-
516
- jsonToExcel(data, options) {
517
- if (!Array.isArray(data)) throw new TypeError('data must be an array.');
518
- const config = options || {};
519
- const payload = { data_list: data };
520
- if (config.sheetName !== undefined) payload.sheet_name = requiredString('sheetName', config.sheetName);
521
- return this._call('jsonToExcel', payload);
522
- }
523
-
524
- summarize(text) {
525
- return this._call('summarize', { text: requiredString('text', text, 100_000) });
526
- }
527
-
528
- polish(text) {
529
- return this._call('polish', { text: requiredString('text', text, 100_000) });
530
- }
531
-
532
- _imageOptions(prompt, options, maxCount) {
533
- const config = options || {};
534
- for (const key of ['model', 'quality', 'count', 'n', 'outputCompression', 'input_fidelity', 'moderation', 'mask', 'maskFilename', 'maskContentType']) {
535
- if (Object.prototype.hasOwnProperty.call(config, key)) throw new TypeError(`${key} is not a supported image option.`);
536
- }
537
- const imageCount = positiveInteger('imageCount', config.imageCount, 1);
538
- if (imageCount > maxCount) throw new RangeError(`imageCount must not exceed ${maxCount}.`);
539
- const size = config.size || '1024x1024';
540
- if (!IMAGE_AI_SIZE_SET.has(size)) throw new TypeError(`size must be one of: ${IMAGE_AI_SIZES.join(', ')}.`);
541
- const payload = {
542
- prompt: requiredString('prompt', prompt, 28_000), image_count: imageCount,
543
- size, output_format: config.outputFormat || 'png',
544
- background: config.background || 'auto'
545
- };
546
- if (config.idempotencyKey !== undefined) {
547
- payload.idempotency_key = requiredString('idempotencyKey', config.idempotencyKey, 128);
548
- if (!/^[A-Za-z0-9_-]{8,128}$/.test(payload.idempotency_key)) throw new TypeError('idempotencyKey must use 8-128 letters, numbers, underscores, or hyphens.');
549
- }
550
- return payload;
551
- }
552
-
553
- async generateImages(prompt, options) {
554
- const config = options || {}, payload = this._imageOptions(prompt, config, 4);
555
- if (config.referenceImage === undefined) return this._call('generateImages', payload);
556
- const uploadOptions = {
557
- filename: config.referenceFilename, contentType: config.referenceContentType,
558
- allowedTypes:['image/png','image/jpeg','image/webp'], maxBytes:MAX_IMAGE_AI_BYTES,
559
- typeError:'referenceImage must be PNG, JPEG, or WebP.'
560
- };
561
- const source = await normalizeImage(config.referenceImage, uploadOptions), form = new FormData();
562
- Object.entries(payload).forEach(([key,value]) => form.append(key, String(value)));
563
- form.append('reference_image', source.blob, source.filename);
564
- return this._call('generateImages', null, form);
565
- }
566
-
567
- async editImages(image, prompt, options) {
568
- const config = options || {}, payload = this._imageOptions(prompt, config, 4);
569
- const uploadOptions = Object.assign({}, config, { allowedTypes:['image/png','image/jpeg','image/webp'], maxBytes:MAX_IMAGE_AI_BYTES, typeError:'image must be PNG, JPEG, or WebP.' });
570
- const source = await normalizeImage(image, uploadOptions), form = new FormData();
571
- Object.entries(payload).forEach(([key,value]) => form.append(key, String(value)));
572
- form.append('image', source.blob, source.filename);
573
- return this._call('generateImages', null, form, { endpoint:'/rest/image-generation/edit' });
574
- }
575
-
576
- async createImageGenerationJob(prompt, options) {
577
- const config = options || {}, payload = this._imageOptions(prompt, config, 50);
578
- if (config.referenceImage === undefined) return this._call('generateImages', payload, null, { endpoint:'/rest/image-generation/jobs/generate', timeoutMs:60_000 });
579
- const uploadOptions = {
580
- filename: config.referenceFilename, contentType: config.referenceContentType,
581
- allowedTypes:['image/png','image/jpeg','image/webp'], maxBytes:MAX_IMAGE_AI_BYTES,
582
- typeError:'referenceImage must be PNG, JPEG, or WebP.'
583
- };
584
- const source = await normalizeImage(config.referenceImage, uploadOptions), form = new FormData();
585
- Object.entries(payload).forEach(([key,value]) => form.append(key, String(value)));
586
- form.append('reference_image', source.blob, source.filename);
587
- return this._call('generateImages', null, form, { endpoint:'/rest/image-generation/jobs/generate', timeoutMs:60_000 });
588
- }
589
-
590
- async createImageEditJob(image, prompt, options) {
591
- const config = options || {}, payload = this._imageOptions(prompt, config, 50);
592
- const uploadOptions = Object.assign({}, config, { allowedTypes:['image/png','image/jpeg','image/webp'], maxBytes:MAX_IMAGE_AI_BYTES, typeError:'image must be PNG, JPEG, or WebP.' });
593
- const source = await normalizeImage(image, uploadOptions), form = new FormData();
594
- Object.entries(payload).forEach(([key,value]) => form.append(key, String(value)));
595
- form.append('image', source.blob, source.filename);
596
- return this._call('generateImages', null, form, { endpoint:'/rest/image-generation/jobs/edit', timeoutMs:60_000 });
597
- }
598
-
599
- getImageJob(jobId) {
600
- const id = normalizeTtsJobId(jobId);
601
- return this._call('generateImages', null, null, { endpoint:'/rest/image-generation/jobs/'+id, method:'GET', timeoutMs:30_000 });
602
- }
603
-
604
- downloadImageJobImage(jobId, index) {
605
- const id=normalizeTtsJobId(jobId), value=Number(index);
606
- if(!Number.isInteger(value)||value<0||value>49) throw new RangeError('index must be an integer from 0 through 49.');
607
- return this._call('generateImages', null, null, { endpoint:'/rest/image-generation/jobs/'+id+'/images/'+value, method:'GET', output:'binary', filename:id+'-'+value+'.bin', timeoutMs:60_000 });
608
- }
609
-
610
- downloadImageJobArchive(jobId) {
611
- const id=normalizeTtsJobId(jobId);
612
- return this._call('generateImages', null, null, { endpoint:'/rest/image-generation/jobs/'+id+'/result', method:'GET', output:'binary', filename:id+'.zip', timeoutMs:60_000 });
613
- }
614
- }
615
-
616
- module.exports = {
617
- ApickClient,
618
- ApickApiError,
619
- ApickBinaryResult,
620
- SERVICES,
621
- TTS_VOICE_IDS,
622
- DEFAULT_BASE_URL
623
- };
1
+ 'use strict';
2
+
3
+ const DEFAULT_BASE_URL = 'https://apick.app';
4
+ const DEFAULT_TIMEOUT_MS = 30_000;
5
+ const MAX_OCR_BYTES = 50 * 1024 * 1024;
6
+ const MAX_IMAGE_AI_BYTES = 50 * 1024 * 1024;
7
+ const IMAGE_AI_SIZES = Object.freeze(['1024x1024', '1536x1024', '1024x1536', '1152x864', '864x1152']);
8
+ const IMAGE_AI_SIZE_SET = new Set(IMAGE_AI_SIZES);
9
+ const TTS_VOICE_IDS = Object.freeze([
10
+ 'v2_ann_m_30s_01', 'v2_ann_m_30s_02', 'v2_ann_m_30s_04', 'v2_ann_m_30s_05', 'v2_ann_f_30s_01', 'v2_ann_f_30s_02', 'v2_ann_f_30s_03', 'v2_ann_f_30s_04', 'v2_ann_f_30s_05', 'v2_m_teen_01', 'v2_m_young_01', 'v2_m_mid_01', 'v2_m_senior_01', 'v2_f_teen_01', 'v2_f_young_01', 'v2_f_senior_01'
11
+ ]);
12
+ const TTS_VOICE_ID_SET = new Set(TTS_VOICE_IDS);
13
+ const AUTH_PROVIDERS = Object.freeze([
14
+ 'kakao', 'naver', 'toss', 'pass', 'samsung', 'kb', 'shinhan', 'hana', 'woori', 'ibk', 'nh', 'kakaobank', 'banksalad'
15
+ ]);
16
+ const AUTH_PROVIDER_SET = new Set(AUTH_PROVIDERS);
17
+
18
+ const SERVICE_DEFINITIONS = Object.freeze({
19
+ businessDetails: { endpoint: '/rest/biz_detail', timeoutMs: 50_000, output: 'json' },
20
+ ventureBusiness: { endpoint: '/rest/venture_biz_info', timeoutMs: 50_000, output: 'json' },
21
+ trackParcel: { endpoint: '/rest/parcel_tracking', timeoutMs: 30_000, output: 'json' },
22
+ trackParcelAuto: { endpoint: '/rest/parcel_tracking_auto', timeoutMs: 30_000, output: 'json' },
23
+ validateEmail: { endpoint: '/rest/check_email_valid', timeoutMs: 20_000, output: 'json' },
24
+ validatePhone: { endpoint: '/rest/check_phone_valid', timeoutMs: 20_000, output: 'json' },
25
+ holidays: { endpoint: '/rest/holiday_info', timeoutMs: 35_000, output: 'json' },
26
+ searchAddress: { endpoint: '/rest/search_juso', timeoutMs: 20_000, output: 'json' },
27
+ ocr: { endpoint: '/rest/ocr', timeoutMs: 35_000, output: 'json' },
28
+ maskResidentNumber: { endpoint: '/rest/hide_rrn', timeoutMs: 35_000, output: 'binary', filename: 'masked.png' },
29
+ maskResidenceCard: { endpoint: '/rest/identity_document_residence_card', timeoutMs: 35_000, output: 'json' },
30
+ maskPassport: { endpoint: '/rest/identity_document_passport', timeoutMs: 35_000, output: 'json' },
31
+ maskIdCard: { endpoint: '/rest/identity_document_id_card', timeoutMs: 35_000, output: 'json' },
32
+ maskDriverLicense: { endpoint: '/rest/identity_document_driver_license', timeoutMs: 35_000, output: 'json' },
33
+ dnsLookup: { endpoint: '/rest/nslookup', timeoutMs: 16_000, output: 'json' },
34
+ geolocate: { endpoint: '/rest/location', timeoutMs: 35_000, output: 'json' },
35
+ whois: { endpoint: '/rest/whois', timeoutMs: 35_000, output: 'json' },
36
+ googleSearch: { endpoint: '/rest/google_search', timeoutMs: 35_000, output: 'json' },
37
+ googleImageSearch: { endpoint: '/rest/google_image_search', timeoutMs: 35_000, output: 'json' },
38
+ screenshot: { endpoint: '/rest/url_screenshot', timeoutMs: 75_000, output: 'binary', filename: 'screenshot.jpeg' },
39
+ createTtsJob: { endpoint: '/rest/tts/jobs', timeoutMs: 35_000, output: 'json' },
40
+ createVideoJob: { endpoint: '/rest/seedance/jobs', timeoutMs: 60_000, output: 'json' },
41
+ htmlToPdf: { endpoint: '/rest/html_to_pdf', timeoutMs: 25_000, output: 'binary', filename: 'document.pdf' },
42
+ jsonToExcel: { endpoint: '/rest/json_to_excel', timeoutMs: 45_000, output: 'binary', filename: 'data.xlsx' },
43
+ summarize: { endpoint: '/rest/llm/text_summary', timeoutMs: 75_000, output: 'json' },
44
+ polish: { endpoint: '/rest/llm/text_polish', timeoutMs: 105_000, output: 'json' },
45
+ generateImages: { endpoint: '/rest/image-generation/generate', timeoutMs: 190_000, output: 'json' },
46
+ requestEmployment: { endpoint: '/rest/req_employment', timeoutMs: 35_000, output: 'json' },
47
+ requestPersonalIncome: { endpoint: '/rest/req_personal_income', timeoutMs: 35_000, output: 'json' },
48
+ requestNpsJoinHistory: { endpoint: '/rest/req_nps_join_history', timeoutMs: 35_000, output: 'json' },
49
+ requestDrivingLicense: { endpoint: '/rest/req_driving_license', timeoutMs: 35_000, output: 'json' },
50
+ requestHealthCheckup: { endpoint: '/rest/req_health_checkup', timeoutMs: 35_000, output: 'json' }
51
+ });
52
+
53
+ const SERVICES = Object.freeze(Object.fromEntries(
54
+ Object.entries(SERVICE_DEFINITIONS).map(([name, definition]) => [
55
+ name,
56
+ Object.freeze({ endpoint: definition.endpoint, output: definition.output })
57
+ ])
58
+ ));
59
+
60
+ function redact(value, apiKey) {
61
+ let text = String(value || '');
62
+ if (apiKey) text = text.split(apiKey).join('***');
63
+ return text.replace(/(Authorization\s*:\s*Bearer\s*)\S+/gi, '$1***').replace(/(CL_AUTH_KEY\s*[:=]\s*)\S+/gi, '$1***');
64
+ }
65
+
66
+ function requiredString(name, value, maxLength) {
67
+ if (typeof value !== 'string' || !value.trim()) {
68
+ throw new TypeError(`${name} must be a non-empty string.`);
69
+ }
70
+ const normalized = value.trim();
71
+ if (maxLength && normalized.length > maxLength) {
72
+ throw new RangeError(`${name} must not exceed ${maxLength} characters.`);
73
+ }
74
+ return normalized;
75
+ }
76
+
77
+ function positiveInteger(name, value, defaultValue) {
78
+ if (value === undefined || value === null || value === '') return defaultValue;
79
+ const number = Number(value);
80
+ if (!Number.isInteger(number) || number < 1) {
81
+ throw new RangeError(`${name} must be a positive integer.`);
82
+ }
83
+ return number;
84
+ }
85
+
86
+ function normalizeBusinessNumber(value) {
87
+ const normalized = requiredString('businessNumber', value).replace(/-/g, '');
88
+ if (!/^\d{10}$/.test(normalized)) {
89
+ throw new TypeError('businessNumber must contain exactly 10 digits.');
90
+ }
91
+ return normalized;
92
+ }
93
+
94
+ function normalizeUrl(value) {
95
+ const input = requiredString('url', value);
96
+ let parsed;
97
+ try {
98
+ parsed = new URL(input);
99
+ } catch {
100
+ throw new TypeError('url must be a valid HTTP or HTTPS URL.');
101
+ }
102
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
103
+ throw new TypeError('url must use HTTP or HTTPS.');
104
+ }
105
+ return parsed.toString();
106
+ }
107
+
108
+ function normalizeTtsJobId(value) {
109
+ const jobId = requiredString('jobId', value);
110
+ if (!/^[a-f0-9]{32}$/.test(jobId)) throw new TypeError('jobId must be a 32-character lowercase hexadecimal string.');
111
+ return jobId;
112
+ }
113
+
114
+ function normalizeTtsVoice(value) {
115
+ const voiceId = requiredString('voiceId', value);
116
+ if (!TTS_VOICE_ID_SET.has(voiceId)) throw new RangeError('voiceId must be one of the supported TTS voice IDs.');
117
+ return voiceId;
118
+ }
119
+
120
+ function normalizeAuthProvider(value) {
121
+ const provider = requiredString('authProvider', value).toLowerCase();
122
+ if (!AUTH_PROVIDER_SET.has(provider)) throw new RangeError('authProvider must be one of the supported simple-auth providers.');
123
+ return provider;
124
+ }
125
+
126
+ function normalizeBirthDate(value) {
127
+ const birthDate = requiredString('birthDate', value);
128
+ if (!/^(18|19|20)\d{6}$/.test(birthDate)) throw new TypeError('birthDate must be 8 digits (YYYYMMDD).');
129
+ return birthDate;
130
+ }
131
+
132
+ function normalizeKoreanMobile(value) {
133
+ const phone = requiredString('phone', value).replace(/[^0-9]/g, '');
134
+ if (!/^01[0-9]{8,9}$/.test(phone)) throw new TypeError('phone must be a Korean mobile number (digits only).');
135
+ return phone;
136
+ }
137
+
138
+ function normalizeTransactionId(value) {
139
+ const transactionId = requiredString('transactionId', value);
140
+ if (!/^[a-f0-9]{32}$/.test(transactionId)) throw new TypeError('transactionId must be a 32-character lowercase hexadecimal string.');
141
+ return transactionId;
142
+ }
143
+
144
+ function normalizeYearMonth(name, value) {
145
+ if (value === undefined || value === null || value === '') return undefined;
146
+ const text = requiredString(name, value);
147
+ if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(text)) throw new TypeError(`${name} must be in YYYY-MM format.`);
148
+ return text;
149
+ }
150
+
151
+ function optionalRangeInteger(name, value, min, max) {
152
+ if (value === undefined || value === null || value === '') return undefined;
153
+ const number = Number(value);
154
+ if (!Number.isInteger(number) || number < min || number > max) {
155
+ throw new RangeError(`${name} must be an integer between ${min} and ${max}.`);
156
+ }
157
+ return number;
158
+ }
159
+
160
+ // 재직·소득·연금·면허·건강검진 등 간편인증 기반 조회 상품의 공통 입력.
161
+ // 서버는 최상위 평면 필드명을 그대로 받으므로 여기서 만든 객체가 폼 필드 이름이 된다.
162
+ function dataRequestInput(input) {
163
+ const config = input || {};
164
+ return {
165
+ name: requiredString('name', config.name),
166
+ birthDate: normalizeBirthDate(config.birthDate),
167
+ phone: normalizeKoreanMobile(config.phone),
168
+ authProvider: normalizeAuthProvider(config.authProvider)
169
+ };
170
+ }
171
+
172
+ function numberOrNull(value) {
173
+ if (value === undefined || value === null || value === '') return null;
174
+ const number = Number(value);
175
+ return Number.isFinite(number) ? number : null;
176
+ }
177
+
178
+ function responseMeta(api, headers) {
179
+ const metadata = api && typeof api === 'object' ? api : {};
180
+ const readHeader = (name) => headers && typeof headers.get === 'function' ? headers.get(name) : null;
181
+ return Object.freeze({
182
+ cost: numberOrNull(metadata.cost ?? readHeader('cost')),
183
+ durationMs: numberOrNull(metadata.ms ?? readHeader('ms'))
184
+ });
185
+ }
186
+
187
+ function publicErrorMessage(body, fallback) {
188
+ if (!body || typeof body !== 'object') return fallback;
189
+ const candidates = [
190
+ body.data && body.data.error,
191
+ body.result && body.result.error,
192
+ body.error,
193
+ body.message,
194
+ body.msg
195
+ ];
196
+ for (const candidate of candidates) {
197
+ if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();
198
+ }
199
+ return fallback;
200
+ }
201
+
202
+ function parseFilename(headerValue, fallback) {
203
+ if (typeof headerValue !== 'string') return fallback;
204
+ const encoded = /filename\*=UTF-8''([^;]+)/i.exec(headerValue);
205
+ if (encoded) {
206
+ try { return decodeURIComponent(encoded[1]).replace(/[\\/]/g, '_'); } catch { /* use fallback parser */ }
207
+ }
208
+ const plain = /filename="?([^";]+)"?/i.exec(headerValue);
209
+ return plain ? plain[1].trim().replace(/[\\/]/g, '_') : fallback;
210
+ }
211
+
212
+ function inferImageType(filename) {
213
+ const lower = String(filename || '').toLowerCase();
214
+ if (lower.endsWith('.png')) return 'image/png';
215
+ if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
216
+ if (lower.endsWith('.webp')) return 'image/webp';
217
+ return '';
218
+ }
219
+
220
+ async function normalizeImage(image, options) {
221
+ const config = options || {};
222
+ let blob;
223
+ let filename = config.filename || '';
224
+
225
+ if (typeof image === 'string') {
226
+ const [{ readFile }, path] = await Promise.all([
227
+ import('node:fs/promises'),
228
+ import('node:path')
229
+ ]);
230
+ const bytes = await readFile(image);
231
+ filename = filename || path.basename(image);
232
+ blob = new Blob([bytes], { type: config.contentType || inferImageType(filename) });
233
+ } else if (typeof Blob !== 'undefined' && image instanceof Blob) {
234
+ blob = image;
235
+ filename = filename || (typeof image.name === 'string' ? image.name : 'image.png');
236
+ } else if (image && typeof image.arrayBuffer === 'function') {
237
+ const bytes = await image.arrayBuffer();
238
+ filename = filename || (typeof image.name === 'string' ? image.name : 'image.png');
239
+ blob = new Blob([bytes], { type: config.contentType || image.type || inferImageType(filename) });
240
+ } else if (image instanceof ArrayBuffer || ArrayBuffer.isView(image)) {
241
+ filename = filename || 'image.png';
242
+ blob = new Blob([image], { type: config.contentType || inferImageType(filename) });
243
+ } else {
244
+ throw new TypeError('image must be a file path, Blob, ArrayBuffer, or typed array.');
245
+ }
246
+
247
+ const contentType = config.contentType || blob.type || inferImageType(filename);
248
+ const allowedTypes = config.allowedTypes || ['image/png', 'image/jpeg'];
249
+ if (!allowedTypes.includes(contentType)) {
250
+ throw new TypeError(config.typeError || 'OCR supports PNG and JPEG images only.');
251
+ }
252
+ if (blob.size > (config.maxBytes || MAX_OCR_BYTES)) {
253
+ throw new RangeError('image must not exceed 50 MB.');
254
+ }
255
+ return { blob, filename, contentType };
256
+ }
257
+
258
+ class ApickApiError extends Error {
259
+ constructor(message, options) {
260
+ super(message);
261
+ this.name = 'ApickApiError';
262
+ this.status = options && options.status || 0;
263
+ this.code = options && options.code || 'APICK_API_ERROR';
264
+ this.serviceCode = options && options.serviceCode || undefined;
265
+ }
266
+
267
+ toJSON() {
268
+ return {
269
+ name: this.name,
270
+ message: this.message,
271
+ status: this.status,
272
+ code: this.code,
273
+ serviceCode: this.serviceCode
274
+ };
275
+ }
276
+ }
277
+
278
+ class ApickBinaryResult {
279
+ constructor(bytes, options) {
280
+ this.bytes = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
281
+ this.contentType = options.contentType || 'application/octet-stream';
282
+ this.filename = options.filename || 'result.bin';
283
+ this.meta = options.meta;
284
+ Object.freeze(this.meta);
285
+ }
286
+
287
+ get size() {
288
+ return this.bytes.byteLength;
289
+ }
290
+
291
+ toArrayBuffer() {
292
+ return this.bytes.buffer.slice(this.bytes.byteOffset, this.bytes.byteOffset + this.bytes.byteLength);
293
+ }
294
+
295
+ toBlob() {
296
+ return new Blob([this.bytes], { type: this.contentType });
297
+ }
298
+
299
+ async save(filePath) {
300
+ const target = requiredString('filePath', filePath);
301
+ const { writeFile } = await import('node:fs/promises');
302
+ await writeFile(target, this.bytes);
303
+ return target;
304
+ }
305
+ }
306
+
307
+ class ApickClient {
308
+ #apiKey;
309
+ #fetch;
310
+ #baseUrl;
311
+ #timeoutMs;
312
+
313
+ constructor(apiKeyOrOptions) {
314
+ const options = typeof apiKeyOrOptions === 'string'
315
+ ? { apiKey: apiKeyOrOptions }
316
+ : (apiKeyOrOptions || {});
317
+
318
+ this.#apiKey = requiredString('apiKey', options.apiKey);
319
+ this.#fetch = options.fetch || globalThis.fetch;
320
+ if (typeof this.#fetch !== 'function') {
321
+ throw new TypeError('A Fetch API implementation is required. Use Node.js 18+ or pass options.fetch.');
322
+ }
323
+
324
+ let baseUrl;
325
+ try {
326
+ baseUrl = new URL(options.baseUrl || DEFAULT_BASE_URL);
327
+ } catch {
328
+ throw new TypeError('baseUrl must be a valid HTTPS URL.');
329
+ }
330
+ if (baseUrl.protocol !== 'https:' || baseUrl.username || baseUrl.password) {
331
+ throw new TypeError('baseUrl must be an HTTPS URL without embedded credentials.');
332
+ }
333
+ this.#baseUrl = baseUrl.toString().replace(/\/+$/, '');
334
+ this.#timeoutMs = options.timeoutMs === undefined
335
+ ? null
336
+ : positiveInteger('timeoutMs', options.timeoutMs);
337
+ }
338
+
339
+ async _call(serviceName, payload, formData, requestOptions) {
340
+ const baseDefinition = SERVICE_DEFINITIONS[serviceName];
341
+ if (!baseDefinition) throw new TypeError(`Unknown APICK service: ${serviceName}`);
342
+ const definition = Object.assign({}, baseDefinition, requestOptions || {});
343
+
344
+ const controller = new AbortController();
345
+ const timeoutMs = this.#timeoutMs || definition.timeoutMs || DEFAULT_TIMEOUT_MS;
346
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
347
+ const headers = {
348
+ Accept: definition.output === 'binary' ? '*/*' : 'application/json',
349
+ Authorization: `Bearer ${this.#apiKey}`
350
+ };
351
+ const method = definition.method || 'POST';
352
+ const request = {
353
+ method,
354
+ headers,
355
+ signal: controller.signal,
356
+ redirect: 'error'
357
+ };
358
+ if (method !== 'GET') {
359
+ request.body = formData || require('./form.cjs').createForm(payload || {}, definition.endpoint);
360
+ }
361
+
362
+ let response;
363
+ try {
364
+ response = await this.#fetch(this.#baseUrl + definition.endpoint, request);
365
+ } catch (error) {
366
+ const timedOut = controller.signal.aborted || error && error.name === 'AbortError';
367
+ throw new ApickApiError(
368
+ timedOut ? `APICK request timed out after ${timeoutMs} ms.` : redact(error && error.message || 'APICK network request failed.', this.#apiKey),
369
+ { code: timedOut ? 'APICK_TIMEOUT' : 'APICK_NETWORK_ERROR' }
370
+ );
371
+ } finally {
372
+ clearTimeout(timer);
373
+ }
374
+
375
+ const contentType = String(response.headers.get('content-type') || '').toLowerCase();
376
+ const isJson = contentType.includes('application/json') || contentType.includes('+json');
377
+
378
+ if (definition.output === 'binary' && !isJson && response.ok) {
379
+ const bytes = new Uint8Array(await response.arrayBuffer());
380
+ return new ApickBinaryResult(bytes, {
381
+ contentType: contentType.split(';')[0] || 'application/octet-stream',
382
+ filename: parseFilename(response.headers.get('content-disposition'), definition.filename),
383
+ meta: responseMeta(null, response.headers)
384
+ });
385
+ }
386
+
387
+ const raw = await response.text();
388
+ let body;
389
+ try {
390
+ body = raw ? JSON.parse(raw) : null;
391
+ } catch {
392
+ throw new ApickApiError(`APICK returned an unexpected response (HTTP ${response.status}).`, {
393
+ status: response.status,
394
+ code: 'APICK_INVALID_RESPONSE'
395
+ });
396
+ }
397
+
398
+ const failureMessage = publicErrorMessage(body, 'APICK request failed.');
399
+ const bodyFailed = Boolean(
400
+ body && body.data && body.data.error
401
+ || body && body.result && body.result.error
402
+ || body && body.api && body.api.success === false
403
+ );
404
+ if (!response.ok || bodyFailed || definition.output === 'binary') {
405
+ throw new ApickApiError(failureMessage, {
406
+ status: response.status,
407
+ code: response.status === 401 ? 'APICK_AUTH_ERROR' : 'APICK_API_ERROR',
408
+ serviceCode: body && body.data && typeof (body.data.code || body.data.error_code) === 'string' ? (body.data.code || body.data.error_code) : undefined
409
+ });
410
+ }
411
+
412
+ return Object.freeze({
413
+ data: body && Object.prototype.hasOwnProperty.call(body, 'data') ? body.data : body,
414
+ meta: responseMeta(body && body.api, response.headers)
415
+ });
416
+ }
417
+
418
+ businessDetails(businessNumber) {
419
+ return this._call('businessDetails', { biz_no: normalizeBusinessNumber(businessNumber) });
420
+ }
421
+
422
+ ventureBusiness(businessNumber) {
423
+ return this._call('ventureBusiness', { biz_no: normalizeBusinessNumber(businessNumber) });
424
+ }
425
+
426
+ trackParcel(carrier, trackingNumber) {
427
+ return this._call('trackParcel', {
428
+ carrier: requiredString('carrier', carrier),
429
+ trackingNumber: requiredString('trackingNumber', trackingNumber)
430
+ });
431
+ }
432
+
433
+ trackParcelAuto(trackingNumber) {
434
+ return this._call('trackParcelAuto', { trackingNumber: requiredString('trackingNumber', trackingNumber) });
435
+ }
436
+
437
+ validateEmail(email) {
438
+ return this._call('validateEmail', { email: requiredString('email', email) });
439
+ }
440
+
441
+ validatePhone(number) {
442
+ return this._call('validatePhone', { number: requiredString('number', number) });
443
+ }
444
+
445
+ holidays(year, month) {
446
+ const normalizedYear = Number(year);
447
+ const normalizedMonth = Number(month);
448
+ if (!Number.isInteger(normalizedYear) || normalizedYear < 1900 || normalizedYear > 2200) {
449
+ throw new RangeError('year must be an integer from 1900 through 2200.');
450
+ }
451
+ if (!Number.isInteger(normalizedMonth) || normalizedMonth < 1 || normalizedMonth > 12) {
452
+ throw new RangeError('month must be an integer from 1 through 12.');
453
+ }
454
+ return this._call('holidays', {
455
+ year: String(normalizedYear),
456
+ month: String(normalizedMonth).padStart(2, '0')
457
+ });
458
+ }
459
+
460
+ searchAddress(query, options) {
461
+ const config = options || {};
462
+ return this._call('searchAddress', {
463
+ juso: requiredString('query', query),
464
+ page: String(positiveInteger('page', config.page, 1))
465
+ });
466
+ }
467
+
468
+ async ocr(image, options) {
469
+ const upload = await normalizeImage(image, options);
470
+ const form = new FormData();
471
+ form.append('image', upload.blob, upload.filename);
472
+ return this._call('ocr', null, form);
473
+ }
474
+
475
+ async _maskImage(serviceName, image, options, type) {
476
+ const upload = await normalizeImage(image, options);
477
+ const form = new FormData();
478
+ form.append('image', upload.blob, upload.filename);
479
+ if (type !== undefined) form.append('type', String(type));
480
+ return this._call(serviceName, null, form);
481
+ }
482
+
483
+ maskResidentNumber(image, options) {
484
+ const config = options || {};
485
+ const type = Number(config.type);
486
+ if (![1, 2, 3, 4].includes(type)) throw new RangeError('type must be one of: 1, 2, 3, 4.');
487
+ return this._maskImage('maskResidentNumber', image, config, type);
488
+ }
489
+
490
+ maskResidenceCard(image, options) { return this._maskImage('maskResidenceCard', image, options); }
491
+ maskPassport(image, options) { return this._maskImage('maskPassport', image, options); }
492
+ maskIdCard(image, options) { return this._maskImage('maskIdCard', image, options); }
493
+ maskDriverLicense(image, options) { return this._maskImage('maskDriverLicense', image, options); }
494
+
495
+ dnsLookup(domain) {
496
+ return this._call('dnsLookup', { domain: requiredString('domain', domain) });
497
+ }
498
+
499
+ geolocate(address) {
500
+ return this._call('geolocate', { address: requiredString('address', address) });
501
+ }
502
+
503
+ whois(address) {
504
+ return this._call('whois', { address: requiredString('address', address) });
505
+ }
506
+
507
+ googleSearch(keyword, options) {
508
+ const config = options || {};
509
+ return this._call('googleSearch', {
510
+ keyword: requiredString('keyword', keyword),
511
+ page: String(positiveInteger('page', config.page, 1))
512
+ });
513
+ }
514
+
515
+ googleImageSearch(keyword, options) {
516
+ const config = options || {};
517
+ return this._call('googleImageSearch', {
518
+ keyword: requiredString('keyword', keyword),
519
+ page: String(positiveInteger('page', config.page, 1))
520
+ });
521
+ }
522
+
523
+ screenshot(url) {
524
+ return this._call('screenshot', { url: normalizeUrl(url) });
525
+ }
526
+
527
+ async createVideoJob(model, prompt, options) {
528
+ if (!['seedance', 'veo', 'kling'].includes(model)) throw new RangeError('model must be seedance, veo or kling.');
529
+ const config = options || {};
530
+ const payload = { prompt: config.mode === 'reference' && prompt === '' ? '' : requiredString('prompt', prompt, 2000) };
531
+ for (const [key, field] of Object.entries({ version:'version', tier:'tier', mode:'mode', duration:'duration', aspectRatio:'aspect_ratio', resolution:'resolution', audio:'audio', negativePrompt:'negative_prompt', seed:'seed', cfgScale:'cfg_scale', idempotencyKey:'idempotency_key' })) {
532
+ if (config[key] !== undefined) payload[field] = config[key];
533
+ }
534
+ const files = [['image', config.image], ['last_image', config.lastImage]];
535
+ for (const input of config.referenceImages || []) files.push(['reference_image', input]);
536
+ for (const input of config.referenceVideos || []) files.push(['reference_video', input]);
537
+ for (const input of config.referenceAudios || []) files.push(['reference_audio', input]);
538
+ const present = files.filter(([, input]) => input !== undefined);
539
+ const request = { endpoint: '/rest/' + model + '/jobs' };
540
+ if (!present.length) return this._call('createVideoJob', payload, null, request);
541
+ const form = new FormData();
542
+ for (const [key, value] of Object.entries(payload)) form.append(key, String(value));
543
+ for (const [field, input] of present) {
544
+ if (field !== 'reference_video' && field !== 'reference_audio') {
545
+ const upload = await normalizeImage(input);
546
+ form.append(field, upload.blob, upload.filename);
547
+ } else {
548
+ let bytes, filename, contentType;
549
+ if (typeof input === 'string') {
550
+ bytes = await require('node:fs/promises').readFile(input);
551
+ filename = require('node:path').basename(input);
552
+ contentType = field === 'reference_audio' ? (/\.wav$/i.test(filename) ? 'audio/wav' : 'audio/mpeg') : /\.webm$/i.test(filename) ? 'video/webm' : /\.mov$/i.test(filename) ? 'video/quicktime' : 'video/mp4';
553
+ } else { bytes = await input.arrayBuffer(); filename = input.name || (field === 'reference_audio' ? 'reference.mp3' : 'reference.mp4'); contentType = input.type || (field === 'reference_audio' ? 'audio/mpeg' : 'video/mp4'); }
554
+ const allowed = field === 'reference_audio' ? ['audio/mpeg', 'audio/wav', 'audio/x-wav'] : ['video/mp4', 'video/quicktime', 'video/webm'];
555
+ const maxBytes = field === 'reference_audio' ? 15 * 1024 * 1024 : 100 * 1024 * 1024;
556
+ if (!allowed.includes(contentType) || bytes.byteLength < 1 || bytes.byteLength > maxBytes) throw new RangeError(field === 'reference_audio' ? 'Invalid reference audio.' : 'Invalid reference video.');
557
+ form.append(field, new Blob([bytes], { type: contentType }), filename);
558
+ }
559
+ }
560
+ return this._call('createVideoJob', null, form, request);
561
+ }
562
+
563
+ getVideoJob(model, jobId) {
564
+ if (!['seedance', 'veo', 'kling'].includes(model) || !/^[a-f0-9]{32}$/.test(jobId)) throw new RangeError('Invalid video model or job ID.');
565
+ return this._call('createVideoJob', null, null, { endpoint:'/rest/'+model+'/jobs/'+jobId, method:'GET', timeoutMs:30_000 });
566
+ }
567
+
568
+ downloadVideoResult(model, jobId) {
569
+ if (!['seedance', 'veo', 'kling'].includes(model) || !/^[a-f0-9]{32}$/.test(jobId)) throw new RangeError('Invalid video model or job ID.');
570
+ return this._call('createVideoJob', null, null, { endpoint:'/rest/'+model+'/jobs/'+jobId+'/result', method:'GET', output:'binary', filename:jobId+'.mp4', timeoutMs:60_000 });
571
+ }
572
+
573
+ createTtsJob(text, options) {
574
+ const config = options || {};
575
+ return this._call('createTtsJob', {
576
+ voice_id: normalizeTtsVoice(config.voiceId || 'v2_ann_m_30s_01'),
577
+ text: requiredString('text', text, 800)
578
+ });
579
+ }
580
+
581
+ getTtsJob(jobId) {
582
+ const id = normalizeTtsJobId(jobId);
583
+ return this._call('createTtsJob', null, null, { endpoint: '/rest/tts/jobs/' + id, method: 'GET' });
584
+ }
585
+
586
+ cancelTtsJob(jobId) {
587
+ const id = normalizeTtsJobId(jobId);
588
+ return this._call('createTtsJob', null, null, { endpoint: '/rest/tts/jobs/' + id + '/cancel' });
589
+ }
590
+
591
+ getTtsQuality(jobId) {
592
+ const id = normalizeTtsJobId(jobId);
593
+ return this._call('createTtsJob', null, null, { endpoint: '/rest/tts/jobs/' + id + '/quality', method: 'GET' });
594
+ }
595
+
596
+ retryTtsJob(jobId, utteranceIds, idempotencyKey) {
597
+ const id = normalizeTtsJobId(jobId);
598
+ if (!Array.isArray(utteranceIds) || utteranceIds.length > 100 || utteranceIds.some(value => typeof value !== 'string' || !/^u\d{3}$/.test(value))) {
599
+ throw new TypeError('utteranceIds must contain up to 100 uNNN identifiers.');
600
+ }
601
+ if (typeof idempotencyKey !== 'string' || !/^[A-Za-z0-9_-]{8,128}$/.test(idempotencyKey)) {
602
+ throw new TypeError('idempotencyKey must contain 8 to 128 letters, digits, underscores or hyphens.');
603
+ }
604
+ return this._call('createTtsJob', { utterance_ids: [...new Set(utteranceIds)].sort(), idempotency_key: idempotencyKey }, null,
605
+ { endpoint: '/rest/tts/jobs/' + id + '/retry' });
606
+ }
607
+
608
+ downloadTtsCandidate(jobId, candidateId) {
609
+ const id = normalizeTtsJobId(jobId);
610
+ if (typeof candidateId !== 'string' || !/^[a-f0-9]{32}$/.test(candidateId)) throw new TypeError('candidateId must be a 32-character hexadecimal identifier.');
611
+ return this._call('createTtsJob', null, null, { endpoint: '/rest/tts/jobs/' + id + '/candidates/' + candidateId + '/audio',
612
+ method: 'GET', output: 'binary', filename: candidateId + '.wav' });
613
+ }
614
+
615
+ downloadTtsResult(jobId) {
616
+ const id = normalizeTtsJobId(jobId);
617
+ return this._call('createTtsJob', null, null, {
618
+ endpoint: '/rest/tts/jobs/' + id + '/result',
619
+ method: 'GET',
620
+ output: 'binary',
621
+ filename: id + '.mp3'
622
+ });
623
+ }
624
+
625
+ downloadTtsSubtitles(jobId) {
626
+ const id = normalizeTtsJobId(jobId);
627
+ return this._call('createTtsJob', null, null, {
628
+ endpoint: '/rest/tts/jobs/' + id + '/subtitles',
629
+ method: 'GET',
630
+ output: 'binary',
631
+ filename: id + '.ass'
632
+ });
633
+ }
634
+
635
+ htmlToPdf(html, options) {
636
+ const config = options || {};
637
+ return this._call('htmlToPdf', {
638
+ html: requiredString('html', html),
639
+ pagination: config.pagination ? 1 : 0
640
+ });
641
+ }
642
+
643
+ jsonToExcel(data, options) {
644
+ if (!Array.isArray(data)) throw new TypeError('data must be an array.');
645
+ const config = options || {};
646
+ const payload = { data_list: data };
647
+ if (config.sheetName !== undefined) payload.sheet_name = requiredString('sheetName', config.sheetName);
648
+ return this._call('jsonToExcel', payload);
649
+ }
650
+
651
+ summarize(text) {
652
+ return this._call('summarize', { text: requiredString('text', text, 100_000) });
653
+ }
654
+
655
+ polish(text) {
656
+ return this._call('polish', { text: requiredString('text', text, 100_000) });
657
+ }
658
+
659
+ _imageOptions(prompt, options, maxCount) {
660
+ const config = options || {};
661
+ for (const key of ['model', 'quality', 'count', 'n', 'outputCompression', 'input_fidelity', 'moderation', 'mask', 'maskFilename', 'maskContentType']) {
662
+ if (Object.prototype.hasOwnProperty.call(config, key)) throw new TypeError(`${key} is not a supported image option.`);
663
+ }
664
+ const imageCount = positiveInteger('imageCount', config.imageCount, 1);
665
+ if (imageCount > maxCount) throw new RangeError(`imageCount must not exceed ${maxCount}.`);
666
+ const size = config.size || '1024x1024';
667
+ if (!IMAGE_AI_SIZE_SET.has(size)) throw new TypeError(`size must be one of: ${IMAGE_AI_SIZES.join(', ')}.`);
668
+ const payload = {
669
+ prompt: requiredString('prompt', prompt, 28_000), image_count: imageCount,
670
+ size, output_format: config.outputFormat || 'png',
671
+ background: config.background || 'auto'
672
+ };
673
+ if (config.idempotencyKey !== undefined) {
674
+ payload.idempotency_key = requiredString('idempotencyKey', config.idempotencyKey, 128);
675
+ if (!/^[A-Za-z0-9_-]{8,128}$/.test(payload.idempotency_key)) throw new TypeError('idempotencyKey must use 8-128 letters, numbers, underscores, or hyphens.');
676
+ }
677
+ return payload;
678
+ }
679
+
680
+ async generateImages(prompt, options) {
681
+ const config = options || {}, payload = this._imageOptions(prompt, config, 4);
682
+ if (config.referenceImage === undefined) return this._call('generateImages', payload);
683
+ const uploadOptions = {
684
+ filename: config.referenceFilename, contentType: config.referenceContentType,
685
+ allowedTypes:['image/png','image/jpeg','image/webp'], maxBytes:MAX_IMAGE_AI_BYTES,
686
+ typeError:'referenceImage must be PNG, JPEG, or WebP.'
687
+ };
688
+ const source = await normalizeImage(config.referenceImage, uploadOptions), form = new FormData();
689
+ Object.entries(payload).forEach(([key,value]) => form.append(key, String(value)));
690
+ form.append('reference_image', source.blob, source.filename);
691
+ return this._call('generateImages', null, form);
692
+ }
693
+
694
+ async editImages(image, prompt, options) {
695
+ const config = options || {}, payload = this._imageOptions(prompt, config, 4);
696
+ const uploadOptions = Object.assign({}, config, { allowedTypes:['image/png','image/jpeg','image/webp'], maxBytes:MAX_IMAGE_AI_BYTES, typeError:'image must be PNG, JPEG, or WebP.' });
697
+ const source = await normalizeImage(image, uploadOptions), form = new FormData();
698
+ Object.entries(payload).forEach(([key,value]) => form.append(key, String(value)));
699
+ form.append('image', source.blob, source.filename);
700
+ return this._call('generateImages', null, form, { endpoint:'/rest/image-generation/edit' });
701
+ }
702
+
703
+ async createImageGenerationJob(prompt, options) {
704
+ const config = options || {}, payload = this._imageOptions(prompt, config, 50);
705
+ if (config.referenceImage === undefined) return this._call('generateImages', payload, null, { endpoint:'/rest/image-generation/jobs/generate', timeoutMs:60_000 });
706
+ const uploadOptions = {
707
+ filename: config.referenceFilename, contentType: config.referenceContentType,
708
+ allowedTypes:['image/png','image/jpeg','image/webp'], maxBytes:MAX_IMAGE_AI_BYTES,
709
+ typeError:'referenceImage must be PNG, JPEG, or WebP.'
710
+ };
711
+ const source = await normalizeImage(config.referenceImage, uploadOptions), form = new FormData();
712
+ Object.entries(payload).forEach(([key,value]) => form.append(key, String(value)));
713
+ form.append('reference_image', source.blob, source.filename);
714
+ return this._call('generateImages', null, form, { endpoint:'/rest/image-generation/jobs/generate', timeoutMs:60_000 });
715
+ }
716
+
717
+ async createImageEditJob(image, prompt, options) {
718
+ const config = options || {}, payload = this._imageOptions(prompt, config, 50);
719
+ const uploadOptions = Object.assign({}, config, { allowedTypes:['image/png','image/jpeg','image/webp'], maxBytes:MAX_IMAGE_AI_BYTES, typeError:'image must be PNG, JPEG, or WebP.' });
720
+ const source = await normalizeImage(image, uploadOptions), form = new FormData();
721
+ Object.entries(payload).forEach(([key,value]) => form.append(key, String(value)));
722
+ form.append('image', source.blob, source.filename);
723
+ return this._call('generateImages', null, form, { endpoint:'/rest/image-generation/jobs/edit', timeoutMs:60_000 });
724
+ }
725
+
726
+ getImageJob(jobId) {
727
+ const id = normalizeTtsJobId(jobId);
728
+ return this._call('generateImages', null, null, { endpoint:'/rest/image-generation/jobs/'+id, method:'GET', timeoutMs:30_000 });
729
+ }
730
+
731
+ downloadImageJobImage(jobId, index) {
732
+ const id=normalizeTtsJobId(jobId), value=Number(index);
733
+ if(!Number.isInteger(value)||value<0||value>49) throw new RangeError('index must be an integer from 0 through 49.');
734
+ return this._call('generateImages', null, null, { endpoint:'/rest/image-generation/jobs/'+id+'/images/'+value, method:'GET', output:'binary', filename:id+'-'+value+'.bin', timeoutMs:60_000 });
735
+ }
736
+
737
+ downloadImageJobArchive(jobId) {
738
+ const id=normalizeTtsJobId(jobId);
739
+ return this._call('generateImages', null, null, { endpoint:'/rest/image-generation/jobs/'+id+'/result', method:'GET', output:'binary', filename:id+'.zip', timeoutMs:60_000 });
740
+ }
741
+
742
+ // 간편인증 기반 조회 상품: 인증 요청(request*) 뒤 결과 조회(get*)로 폴링한다.
743
+ // 각 상품은 접수 시 정액, 최초 결과 반환 시 항목 단가가 과금되고 재조회는 무과금이다.
744
+ requestEmployment(input) {
745
+ const payload = dataRequestInput(input);
746
+ const insuranceYears = optionalRangeInteger('insuranceYears', (input || {}).insuranceYears, 1, 3);
747
+ if (insuranceYears !== undefined) payload.insuranceYears = insuranceYears;
748
+ return this._call('requestEmployment', payload);
749
+ }
750
+
751
+ getEmployment(transactionId) {
752
+ return this._call('requestEmployment', { transactionId: normalizeTransactionId(transactionId) }, null, { endpoint: '/rest/get_employment' });
753
+ }
754
+
755
+ requestPersonalIncome(input) {
756
+ const payload = dataRequestInput(input);
757
+ const incomeYears = optionalRangeInteger('incomeYears', (input || {}).incomeYears, 1, 5);
758
+ if (incomeYears !== undefined) payload.incomeYears = incomeYears;
759
+ return this._call('requestPersonalIncome', payload);
760
+ }
761
+
762
+ getPersonalIncome(transactionId) {
763
+ return this._call('requestPersonalIncome', { transactionId: normalizeTransactionId(transactionId) }, null, { endpoint: '/rest/get_personal_income' });
764
+ }
765
+
766
+ requestNpsJoinHistory(input) {
767
+ const payload = dataRequestInput(input);
768
+ const config = input || {};
769
+ const from = normalizeYearMonth('from', config.from);
770
+ const to = normalizeYearMonth('to', config.to);
771
+ if (from !== undefined) payload.from = from;
772
+ if (to !== undefined) payload.to = to;
773
+ return this._call('requestNpsJoinHistory', payload);
774
+ }
775
+
776
+ getNpsJoinHistory(transactionId) {
777
+ return this._call('requestNpsJoinHistory', { transactionId: normalizeTransactionId(transactionId) }, null, { endpoint: '/rest/get_nps_join_history' });
778
+ }
779
+
780
+ requestDrivingLicense(input) {
781
+ return this._call('requestDrivingLicense', dataRequestInput(input));
782
+ }
783
+
784
+ getDrivingLicense(transactionId) {
785
+ return this._call('requestDrivingLicense', { transactionId: normalizeTransactionId(transactionId) }, null, { endpoint: '/rest/get_driving_license' });
786
+ }
787
+
788
+ requestHealthCheckup(input) {
789
+ return this._call('requestHealthCheckup', dataRequestInput(input));
790
+ }
791
+
792
+ getHealthCheckup(transactionId) {
793
+ return this._call('requestHealthCheckup', { transactionId: normalizeTransactionId(transactionId) }, null, { endpoint: '/rest/get_health_checkup' });
794
+ }
795
+ }
796
+
797
+ module.exports = {
798
+ ApickClient,
799
+ ApickApiError,
800
+ ApickBinaryResult,
801
+ SERVICES,
802
+ TTS_VOICE_IDS,
803
+ AUTH_PROVIDERS,
804
+ DEFAULT_BASE_URL
805
+ };