entity-server-client 0.3.0 → 0.3.1

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.
@@ -9,10 +9,16 @@
9
9
  ```json
10
10
  {
11
11
  "ok": true,
12
- "packet_encryption": false
12
+ "packet_encryption": true,
13
+ "packet_mode": "anonymous",
14
+ "packet_token": "anon.v1...."
13
15
  }
14
16
  ```
15
17
 
18
+ - `packet_encryption`: 서버가 패킷 암호화를 요구하는지 여부
19
+ - `packet_mode`: `"jwt"` (JWT 인증) 또는 `"anonymous"` (익명 토큰)
20
+ - `packet_token`: `anonymous` 모드일 때 포함. `checkHealth()` 호출 시 클라이언트 내부 `anonymousPacketToken`에 **자동 저장**됨
21
+
16
22
  ## 자동 초기화 (권장)
17
23
 
18
24
  **admin-web 예시:**
@@ -32,3 +32,14 @@ React Hook:
32
32
  ```ts
33
33
  import { useEntityServer } from "entity-server-client/react";
34
34
  ```
35
+
36
+ 패킷 프로토콜 코어 (SDK 없이 암복호만 필요할 때):
37
+
38
+ ```ts
39
+ import {
40
+ derivePacketKey,
41
+ packetMagicLenFromKey,
42
+ encryptPacket,
43
+ decryptPacket,
44
+ } from "entity-server-client/packet";
45
+ ```
@@ -3,7 +3,12 @@
3
3
  ## 자동 복호화
4
4
 
5
5
  서버 응답이 `Content-Type: application/octet-stream`이면 `request()` 내부에서 자동으로 복호화됩니다.
6
- XChaCha20-Poly1305 알고리즘을 사용하며, 키는 현재 JWT 토큰의 SHA-256 해시입니다.
6
+ XChaCha20-Poly1305 알고리즘을 사용하며, 키는 HKDF-SHA256으로 파생됩니다.
7
+
8
+ - 키 소스: `hmacSecret` → `token`(JWT) → `anonymousPacketToken` 순서로 결정
9
+ - salt: `entity-server:hkdf:v1`
10
+ - info: `entity-server:packet-encryption`
11
+ - 출력: 32바이트
7
12
 
8
13
  별도 처리 없이 일반 응답과 동일하게 사용하면 됩니다.
9
14
 
@@ -23,3 +28,63 @@ const data = client.readRequestBody(
23
28
  // 일반 JSON body 파싱
24
29
  const data2 = client.readRequestBody(jsonString, "application/json");
25
30
  ```
31
+
32
+ ## `entity-server-client/packet` 서브패스 — 프로토콜 코어
33
+
34
+ 패킷 암복호 프로토콜만 독립적으로 사용할 때는 서브패스를 import합니다.
35
+ 브라우저, Node.js, 중간 계층 서버 등 SDK 전체가 불필요한 환경에서 유용합니다.
36
+
37
+ ```ts
38
+ import {
39
+ derivePacketKey,
40
+ packetMagicLenFromKey,
41
+ encryptPacket,
42
+ decryptPacket,
43
+ PACKET_KEY_SIZE,
44
+ PACKET_MAGIC_MIN,
45
+ PACKET_MAGIC_RANGE,
46
+ PACKET_NONCE_SIZE,
47
+ PACKET_TAG_SIZE,
48
+ PACKET_HKDF_SALT,
49
+ PACKET_INFO_LABEL,
50
+ } from "entity-server-client/packet";
51
+ ```
52
+
53
+ ### `derivePacketKey(source, infoLabel?)`
54
+
55
+ HKDF-SHA256으로 32바이트 패킷 키를 파생합니다.
56
+
57
+ ```ts
58
+ const key = derivePacketKey("anon.v1.example-token");
59
+ // infoLabel 기본값: "entity-server:packet-encryption"
60
+ ```
61
+
62
+ ### `packetMagicLenFromKey(key, magicMin?, magicRange?)`
63
+
64
+ 키에서 magic 바이트 길이를 파생합니다. `magicLen = magicMin + key[31] % magicRange`
65
+
66
+ ```ts
67
+ const magicLen = packetMagicLenFromKey(key);
68
+ // 기본값: magicMin=2, magicRange=14 → 결과 범위 [2, 15]
69
+ ```
70
+
71
+ ### `encryptPacket(plaintext, key, magicMin?, magicRange?)`
72
+
73
+ 평문 바이트를 XChaCha20-Poly1305로 암호화합니다.
74
+ 포맷: `[random_magic:magicLen][random_nonce:24][ciphertext+tag]`
75
+
76
+ ```ts
77
+ const encrypted = encryptPacket(
78
+ new TextEncoder().encode(JSON.stringify({ ok: true })),
79
+ key,
80
+ );
81
+ ```
82
+
83
+ ### `decryptPacket(buffer, key, magicMin?, magicRange?)`
84
+
85
+ 암호화된 패킷을 복호화해 평문 바이트를 반환합니다.
86
+
87
+ ```ts
88
+ const plaintext = decryptPacket(encrypted, key);
89
+ console.log(new TextDecoder().decode(plaintext));
90
+ ```
@@ -0,0 +1,118 @@
1
+ # 범용 HTTP 요청
2
+
3
+ `EntityServerClient`에서 제공하지 않는 커스텀 라우트(go서버·앱서버 신규 엔드포인트 등)를 직접 호출할 때 사용합니다.
4
+ 인증 헤더, 패킷 암호화, HMAC 서명 등은 기존 SDK 옵션 그대로 자동 적용됩니다.
5
+
6
+ ---
7
+
8
+ ## `requestJson<T>(method, path, body?, withAuth?, extraHeaders?)`
9
+
10
+ JSON 요청·응답의 범용 메서드입니다.
11
+
12
+ - 응답이 `application/json`이면 파싱하여 반환합니다.
13
+ - 응답이 `application/octet-stream`이면 패킷 복호화 후 반환합니다.
14
+ - `ok` 필드 강제 없음 — go서버처럼 자유 응답 포맷을 그대로 반환합니다.
15
+ - `encryptRequests: true`이면 요청 바디도 자동 암호화됩니다.
16
+
17
+ | 파라미터 | 타입 | 기본값 | 설명 |
18
+ | -------------- | ------------------------ | ------ | ------------------------- |
19
+ | `method` | `string` | | `"GET"`, `"POST"` 등 |
20
+ | `path` | `string` | | `/api/v1/...` 형태의 경로 |
21
+ | `body` | `unknown` | — | 요청 바디 (GET이면 생략) |
22
+ | `withAuth` | `boolean` | `true` | `Authorization` 헤더 포함 |
23
+ | `extraHeaders` | `Record<string, string>` | — | 추가 헤더 |
24
+
25
+ ```ts
26
+ // GET — 인증 없이
27
+ const res = await client.requestJson<{ version: string }>(
28
+ "GET",
29
+ "/api/v1/status",
30
+ undefined,
31
+ false,
32
+ );
33
+ console.log(res.version);
34
+
35
+ // POST — 인증 포함, 암호화 자동 적용
36
+ const res = await client.requestJson<{ distance_nm: number }>(
37
+ "POST",
38
+ "/api/v1/distance-server/route",
39
+ { from: [37.5665, 126.978], to: [35.1796, 129.0756] },
40
+ );
41
+ console.log(res.distance_nm);
42
+
43
+ // 추가 헤더
44
+ const res = await client.requestJson<MyResponse>(
45
+ "POST",
46
+ "/api/v1/custom/endpoint",
47
+ { key: "value" },
48
+ true,
49
+ { "X-Custom-Header": "value" },
50
+ );
51
+ ```
52
+
53
+ ---
54
+
55
+ ## `requestBinary(method, path, body?, withAuth?)`
56
+
57
+ 바이너리(ArrayBuffer) 응답을 그대로 반환합니다.
58
+ 이미지, PDF, 압축 파일 등 바이너리 스트림이 오는 엔드포인트에 사용합니다.
59
+
60
+ ```ts
61
+ const buffer = await client.requestBinary("GET", "/api/v1/export/report.pdf");
62
+
63
+ // Blob 변환 후 다운로드
64
+ const blob = new Blob([buffer], { type: "application/pdf" });
65
+ const url = URL.createObjectURL(blob);
66
+ ```
67
+
68
+ ---
69
+
70
+ ## `requestForm<T>(method, path, form, withAuth?)`
71
+
72
+ `multipart/form-data` 요청을 보내고 JSON 응답을 반환합니다.
73
+ 파일과 일반 필드를 함께 전송해야 하는 커스텀 엔드포인트에 사용합니다.
74
+
75
+ ```ts
76
+ const form = new FormData();
77
+ form.append("file", fileBlob, "document.pdf");
78
+ form.append("category", "report");
79
+
80
+ const res = await client.requestForm<{ ok: boolean; seq: number }>(
81
+ "POST",
82
+ "/api/v1/custom/upload",
83
+ form,
84
+ );
85
+ console.log(res.seq);
86
+ ```
87
+
88
+ ---
89
+
90
+ ## `requestFormBinary(method, path, form, withAuth?)`
91
+
92
+ `multipart/form-data` 요청을 보내고 바이너리(ArrayBuffer)를 반환합니다.
93
+ 업로드 후 처리된 파일(변환·압축 등)을 바이너리로 받을 때 사용합니다.
94
+
95
+ ```ts
96
+ const form = new FormData();
97
+ form.append("image", imageBlob, "photo.jpg");
98
+
99
+ const processedBuffer = await client.requestFormBinary(
100
+ "POST",
101
+ "/api/v1/custom/image-process",
102
+ form,
103
+ );
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 내부 메서드와의 차이
109
+
110
+ SDK 내부에서 엔티티 API를 호출할 때는 `ok: true` 강제 검사가 있는 `_request()`를 사용합니다.
111
+ 외부에서 커스텀 엔드포인트를 호출할 때는 위의 public 메서드를 사용하세요.
112
+
113
+ | 메서드 | public | `ok` 강제 | 암호화 | 용도 |
114
+ | ------------------- | ------ | --------- | ------ | ----------------------------- |
115
+ | `requestJson` | ✅ | ❌ | ✅ | 커스텀 라우트 (JSON) |
116
+ | `requestBinary` | ✅ | ❌ | ❌ | 커스텀 라우트 (바이너리) |
117
+ | `requestForm` | ✅ | ✅ | ❌ | 커스텀 라우트 (form) |
118
+ | `requestFormBinary` | ✅ | ❌ | ❌ | 커스텀 라우트 (form→바이너리) |
package/docs/api/setup.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  ## `new EntityServerClient(options?)`
4
4
 
5
- | 옵션 | 타입 | 기본값 | 설명 |
6
- | ------------------ | ---------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
7
- | `baseUrl` | `string` | `VITE_ENTITY_SERVER_URL` 또는 `http://localhost:47200` | 서버 주소 |
8
- | `token` | `string` | `""` | JWT Access Token |
9
- | `encryptRequests` | `boolean` | `false` | `true` 로 설정하면 인증된 POST/PUT/PATCH 요청 바디를 XChaCha20-Poly1305 로 암호화하여 전송합니다. 서버에서 `requirePacketEncryption = true` 로 설정된 경우 반드시 활성화해야 합니다. |
10
- | `keepSession` | `boolean` | `false` | `true`이면 `login()` 성공 후 Access Token 만료 전에 자동으로 갱신합니다. |
11
- | `refreshBuffer` | `number` | `60` | 만료 몇 초 전에 갱신을 시도할지 설정합니다. 예: `expires_in=3600`, `refreshBuffer=60` → 3540초 후 갱신 |
12
- | `onTokenRefreshed` | `(accessToken, expiresIn) => void` | — | 자동 갱신 성공 시 호출됩니다. 새 `access_token`을 받아 앱 레벨 저장소에 업데이트하세요. |
13
- | `onSessionExpired` | `(error: Error) => void` | — | 세션 유지 갱신 실패 시 호출됩니다 (refresh_token 만료 등). 로그인 페이지로 이동하는 등의 처리를 여기서 합니다. |
5
+ | 옵션 | 타입 | 기본값 | 설명 |
6
+ | ------------------ | ---------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
7
+ | `baseUrl` | `string` | `VITE_ENTITY_SERVER_URL` 또는 `""` | 서버 주소 |
8
+ | `token` | `string` | `""` | JWT Access Token |
9
+ | `encryptRequests` | `boolean` | `false` | `true` 로 설정하면 인증된 POST/PUT/PATCH 요청 바디를 XChaCha20-Poly1305 로 암호화하여 전송합니다. 서버에서 `requirePacketEncryption = true` 로 설정된 경우 반드시 활성화해야 합니다. |
10
+ | `keepSession` | `boolean` | `false` | `true`이면 `login()` 성공 후 Access Token 만료 전에 자동으로 갱신합니다. |
11
+ | `refreshBuffer` | `number` | `60` | 만료 몇 초 전에 갱신을 시도할지 설정합니다. 예: `expires_in=3600`, `refreshBuffer=60` → 3540초 후 갱신 |
12
+ | `onTokenRefreshed` | `(accessToken, expiresIn) => void` | — | 자동 갱신 성공 시 호출됩니다. 새 `access_token`을 받아 앱 레벨 저장소에 업데이트하세요. |
13
+ | `onSessionExpired` | `(error: Error) => void` | — | 세션 유지 갱신 실패 시 호출됩니다 (refresh_token 만료 등). 로그인 페이지로 이동하는 등의 처리를 여기서 합니다. |
14
14
 
15
15
  ```ts
16
16
  // 직접 생성
package/docs/apis.md CHANGED
@@ -4,22 +4,23 @@
4
4
 
5
5
  ## 목차
6
6
 
7
- | 구분 | 파일 |
8
- | ------------------ | -------------------------------------------- |
9
- | import | [api/import.md](api/import.md) |
10
- | 서버 헬스체크 | [api/health.md](api/health.md) |
11
- | 인스턴스 생성/설정 | [api/setup.md](api/setup.md) |
12
- | 인증 | [api/auth.md](api/auth.md) |
13
- | 트랜잭션 | [api/transaction.md](api/transaction.md) |
14
- | 엔티티 CRUD / 조회 | [api/entity.md](api/entity.md) |
15
- | 푸시 관련 | [api/push.md](api/push.md) |
16
- | 이메일 인증 / 변경 | [api/email.md](api/email.md) |
17
- | SMS | [api/sms.md](api/sms.md) |
18
- | SMTP 메일 | [api/smtp.md](api/smtp.md) |
19
- | 알림톡 / 친구톡 | [api/alimtalk.md](api/alimtalk.md) |
20
- | PG 결제 | [api/pg.md](api/pg.md) |
21
- | 파일 스토리지 | [api/file.md](api/file.md) |
22
- | 본인인증 | [api/identity.md](api/identity.md) |
23
- | QR코드 / 바코드 | [api/utils.md](api/utils.md) |
24
- | React Hook | [api/react.md](api/react.md) |
25
- | 암호화 패킷 처리 | [api/packet.md](api/packet.md) |
7
+ | 구분 | 파일 |
8
+ | ------------------ | ---------------------------------------- |
9
+ | import | [api/import.md](api/import.md) |
10
+ | 서버 헬스체크 | [api/health.md](api/health.md) |
11
+ | 인스턴스 생성/설정 | [api/setup.md](api/setup.md) |
12
+ | 인증 | [api/auth.md](api/auth.md) |
13
+ | 트랜잭션 | [api/transaction.md](api/transaction.md) |
14
+ | 엔티티 CRUD / 조회 | [api/entity.md](api/entity.md) |
15
+ | 푸시 관련 | [api/push.md](api/push.md) |
16
+ | 이메일 인증 / 변경 | [api/email.md](api/email.md) |
17
+ | SMS | [api/sms.md](api/sms.md) |
18
+ | SMTP 메일 | [api/smtp.md](api/smtp.md) |
19
+ | 알림톡 / 친구톡 | [api/alimtalk.md](api/alimtalk.md) |
20
+ | PG 결제 | [api/pg.md](api/pg.md) |
21
+ | 파일 스토리지 | [api/file.md](api/file.md) |
22
+ | 본인인증 | [api/identity.md](api/identity.md) |
23
+ | QR코드 / 바코드 | [api/utils.md](api/utils.md) |
24
+ | React Hook | [api/react.md](api/react.md) |
25
+ | 범용 HTTP 요청 | [api/request.md](api/request.md) |
26
+ | 암호화 패킷 처리 | [api/packet.md](api/packet.md) |
package/docs/react.md CHANGED
@@ -37,7 +37,7 @@ const { client, submit, del } = useEntityServer({
37
37
  });
38
38
 
39
39
  // 인증
40
- await client.login({ email: "hong@example.com", password: "pw" });
40
+ await client.login("hong@example.com", "pw");
41
41
  await client.logout();
42
42
 
43
43
  // 트랜잭션
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entity-server-client",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -177,6 +177,11 @@ export class EntityServerClientBase {
177
177
  };
178
178
  }
179
179
 
180
+ /**
181
+ * 임의 경로에 JSON 요청을 보냅니다. 응답이 JSON이면 파싱, octet-stream이면 자동 복호화합니다.
182
+ * `ok` 필드를 강제하지 않아 go서버/앱서버 신규 라우트 등 자유 응답 포맷에 사용합니다.
183
+ * `encryptRequests: true`이면 요청 바디도 자동 암호화됩니다.
184
+ */
180
185
  requestJson<T>(
181
186
  method: string,
182
187
  path: string,
@@ -195,6 +200,44 @@ export class EntityServerClientBase {
195
200
  );
196
201
  }
197
202
 
203
+ /**
204
+ * 임의 경로에 요청을 보내고 바이너리(ArrayBuffer)를 반환합니다.
205
+ * 이미지, PDF, 압축 파일 등 바이너리 응답이 오는 엔드포인트에 사용합니다.
206
+ */
207
+ requestBinary(
208
+ method: string,
209
+ path: string,
210
+ body?: unknown,
211
+ withAuth = true,
212
+ ): Promise<ArrayBuffer> {
213
+ return this._requestBinary(method, path, body, withAuth);
214
+ }
215
+
216
+ /**
217
+ * multipart/form-data 요청을 보냅니다. 파일 업로드 등에 사용합니다.
218
+ * 응답은 JSON으로 파싱하여 반환합니다.
219
+ */
220
+ requestForm<T>(
221
+ method: string,
222
+ path: string,
223
+ form: FormData,
224
+ withAuth = true,
225
+ ): Promise<T> {
226
+ return this._requestForm<T>(method, path, form, withAuth);
227
+ }
228
+
229
+ /**
230
+ * multipart/form-data 요청을 보내고 바이너리(ArrayBuffer)를 반환합니다.
231
+ */
232
+ requestFormBinary(
233
+ method: string,
234
+ path: string,
235
+ form: FormData,
236
+ withAuth = true,
237
+ ): Promise<ArrayBuffer> {
238
+ return this._requestFormBinary(method, path, form, withAuth);
239
+ }
240
+
198
241
  _request<T>(
199
242
  method: string,
200
243
  path: string,