@sazuapp/client 0.1.1 → 0.3.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/README.md +16 -5
- package/dist/http.d.ts +14 -7
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +165 -25
- package/dist/types.d.ts +31 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
> ℹ️ **이름에 대해**: `client` 는 **SAZU API 의 클라이언트 (= API 를 호출하는 라이브러리)** 라는 의미입니다.
|
|
9
9
|
> 브라우저(client-side) 전용이라는 뜻이 아닙니다. **Node.js·서버 사이드에서만 사용**하세요.
|
|
10
|
-
> (`@aws-sdk/client-s3`·`@
|
|
10
|
+
> (`@aws-sdk/client-s3`·`@octokit/rest` 같은 업계 관용 네이밍을 따릅니다.)
|
|
11
11
|
|
|
12
12
|
<br/><br/><br/>
|
|
13
13
|
|
|
@@ -143,10 +143,11 @@ export async function POST(req: Request) {
|
|
|
143
143
|
```ts
|
|
144
144
|
const sazu = new SazuClient({
|
|
145
145
|
apiKey: process.env.SAZU_API_KEY!,
|
|
146
|
-
baseUrl: 'https://api.sazu.app', // 기본값
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
baseUrl: 'https://api.sazu.app', // 기본값 (*.sazu.app · localhost · 127.0.0.1 만 허용)
|
|
147
|
+
allowCustomBaseUrl: false, // 자체 호스팅·프록시 사용 시 true 필요
|
|
148
|
+
maxRetries: 2, // 기본 2, 상한 5
|
|
149
|
+
timeoutMs: 30000, // 기본 30s, 하한 1000 / 상한 600000
|
|
150
|
+
headers: { // 추가 헤더 (Authorization·Cookie·X-Forwarded-* 등 위험 헤더는 자동 제거)
|
|
150
151
|
'X-Trace-Id': '...',
|
|
151
152
|
},
|
|
152
153
|
})
|
|
@@ -154,6 +155,16 @@ const sazu = new SazuClient({
|
|
|
154
155
|
|
|
155
156
|
<br/><br/><br/>
|
|
156
157
|
|
|
158
|
+
## 보안 옵션 (v0.2.0+)
|
|
159
|
+
|
|
160
|
+
- **baseUrl 화이트리스트**: 기본적으로 `*.sazu.app`, `localhost`, `127.0.0.1` 만 허용합니다. 자체 호스팅·프록시 사용 시 `allowCustomBaseUrl: true` 를 명시하세요. (공급망 공격에 의한 키 유출 차단)
|
|
161
|
+
- **응답 크기 5MB 한도**: MITM/다운그레이드로 인한 OOM 방어. 초과 시 `RESPONSE_TOO_LARGE` 에러. (SDK 측 safety cap 입니다 — 서버 측 사용량 통계(대시보드)는 정상 호출로 기록됩니다. SAZU API 정상 응답은 1MB 미만이라 실제 발현 가능성은 극히 낮습니다.)
|
|
162
|
+
- **재시도·타임아웃 clamp**: `maxRetries` 0~5, `timeoutMs` 1000~600000 범위로 자동 제한.
|
|
163
|
+
- **위험 헤더 자동 제거**: `Authorization`·`Cookie`·`Host`·`X-Forwarded-*`·`X-API-Key`·`User-Agent` 등은 옵션으로 넘겨도 제거됩니다.
|
|
164
|
+
- **브라우저 차단 강제**: `SAZU_CLIENT_BLOCK_BROWSER=1` 환경변수 설정 시 브라우저 환경에서 throw.
|
|
165
|
+
|
|
166
|
+
<br/><br/><br/>
|
|
167
|
+
|
|
157
168
|
## 문의
|
|
158
169
|
|
|
159
170
|
contact@sazu.app
|
package/dist/http.d.ts
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP fetch wrapper — 서버 사이드 전용.
|
|
3
3
|
*
|
|
4
|
-
* 보안
|
|
5
|
-
* -
|
|
4
|
+
* 보안 정책 (v0.2.0):
|
|
5
|
+
* - baseUrl 화이트리스트 (*.sazu.app · localhost · 127.0.0.1). 다른 호스트는
|
|
6
|
+
* allowCustomBaseUrl: true 옵션을 명시해야 통과 — 공급망 공격 차단.
|
|
7
|
+
* - 응답 본문 5MB cap (ReadableStream) — MITM/다운그레이드 거대 응답 OOM 방어.
|
|
8
|
+
* - maxRetries 0~5 clamp — 5xx 증폭 공격 차단.
|
|
9
|
+
* - timeoutMs 1000~600000 clamp — 즉시 타임아웃 → 재시도 폭주 차단.
|
|
10
|
+
* - 위험 헤더 (Cookie·Host·X-Forwarded-*·Authorization 등) 거부 — smuggling 차단.
|
|
11
|
+
* - 브라우저 환경: 기본 console.warn. SAZU_CLIENT_BLOCK_BROWSER=1 시 throw.
|
|
6
12
|
* - 모든 요청에 X-Client-Type: sdk 헤더 부착 (서버 측 통계 분리)
|
|
7
13
|
* - 응답 _meta.responseId 자동 추출 → SazuApiError 에 포함
|
|
8
|
-
* - 5xx / 0 (네트워크) 에러는 exponential backoff 로 재시도 (옵션)
|
|
9
14
|
*/
|
|
10
15
|
export interface ClientOptions {
|
|
11
16
|
/** SAZU API 키 (필수). 환경변수 SAZU_API_KEY 권장. */
|
|
12
17
|
apiKey: string;
|
|
13
|
-
/** API 베이스 URL. 기본 https://api.sazu.app */
|
|
18
|
+
/** API 베이스 URL. 기본 https://api.sazu.app. 다른 호스트 사용 시 allowCustomBaseUrl 옵션 필요. */
|
|
14
19
|
baseUrl?: string;
|
|
15
|
-
/**
|
|
20
|
+
/** baseUrl 화이트리스트 우회 (자체 호스팅·프록시 등 명시적 opt-in). 기본 false. */
|
|
21
|
+
allowCustomBaseUrl?: boolean;
|
|
22
|
+
/** 추가 HTTP 헤더 (위험 헤더 자동 제거). */
|
|
16
23
|
headers?: Record<string, string>;
|
|
17
|
-
/** 재시도 횟수 (5xx·네트워크 에러). 기본 2. */
|
|
24
|
+
/** 재시도 횟수 (5xx·네트워크 에러). 기본 2. 상한 5. */
|
|
18
25
|
maxRetries?: number;
|
|
19
|
-
/** fetch 타임아웃 ms. 기본 30000. */
|
|
26
|
+
/** fetch 타임아웃 ms. 기본 30000. 하한 1000, 상한 600000. */
|
|
20
27
|
timeoutMs?: number;
|
|
21
28
|
}
|
|
22
29
|
export declare class HttpClient {
|
package/dist/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6DAA6D;IAC7D,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA8KD,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,SAAS,CAAQ;gBAEb,IAAI,EAAE,aAAa;IAgBzB,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,CAAC,CAAC;CA4GnE"}
|
package/dist/http.js
CHANGED
|
@@ -1,37 +1,181 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP fetch wrapper — 서버 사이드 전용.
|
|
3
3
|
*
|
|
4
|
-
* 보안
|
|
5
|
-
* -
|
|
4
|
+
* 보안 정책 (v0.2.0):
|
|
5
|
+
* - baseUrl 화이트리스트 (*.sazu.app · localhost · 127.0.0.1). 다른 호스트는
|
|
6
|
+
* allowCustomBaseUrl: true 옵션을 명시해야 통과 — 공급망 공격 차단.
|
|
7
|
+
* - 응답 본문 5MB cap (ReadableStream) — MITM/다운그레이드 거대 응답 OOM 방어.
|
|
8
|
+
* - maxRetries 0~5 clamp — 5xx 증폭 공격 차단.
|
|
9
|
+
* - timeoutMs 1000~600000 clamp — 즉시 타임아웃 → 재시도 폭주 차단.
|
|
10
|
+
* - 위험 헤더 (Cookie·Host·X-Forwarded-*·Authorization 등) 거부 — smuggling 차단.
|
|
11
|
+
* - 브라우저 환경: 기본 console.warn. SAZU_CLIENT_BLOCK_BROWSER=1 시 throw.
|
|
6
12
|
* - 모든 요청에 X-Client-Type: sdk 헤더 부착 (서버 측 통계 분리)
|
|
7
13
|
* - 응답 _meta.responseId 자동 추출 → SazuApiError 에 포함
|
|
8
|
-
* - 5xx / 0 (네트워크) 에러는 exponential backoff 로 재시도 (옵션)
|
|
9
14
|
*/
|
|
10
15
|
import { SazuApiError } from './errors.js';
|
|
11
16
|
const DEFAULT_BASE_URL = 'https://api.sazu.app';
|
|
12
17
|
const DEFAULT_MAX_RETRIES = 2;
|
|
13
18
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
const MAX_RETRIES_HARD_CAP = 5;
|
|
20
|
+
const MIN_TIMEOUT_MS = 1_000;
|
|
21
|
+
const MAX_TIMEOUT_MS = 600_000;
|
|
22
|
+
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024; // 5MB
|
|
23
|
+
// 화이트리스트: 정확 호스트 또는 suffix 매칭 (.sazu.app)
|
|
24
|
+
const ALLOWED_HOST_SUFFIXES = ['.sazu.app'];
|
|
25
|
+
const ALLOWED_HOSTS_EXACT = ['sazu.app', 'localhost', '127.0.0.1'];
|
|
26
|
+
// 거부 헤더 (소문자 비교, smuggling/세션 위조/보호 헤더 override 차단)
|
|
27
|
+
const DENIED_HEADER_LOWER = new Set([
|
|
28
|
+
'authorization',
|
|
29
|
+
'cookie',
|
|
30
|
+
'host',
|
|
31
|
+
'x-api-key',
|
|
32
|
+
'x-client-type',
|
|
33
|
+
'x-forwarded-for',
|
|
34
|
+
'x-forwarded-host',
|
|
35
|
+
'x-forwarded-proto',
|
|
36
|
+
'x-forwarded-port',
|
|
37
|
+
'x-real-ip',
|
|
38
|
+
'user-agent',
|
|
39
|
+
'content-length',
|
|
40
|
+
'transfer-encoding',
|
|
41
|
+
]);
|
|
42
|
+
// ── 환경 가드 ────────────────────────────────────────────────────────────
|
|
19
43
|
let warnedBrowser = false;
|
|
20
44
|
function warnIfBrowser() {
|
|
45
|
+
if (typeof window === 'undefined' || typeof window.document === 'undefined')
|
|
46
|
+
return;
|
|
47
|
+
if (typeof process !== 'undefined' &&
|
|
48
|
+
process.env?.SAZU_CLIENT_BLOCK_BROWSER === '1') {
|
|
49
|
+
throw new SazuApiError({
|
|
50
|
+
status: 0,
|
|
51
|
+
code: 'BROWSER_BLOCKED',
|
|
52
|
+
message: '[@sazuapp/client] 브라우저 환경에서 차단됨 (SAZU_CLIENT_BLOCK_BROWSER=1). 서버 사이드에서 사용하세요.',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
21
55
|
if (warnedBrowser)
|
|
22
56
|
return;
|
|
23
|
-
|
|
24
|
-
|
|
57
|
+
warnedBrowser = true;
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.warn('[@sazuapp/client] ⚠ 브라우저 환경 감지. 본 SDK 는 서버 사이드 전용입니다.\n' +
|
|
60
|
+
' API 키가 클라이언트 번들에 포함되면 외부에 노출됩니다.\n' +
|
|
61
|
+
' Next.js: app/api/* 또는 server actions 안에서만 사용하세요.\n' +
|
|
62
|
+
' Vite/SvelteKit: +server.ts 또는 endpoint 에서만 사용하세요.\n' +
|
|
63
|
+
' 강제 차단: SAZU_CLIENT_BLOCK_BROWSER=1 환경변수 설정.');
|
|
64
|
+
}
|
|
65
|
+
// ── 입력 sanitize ────────────────────────────────────────────────────────
|
|
66
|
+
function sanitizeBaseUrl(baseUrl, allowCustom) {
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = new URL(baseUrl);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new SazuApiError({
|
|
73
|
+
status: 0,
|
|
74
|
+
code: 'INVALID_BASE_URL',
|
|
75
|
+
message: `baseUrl 파싱 실패: ${baseUrl}`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
79
|
+
throw new SazuApiError({
|
|
80
|
+
status: 0,
|
|
81
|
+
code: 'INVALID_BASE_URL',
|
|
82
|
+
message: `baseUrl 프로토콜은 http(s) 만 허용됩니다: ${parsed.protocol}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (!allowCustom) {
|
|
86
|
+
const host = parsed.hostname.toLowerCase();
|
|
87
|
+
const isAllowed = ALLOWED_HOSTS_EXACT.includes(host) ||
|
|
88
|
+
ALLOWED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix));
|
|
89
|
+
if (!isAllowed) {
|
|
90
|
+
throw new SazuApiError({
|
|
91
|
+
status: 0,
|
|
92
|
+
code: 'BASE_URL_NOT_ALLOWED',
|
|
93
|
+
message: `baseUrl 호스트가 화이트리스트 밖입니다 (${host}). ` +
|
|
94
|
+
`*.sazu.app · localhost · 127.0.0.1 만 기본 허용. ` +
|
|
95
|
+
`자체 호스팅·프록시 사용 시 allowCustomBaseUrl: true 옵션을 명시하세요.`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return baseUrl.replace(/\/$/, '');
|
|
100
|
+
}
|
|
101
|
+
let warnedStrippedHeaders = false;
|
|
102
|
+
function sanitizeHeaders(input) {
|
|
103
|
+
if (!input)
|
|
104
|
+
return {};
|
|
105
|
+
const out = {};
|
|
106
|
+
const stripped = [];
|
|
107
|
+
for (const [k, v] of Object.entries(input)) {
|
|
108
|
+
if (typeof v !== 'string')
|
|
109
|
+
continue;
|
|
110
|
+
if (DENIED_HEADER_LOWER.has(k.toLowerCase())) {
|
|
111
|
+
stripped.push(k);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
out[k] = v;
|
|
115
|
+
}
|
|
116
|
+
if (stripped.length > 0 && !warnedStrippedHeaders) {
|
|
117
|
+
warnedStrippedHeaders = true;
|
|
25
118
|
// eslint-disable-next-line no-console
|
|
26
|
-
console.warn(
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
' Vite/SvelteKit: +server.ts 또는 endpoint 에서만 사용하세요.');
|
|
119
|
+
console.warn(`[@sazuapp/client] ⚠ 위험 헤더 자동 제거: ${stripped.join(', ')}. ` +
|
|
120
|
+
'보호 헤더 (x-api-key·Authorization·X-Client-Type·User-Agent) 와 ' +
|
|
121
|
+
'smuggling 위험 헤더 (Cookie·Host·X-Forwarded-*) 는 옵션으로 넘겨도 무시됩니다.');
|
|
30
122
|
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function clampNumber(value, fallback, min, max) {
|
|
126
|
+
const n = typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
127
|
+
return Math.min(Math.max(n, min), max);
|
|
31
128
|
}
|
|
32
129
|
async function sleep(ms) {
|
|
33
130
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* 응답 본문을 size cap 적용해 텍스트로 읽기.
|
|
134
|
+
* 한도 초과 시 SazuApiError throw.
|
|
135
|
+
*/
|
|
136
|
+
async function readBodyWithCap(res, maxBytes) {
|
|
137
|
+
if (!res.body)
|
|
138
|
+
return await res.text();
|
|
139
|
+
const reader = res.body.getReader();
|
|
140
|
+
const chunks = [];
|
|
141
|
+
let received = 0;
|
|
142
|
+
try {
|
|
143
|
+
while (true) {
|
|
144
|
+
const { done, value } = await reader.read();
|
|
145
|
+
if (done)
|
|
146
|
+
break;
|
|
147
|
+
if (value) {
|
|
148
|
+
received += value.byteLength;
|
|
149
|
+
if (received > maxBytes) {
|
|
150
|
+
try {
|
|
151
|
+
await reader.cancel();
|
|
152
|
+
}
|
|
153
|
+
catch { /* ignore */ }
|
|
154
|
+
throw new SazuApiError({
|
|
155
|
+
status: res.status,
|
|
156
|
+
code: 'RESPONSE_TOO_LARGE',
|
|
157
|
+
message: `응답 본문이 ${maxBytes} bytes 한도를 초과했습니다.`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
chunks.push(value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
try {
|
|
166
|
+
reader.releaseLock();
|
|
167
|
+
}
|
|
168
|
+
catch { /* ignore */ }
|
|
169
|
+
}
|
|
170
|
+
const merged = new Uint8Array(received);
|
|
171
|
+
let offset = 0;
|
|
172
|
+
for (const c of chunks) {
|
|
173
|
+
merged.set(c, offset);
|
|
174
|
+
offset += c.byteLength;
|
|
175
|
+
}
|
|
176
|
+
return new TextDecoder('utf-8').decode(merged);
|
|
177
|
+
}
|
|
178
|
+
// ── HttpClient ────────────────────────────────────────────────────────────
|
|
35
179
|
export class HttpClient {
|
|
36
180
|
apiKey;
|
|
37
181
|
baseUrl;
|
|
@@ -47,10 +191,10 @@ export class HttpClient {
|
|
|
47
191
|
});
|
|
48
192
|
}
|
|
49
193
|
this.apiKey = opts.apiKey;
|
|
50
|
-
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL
|
|
51
|
-
this.headers = opts.headers
|
|
52
|
-
this.maxRetries = opts.maxRetries
|
|
53
|
-
this.timeoutMs = opts.timeoutMs
|
|
194
|
+
this.baseUrl = sanitizeBaseUrl(opts.baseUrl ?? DEFAULT_BASE_URL, opts.allowCustomBaseUrl === true);
|
|
195
|
+
this.headers = sanitizeHeaders(opts.headers);
|
|
196
|
+
this.maxRetries = clampNumber(opts.maxRetries, DEFAULT_MAX_RETRIES, 0, MAX_RETRIES_HARD_CAP);
|
|
197
|
+
this.timeoutMs = clampNumber(opts.timeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
54
198
|
}
|
|
55
199
|
async request(path, init = {}) {
|
|
56
200
|
warnIfBrowser();
|
|
@@ -58,7 +202,7 @@ export class HttpClient {
|
|
|
58
202
|
const headers = {
|
|
59
203
|
'Content-Type': 'application/json',
|
|
60
204
|
...this.headers,
|
|
61
|
-
// 아래 3개는 사용자가 override 불가
|
|
205
|
+
// 아래 3개는 사용자가 override 불가 (sanitizeHeaders 도 차단하지만 이중 보호)
|
|
62
206
|
'x-api-key': this.apiKey,
|
|
63
207
|
'X-Client-Type': 'sdk',
|
|
64
208
|
'User-Agent': `@sazuapp/client/${PACKAGE_VERSION}`,
|
|
@@ -74,7 +218,7 @@ export class HttpClient {
|
|
|
74
218
|
signal: controller.signal,
|
|
75
219
|
});
|
|
76
220
|
clearTimeout(timeoutHandle);
|
|
77
|
-
const text = await res
|
|
221
|
+
const text = await readBodyWithCap(res, MAX_RESPONSE_BYTES);
|
|
78
222
|
let body = null;
|
|
79
223
|
try {
|
|
80
224
|
body = text ? JSON.parse(text) : null;
|
|
@@ -96,7 +240,6 @@ export class HttpClient {
|
|
|
96
240
|
responseId,
|
|
97
241
|
retryAfterSec: Number.isFinite(retryAfterSec) ? retryAfterSec : undefined,
|
|
98
242
|
});
|
|
99
|
-
// 재시도 가능한 케이스: 5xx, 429
|
|
100
243
|
if (err.isTransient && attempt < this.maxRetries) {
|
|
101
244
|
const backoffMs = Math.min(2 ** attempt * 500, 5000);
|
|
102
245
|
lastError = err;
|
|
@@ -132,7 +275,6 @@ export class HttpClient {
|
|
|
132
275
|
}
|
|
133
276
|
throw err;
|
|
134
277
|
}
|
|
135
|
-
// 네트워크·timeout 등
|
|
136
278
|
const message = err instanceof Error ? err.message : String(err);
|
|
137
279
|
const netErr = new SazuApiError({
|
|
138
280
|
status: 0,
|
|
@@ -152,6 +294,4 @@ export class HttpClient {
|
|
|
152
294
|
throw lastError ?? new SazuApiError({ status: 0, code: 'UNKNOWN', message: '호출 실패.' });
|
|
153
295
|
}
|
|
154
296
|
}
|
|
155
|
-
|
|
156
|
-
// 단순화 위해 const 로 fix — 패키지 메이저 변경 시 동기 갱신.
|
|
157
|
-
const PACKAGE_VERSION = '0.1.0';
|
|
297
|
+
const PACKAGE_VERSION = '0.2.0';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SAZU API 입력·출력 타입.
|
|
3
3
|
*
|
|
4
|
-
* TypeScript 인터페이스만 정의 (런타임
|
|
4
|
+
* TypeScript 인터페이스만 정의 (런타임 검증 없음 — 의존성 0 유지).
|
|
5
5
|
* 응답 schema 는 공개 docs (https://www.sazu.app/manse-api/docs) 와 동기.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* sazu-logic 코드·계산 알고리즘은 절대 포함하지 않는다.
|
|
7
|
+
* 본 SDK 는 REST API 의 얇은 wrapper 일 뿐, 계산 엔진·내부 알고리즘은 포함하지 않습니다.
|
|
9
8
|
*/
|
|
10
9
|
export interface ResponseMeta {
|
|
11
10
|
/** forensics 추적용 응답 ID. 형식: `${ts36}-${keyHash8}-${rand8}` */
|
|
@@ -51,7 +50,7 @@ export interface CalculateInput {
|
|
|
51
50
|
isFemale: boolean;
|
|
52
51
|
/** 음력 여부. 기본 false. */
|
|
53
52
|
isLunar?: boolean;
|
|
54
|
-
/** 출생 도시 (
|
|
53
|
+
/** 출생 도시 (도시 경도 보정용). 기본 '서울'. */
|
|
55
54
|
birthCity?: string;
|
|
56
55
|
/** 한글 또는 한자 출력. 기본 'ko'. */
|
|
57
56
|
locale?: Locale;
|
|
@@ -59,6 +58,11 @@ export interface CalculateInput {
|
|
|
59
58
|
modules?: string[];
|
|
60
59
|
/** 대운 개수 (13~20) */
|
|
61
60
|
decadeCount?: number;
|
|
61
|
+
/**
|
|
62
|
+
* 진태양시(경도차 + 균시차) 적용 여부. 기본 false.
|
|
63
|
+
* false = 한국 관습(자시 23:30, 경도차만). true = 진태양시(자시 23:00, 균시차 포함).
|
|
64
|
+
*/
|
|
65
|
+
trueSolarTime?: boolean;
|
|
62
66
|
/** 응답 상세 수준. 기본 'standard'. */
|
|
63
67
|
detail?: DetailLevel;
|
|
64
68
|
}
|
|
@@ -93,14 +97,35 @@ export interface CalculateModules {
|
|
|
93
97
|
/** 기타 모듈 (확장 가능) */
|
|
94
98
|
[moduleId: string]: unknown;
|
|
95
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* 시간대·경도·진태양시 보정 정보 (응답 timezone).
|
|
102
|
+
*/
|
|
103
|
+
export interface TimezoneInfo {
|
|
104
|
+
/** 출생 도시 (요청값 echo) */
|
|
105
|
+
city: string;
|
|
106
|
+
/** 도시 표준시 UTC offset (분) */
|
|
107
|
+
utcOffset: number;
|
|
108
|
+
/** 도시 경도 (°, 동경 +) */
|
|
109
|
+
longitude: number;
|
|
110
|
+
/** 'convention' = 관습(자시 23:30, 경도만) / 'trueSolar' = 진태양시(자시 23:00, 균시차 포함) */
|
|
111
|
+
mode: 'convention' | 'trueSolar';
|
|
112
|
+
/** 경도차 보정 (분) */
|
|
113
|
+
longitudeCorrectionMinutes: number;
|
|
114
|
+
/** 균시차 (분). 관습 모드는 null. */
|
|
115
|
+
equationOfTimeMinutes: number | null;
|
|
116
|
+
/** 최종 보정 (분) */
|
|
117
|
+
correctionMinutes: number;
|
|
118
|
+
/** 확장 필드 대비 */
|
|
119
|
+
[key: string]: unknown;
|
|
120
|
+
}
|
|
96
121
|
/**
|
|
97
122
|
* 사주 분석 응답 — 서버가 반환하는 data 전체.
|
|
98
123
|
*/
|
|
99
124
|
export interface CalculateResult {
|
|
100
125
|
/** 정규화된 입력값 (서버 측 검증·변환 결과) */
|
|
101
126
|
input: Record<string, unknown>;
|
|
102
|
-
/**
|
|
103
|
-
timezone:
|
|
127
|
+
/** 시간대·경도·진태양시 등 보정 정보 */
|
|
128
|
+
timezone: TimezoneInfo;
|
|
104
129
|
/** 요청한 모듈별 분석 결과 */
|
|
105
130
|
modules: CalculateModules;
|
|
106
131
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SAZU API 입력·출력 타입.
|
|
3
3
|
*
|
|
4
|
-
* TypeScript 인터페이스만 정의 (런타임
|
|
4
|
+
* TypeScript 인터페이스만 정의 (런타임 검증 없음 — 의존성 0 유지).
|
|
5
5
|
* 응답 schema 는 공개 docs (https://www.sazu.app/manse-api/docs) 와 동기.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* sazu-logic 코드·계산 알고리즘은 절대 포함하지 않는다.
|
|
7
|
+
* 본 SDK 는 REST API 의 얇은 wrapper 일 뿐, 계산 엔진·내부 알고리즘은 포함하지 않습니다.
|
|
9
8
|
*/
|
|
10
9
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sazuapp/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "SAZU API 공식 TypeScript SDK — 사주·만세력·합형충파해·격국·용신 등 14개 명리 분석을 한 줄로.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc -p tsconfig.json",
|
|
21
21
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
22
|
-
"dev": "tsc -p tsconfig.json --watch"
|
|
22
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
23
|
+
"prepublishOnly": "tsc -p tsconfig.json"
|
|
23
24
|
},
|
|
24
25
|
"engines": {
|
|
25
26
|
"node": ">=18"
|