@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 CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  > ℹ️ **이름에 대해**: `client` 는 **SAZU API 의 클라이언트 (= API 를 호출하는 라이브러리)** 라는 의미입니다.
9
9
  > 브라우저(client-side) 전용이라는 뜻이 아닙니다. **Node.js·서버 사이드에서만 사용**하세요.
10
- > (`@aws-sdk/client-s3`·`@supabase/supabase-js` 같은 업계 관용 네이밍을 따릅니다.)
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
- maxRetries: 2, // 기본 2 (5xx·네트워크 에러)
148
- timeoutMs: 30000, // 기본 30s
149
- headers: { // 추가 헤더 (선택)
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
- * - 브라우저 환경 감지 경고 (API 노출 방지)
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
- /** 추가 HTTP 헤더 (재정의 금지: Authorization·x-api-key·X-Client-Type) */
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 {
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA8BD,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;CA8GnE"}
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
- * - 브라우저 환경 감지 경고 (API 노출 방지)
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
- * 클라이언트 브라우저 환경 감지 — API 키 유출 위험 경고.
16
- *
17
- * 1회만 경고 출력 (반복 호출 spam 방지).
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
- if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
24
- warnedBrowser = true;
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('[@sazuapp/client] ⚠ 브라우저 환경 감지. SDK 서버 사이드 전용입니다.\n' +
27
- ' API 키가 클라이언트 번들에 포함되면 외부에 노출됩니다.\n' +
28
- ' Next.js: app/api/* 또는 server actions 안에서만 사용하세요.\n' +
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).replace(/\/$/, '');
51
- this.headers = opts.headers ?? {};
52
- this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
53
- this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
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.text();
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
- // build 시점에 tsc 가 dist/index.js 의 import 가 필요. 패키지 버전은 빌드 산출물에서 가져옴.
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 인터페이스만 정의 (런타임 zod 검증 — 의존성 0 유지).
4
+ * TypeScript 인터페이스만 정의 (런타임 검증 없음 — 의존성 0 유지).
5
5
  * 응답 schema 는 공개 docs (https://www.sazu.app/manse-api/docs) 와 동기.
6
6
  *
7
- * 비공개 내부 구조는 노출하지 않는다 — 본 SDK 는 REST API 의 얇은 wrapper 일 뿐,
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: Record<string, unknown>;
127
+ /** 시간대·경도·진태양시 등 보정 정보 */
128
+ timezone: TimezoneInfo;
104
129
  /** 요청한 모듈별 분석 결과 */
105
130
  modules: CalculateModules;
106
131
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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 인터페이스만 정의 (런타임 zod 검증 — 의존성 0 유지).
4
+ * TypeScript 인터페이스만 정의 (런타임 검증 없음 — 의존성 0 유지).
5
5
  * 응답 schema 는 공개 docs (https://www.sazu.app/manse-api/docs) 와 동기.
6
6
  *
7
- * 비공개 내부 구조는 노출하지 않는다 — 본 SDK 는 REST API 의 얇은 wrapper 일 뿐,
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.1.1",
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"