-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathzyte.ts
More file actions
108 lines (89 loc) · 3.01 KB
/
zyte.ts
File metadata and controls
108 lines (89 loc) · 3.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
* Zyte Extract API 공통 유틸리티
*/
import { createTimeoutController } from './http.js';
export interface ZyteExtractResponse {
statusCode?: number;
httpResponseBody?: string;
detail?: string;
title?: string;
}
export interface ZyteExtractOptions {
apiKey?: string;
url: string;
timeout?: number;
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
headers?: Array<{ name: string; value: string }>;
bodyText?: string;
}
export function resolveZyteApiKey(apiKey?: string): string {
if (apiKey && apiKey.trim().length > 0) {
return apiKey;
}
/* c8 ignore start */
if (typeof process !== 'undefined' && process.env?.ZYTE_API_KEY) {
return process.env.ZYTE_API_KEY;
}
/* c8 ignore end */
throw new Error('ZYTE_API_KEY가 설정되지 않았습니다. .env 또는 Cloudflare Worker Secret을 확인해주세요.');
}
export function encodeBasicAuth(apiKey: string): string {
if (typeof btoa === 'function') {
return btoa(`${apiKey}:`);
}
/* c8 ignore start */
if (typeof Buffer !== 'undefined') {
return Buffer.from(`${apiKey}:`).toString('base64');
}
/* c8 ignore end */
throw new Error('Basic 인증 인코딩을 지원하지 않는 런타임입니다.');
}
export function decodeBase64(value: string): string {
if (typeof atob === 'function') {
const binary = atob(value);
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
/* c8 ignore start */
if (typeof Buffer !== 'undefined') {
return Buffer.from(value, 'base64').toString('utf8');
}
/* c8 ignore end */
throw new Error('Base64 디코딩을 지원하지 않는 런타임입니다.');
}
export async function requestByZyte(options: ZyteExtractOptions): Promise<ZyteExtractResponse> {
const { timeout = 15000, url, method = 'GET', headers = [], bodyText, apiKey } = options;
const auth = encodeBasicAuth(resolveZyteApiKey(apiKey));
const { controller, timeoutId } = createTimeoutController(timeout);
try {
const response = await fetch('https://api.zyte.com/v1/extract', {
method: 'POST',
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
url,
httpRequestMethod: method,
customHttpRequestHeaders: headers,
httpRequestText: bodyText,
httpResponseBody: true,
}),
signal: controller.signal,
});
const result = (await response.json()) as ZyteExtractResponse;
if (!response.ok) {
throw new Error(`Zyte API 호출 실패: ${response.status} ${result.detail || result.title || ''}`.trim());
}
return result;
} finally {
clearTimeout(timeoutId);
}
}
export function decodeZyteHttpBody<TResponse>(result: ZyteExtractResponse): TResponse {
if (!result.httpResponseBody) {
throw new Error('Zyte HTTP 응답 본문이 비어 있습니다.');
}
return JSON.parse(decodeBase64(result.httpResponseBody)) as TResponse;
}