entity-server-client 0.2.5 → 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.
Files changed (50) hide show
  1. package/README.md +32 -1
  2. package/build.mjs +11 -1
  3. package/docs/api/alimtalk.md +62 -0
  4. package/docs/api/auth.md +256 -0
  5. package/docs/api/email.md +37 -0
  6. package/docs/api/entity.md +273 -0
  7. package/docs/api/file.md +80 -0
  8. package/docs/api/health.md +41 -0
  9. package/docs/api/identity.md +32 -0
  10. package/docs/api/import.md +34 -0
  11. package/docs/api/packet.md +25 -0
  12. package/docs/api/pg.md +90 -0
  13. package/docs/api/push.md +107 -0
  14. package/docs/api/react.md +141 -0
  15. package/docs/api/setup.md +43 -0
  16. package/docs/api/sms.md +45 -0
  17. package/docs/api/smtp.md +33 -0
  18. package/docs/api/transaction.md +50 -0
  19. package/docs/api/utils.md +52 -0
  20. package/docs/apis.md +22 -787
  21. package/docs/react.md +64 -7
  22. package/package.json +7 -1
  23. package/src/EntityServerClient.ts +28 -546
  24. package/src/client/base.ts +305 -0
  25. package/src/client/packet.ts +16 -52
  26. package/src/client/request.ts +46 -9
  27. package/src/client/utils.ts +17 -2
  28. package/src/hooks/useEntityServer.ts +3 -4
  29. package/src/mixins/auth.ts +143 -0
  30. package/src/mixins/entity.ts +205 -0
  31. package/src/mixins/file.ts +99 -0
  32. package/src/mixins/push.ts +109 -0
  33. package/src/mixins/smtp.ts +20 -0
  34. package/src/mixins/utils.ts +106 -0
  35. package/src/packet.ts +84 -0
  36. package/src/types.ts +93 -1
  37. package/tests/packet.test.mjs +50 -0
  38. package/dist/EntityServerClient.d.ts +0 -203
  39. package/dist/client/hmac.d.ts +0 -8
  40. package/dist/client/packet.d.ts +0 -22
  41. package/dist/client/request.d.ts +0 -16
  42. package/dist/client/utils.d.ts +0 -4
  43. package/dist/hooks/useEntityServer.d.ts +0 -63
  44. package/dist/index.d.ts +0 -4
  45. package/dist/index.js +0 -2
  46. package/dist/index.js.map +0 -7
  47. package/dist/react.d.ts +0 -1
  48. package/dist/react.js +0 -2
  49. package/dist/react.js.map +0 -7
  50. package/dist/types.d.ts +0 -165
package/src/packet.ts ADDED
@@ -0,0 +1,84 @@
1
+ // @ts-ignore
2
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha";
3
+ // @ts-ignore
4
+ import { sha256 } from "@noble/hashes/sha2";
5
+ // @ts-ignore
6
+ import { hkdf } from "@noble/hashes/hkdf";
7
+
8
+ export const PACKET_KEY_SIZE = 32;
9
+ export const PACKET_MAGIC_MIN = 2;
10
+ export const PACKET_MAGIC_RANGE = 14;
11
+ export const PACKET_NONCE_SIZE = 24;
12
+ export const PACKET_TAG_SIZE = 16;
13
+ export const PACKET_HKDF_SALT = "entity-server:hkdf:v1";
14
+ export const PACKET_INFO_LABEL = "entity-server:packet-encryption";
15
+
16
+ function toUint8Array(data: ArrayBuffer | Uint8Array): Uint8Array {
17
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
18
+ }
19
+
20
+ export function derivePacketKey(
21
+ source: string,
22
+ infoLabel = PACKET_INFO_LABEL,
23
+ ): Uint8Array {
24
+ return hkdf(
25
+ sha256,
26
+ new TextEncoder().encode(source),
27
+ new TextEncoder().encode(PACKET_HKDF_SALT),
28
+ new TextEncoder().encode(infoLabel),
29
+ PACKET_KEY_SIZE,
30
+ );
31
+ }
32
+
33
+ export function packetMagicLenFromKey(
34
+ key: ArrayBuffer | Uint8Array,
35
+ magicMin = PACKET_MAGIC_MIN,
36
+ magicRange = PACKET_MAGIC_RANGE,
37
+ ): number {
38
+ const keyBytes = toUint8Array(key);
39
+ if (keyBytes.length < PACKET_KEY_SIZE) return magicMin;
40
+ return magicMin + (keyBytes[PACKET_KEY_SIZE - 1]! % magicRange);
41
+ }
42
+
43
+ export function encryptPacket(
44
+ plaintext: ArrayBuffer | Uint8Array,
45
+ key: ArrayBuffer | Uint8Array,
46
+ magicMin = PACKET_MAGIC_MIN,
47
+ magicRange = PACKET_MAGIC_RANGE,
48
+ ): Uint8Array {
49
+ const plaintextBytes = toUint8Array(plaintext);
50
+ const keyBytes = toUint8Array(key);
51
+ const magicLen = packetMagicLenFromKey(keyBytes, magicMin, magicRange);
52
+ const magic = crypto.getRandomValues(new Uint8Array(magicLen));
53
+ const nonce = crypto.getRandomValues(new Uint8Array(PACKET_NONCE_SIZE));
54
+ const cipher = xchacha20poly1305(keyBytes, nonce);
55
+ const ciphertext = cipher.encrypt(plaintextBytes);
56
+ const result = new Uint8Array(
57
+ magicLen + PACKET_NONCE_SIZE + ciphertext.length,
58
+ );
59
+
60
+ result.set(magic, 0);
61
+ result.set(nonce, magicLen);
62
+ result.set(ciphertext, magicLen + PACKET_NONCE_SIZE);
63
+ return result;
64
+ }
65
+
66
+ export function decryptPacket(
67
+ buffer: ArrayBuffer | Uint8Array,
68
+ key: ArrayBuffer | Uint8Array,
69
+ magicMin = PACKET_MAGIC_MIN,
70
+ magicRange = PACKET_MAGIC_RANGE,
71
+ ): Uint8Array {
72
+ const data = toUint8Array(buffer);
73
+ const keyBytes = toUint8Array(key);
74
+ const magicLen = packetMagicLenFromKey(keyBytes, magicMin, magicRange);
75
+
76
+ if (data.length < magicLen + PACKET_NONCE_SIZE + PACKET_TAG_SIZE) {
77
+ throw new Error("Encrypted packet too short");
78
+ }
79
+
80
+ const nonce = data.slice(magicLen, magicLen + PACKET_NONCE_SIZE);
81
+ const ciphertext = data.slice(magicLen + PACKET_NONCE_SIZE);
82
+ const cipher = xchacha20poly1305(keyBytes, nonce);
83
+ return cipher.decrypt(ciphertext);
84
+ }
package/src/types.ts CHANGED
@@ -129,7 +129,11 @@ export interface RegisterPushDeviceOptions {
129
129
  export interface EntityServerClientOptions {
130
130
  baseUrl?: string;
131
131
  token?: string;
132
- packetMagicLen?: number;
132
+ /**
133
+ * 익명 패킷 암호화용 부트스트랩 토큰입니다.
134
+ * entity-app-server의 `/v1/health` 응답으로 설정되는 용도입니다.
135
+ */
136
+ anonymousPacketToken?: string;
133
137
  /**
134
138
  * `true`이면 인증된 POST/PUT 요청 바디를 XChaCha20-Poly1305로 암호화합니다.
135
139
  *
@@ -184,3 +188,91 @@ export interface EntityServerClientOptions {
184
188
  */
185
189
  hmacSecret?: string;
186
190
  }
191
+
192
+ // ─── SMTP ─────────────────────────────────────────────────────────────────────
193
+
194
+ /** `smtpSend()` 요청 파라미터입니다. */
195
+ export interface SmtpSendRequest {
196
+ /** provider 식별자 (생략 시 기본 provider 사용) */
197
+ provider?: string;
198
+ from?: string;
199
+ to: string[];
200
+ cc?: string[];
201
+ bcc?: string[];
202
+ subject?: string;
203
+ body_text?: string;
204
+ body_html?: string;
205
+ /** 이메일 템플릿 이름 */
206
+ template_name?: string;
207
+ /** 템플릿 변수 */
208
+ template_data?: Record<string, unknown>;
209
+ /** 첨부 파일 seq 배열 */
210
+ attachments?: number[];
211
+ reply_to?: string;
212
+ ref_entity?: string;
213
+ ref_seq?: number;
214
+ }
215
+
216
+ // ─── Utils ────────────────────────────────────────────────────────────────────
217
+
218
+ /** `qrcode()` / `qrcodeBase64()` / `qrcodeText()` 공통 옵션 */
219
+ export interface QRCodeOptions {
220
+ /** PNG 크기 픽셀 (기본 256, 최대 2048) */
221
+ size?: number;
222
+ /** 오류 복구 수준 (기본 `"medium"`) */
223
+ error_correction?: "low" | "medium" | "high" | "highest";
224
+ /** 전경색 hex (기본 `"#000000"`) */
225
+ fg_color?: string;
226
+ /** 배경색 hex (기본 `"#ffffff"`) */
227
+ bg_color?: string;
228
+ }
229
+
230
+ /** `barcode()` 옵션 */
231
+ export interface BarcodeOptions {
232
+ /** 바코드 타입 (기본 `"code128"`) */
233
+ type?:
234
+ | "code128"
235
+ | "code39"
236
+ | "ean13"
237
+ | "ean8"
238
+ | "codabar"
239
+ | "datamatrix"
240
+ | "itf";
241
+ /** 너비 픽셀 (기본 300, 최대 2048) */
242
+ width?: number;
243
+ /** 높이 픽셀 (기본 100, 최대 2048) */
244
+ height?: number;
245
+ }
246
+
247
+ /** `pdf2png()` 옵션 */
248
+ export interface Pdf2PngOptions {
249
+ /** 해상도 DPI (기본 300) */
250
+ dpi?: number;
251
+ /** 시작 페이지 1-based (기본: 첫 번째 페이지) */
252
+ firstPage?: number;
253
+ /** 종료 페이지 1-based (기본: 마지막 페이지) */
254
+ lastPage?: number;
255
+ }
256
+
257
+ // ─── 파일 ─────────────────────────────────────────────────────────────────────
258
+
259
+ /** 파일 메타 정보 */
260
+ export interface FileMeta {
261
+ uuid: string;
262
+ original_name: string;
263
+ size: number;
264
+ mime_type: string;
265
+ entity: string;
266
+ ref_seq?: number;
267
+ is_public?: boolean;
268
+ created_time: string;
269
+ url?: string;
270
+ }
271
+
272
+ /** `fileUpload()` 옵션 */
273
+ export interface FileUploadOptions {
274
+ /** 파일에 연결할 ref_seq */
275
+ refSeq?: number;
276
+ /** 공개 파일 여부 (기본 서버 설정 따름) */
277
+ isPublic?: boolean;
278
+ }
@@ -0,0 +1,50 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ derivePacketKey,
6
+ packetMagicLenFromKey,
7
+ encryptPacket,
8
+ decryptPacket,
9
+ PACKET_MAGIC_MIN,
10
+ PACKET_MAGIC_RANGE,
11
+ PACKET_NONCE_SIZE,
12
+ PACKET_TAG_SIZE,
13
+ } from "../dist/packet.js";
14
+
15
+ test("packet core roundtrips plaintext", () => {
16
+ const key = derivePacketKey("packet-test-token");
17
+ const payload = new TextEncoder().encode(
18
+ JSON.stringify({ ok: true, message: "hello" }),
19
+ );
20
+
21
+ const encrypted = encryptPacket(payload, key);
22
+ const decrypted = decryptPacket(encrypted, key);
23
+
24
+ assert.equal(
25
+ new TextDecoder().decode(decrypted),
26
+ JSON.stringify({ ok: true, message: "hello" }),
27
+ );
28
+ });
29
+
30
+ test("packet core derives a stable magic length from key", () => {
31
+ const key = derivePacketKey("packet-test-token");
32
+ const magicLen = packetMagicLenFromKey(key);
33
+
34
+ assert.ok(magicLen >= PACKET_MAGIC_MIN);
35
+ assert.ok(magicLen < PACKET_MAGIC_MIN + PACKET_MAGIC_RANGE);
36
+ assert.equal(magicLen, packetMagicLenFromKey(key));
37
+ });
38
+
39
+ test("encrypted packet has expected minimum shape", () => {
40
+ const key = derivePacketKey("packet-test-token");
41
+ const payload = new TextEncoder().encode("abc");
42
+ const magicLen = packetMagicLenFromKey(key);
43
+
44
+ const encrypted = encryptPacket(payload, key);
45
+
46
+ assert.ok(
47
+ encrypted.length >=
48
+ magicLen + PACKET_NONCE_SIZE + PACKET_TAG_SIZE + payload.length,
49
+ );
50
+ });
@@ -1,203 +0,0 @@
1
- import type { EntityHistoryRecord, EntityListParams, EntityListResult, EntityQueryRequest, EntityServerClientOptions, RegisterPushDeviceOptions } from "./types";
2
- export declare class EntityServerClient {
3
- private baseUrl;
4
- private token;
5
- private apiKey;
6
- private hmacSecret;
7
- private packetMagicLen;
8
- private encryptRequests;
9
- private activeTxId;
10
- private keepSession;
11
- private refreshBuffer;
12
- private onTokenRefreshed?;
13
- private onSessionExpired?;
14
- private _sessionRefreshToken;
15
- private _refreshTimer;
16
- /**
17
- * EntityServerClient 인스턴스를 생성합니다.
18
- *
19
- * 기본값:
20
- * - `baseUrl`: `VITE_ENTITY_SERVER_URL` 또는 `http://localhost:47200`
21
- * - `packetMagicLen`: `VITE_ENTITY_SERVER_PACKET_MAGIC_LEN` 또는 `4`
22
- */
23
- constructor(options?: EntityServerClientOptions);
24
- /** baseUrl, token, packetMagicLen, encryptRequests 값을 런타임에 갱신합니다. */
25
- configure(options: Partial<EntityServerClientOptions>): void;
26
- /** 인증 요청에 사용할 JWT Access Token을 설정합니다. */
27
- setToken(token: string): void;
28
- /** HMAC 인증용 API Key를 설정합니다. */
29
- setApiKey(apiKey: string): void;
30
- /** HMAC 인증용 시크릿을 설정합니다. */
31
- setHmacSecret(secret: string): void;
32
- /** 암호화 패킷 magic 길이(`packet_magic_len`)를 설정합니다. */
33
- setPacketMagicLen(length: number): void;
34
- /** 현재 암호화 패킷 magic 길이를 반환합니다. */
35
- getPacketMagicLen(): number;
36
- /** @internal 자동 토큰 갱신 타이머를 시작합니다. */
37
- private _scheduleKeepSession;
38
- /** @internal 자동 갱신 타이머를 정리합니다. */
39
- private _clearRefreshTimer;
40
- /**
41
- * 세션 유지 타이머를 중지합니다.
42
- * `logout()` 호출 시 자동으로 중지되며, 직접 호출이 필요한 경우는 드뭅니다.
43
- */
44
- stopKeepSession(): void;
45
- /**
46
- * 서버 헬스 체크를 수행하고 패킷 암호화 활성 여부를 자동으로 감지합니다.
47
- *
48
- * 서버가 `packet_encryption: true`를 응답하면 이후 모든 요청에 암호화가 자동 적용됩니다.
49
- *
50
- * ```ts
51
- * await client.checkHealth();
52
- * await client.login(email, password);
53
- * ```
54
- */
55
- checkHealth(): Promise<{
56
- ok: boolean;
57
- packet_encryption?: boolean;
58
- }>;
59
- /** 로그인 후 `access_token`을 내부 상태에 저장합니다. */
60
- login(email: string, password: string): Promise<{
61
- access_token: string;
62
- refresh_token: string;
63
- expires_in: number;
64
- }>;
65
- /** Refresh Token으로 Access Token을 재발급받아 내부 토큰을 교체합니다. */
66
- refreshToken(refreshToken: string): Promise<{
67
- access_token: string;
68
- expires_in: number;
69
- }>;
70
- /**
71
- * 서버에 로그아웃을 요청하고 내부 토큰을 초기화합니다.
72
- * refresh_token을 서버에 전달해 무효화합니다.
73
- */
74
- logout(refreshToken: string): Promise<{
75
- ok: boolean;
76
- }>;
77
- /** 트랜잭션을 시작하고 활성 트랜잭션 ID를 저장합니다. */
78
- transStart(): Promise<string>;
79
- /** 활성 트랜잭션(또는 전달된 transactionId)을 롤백합니다. */
80
- transRollback(transactionId?: string): Promise<{
81
- ok: boolean;
82
- }>;
83
- /**
84
- * 활성 트랜잭션(또는 전달된 transactionId)을 커밋합니다.
85
- *
86
- * @returns `results` 배열: commit된 각 작업의 `entity`, `action`, `seq`
87
- */
88
- transCommit(transactionId?: string): Promise<{
89
- ok: boolean;
90
- results: Array<{
91
- entity: string;
92
- action: string;
93
- seq: number;
94
- }>;
95
- }>;
96
- /** 시퀀스 ID로 엔티티 단건을 조회합니다. */
97
- get<T = unknown>(entity: string, seq: number, opts?: {
98
- skipHooks?: boolean;
99
- }): Promise<{
100
- ok: boolean;
101
- data: T;
102
- }>;
103
- /** 조건으로 엔티티 단건을 조회합니다. data 컬럼을 완전히 복호화하여 반환합니다. */
104
- find<T = unknown>(entity: string, conditions?: Record<string, unknown>, opts?: {
105
- skipHooks?: boolean;
106
- }): Promise<{
107
- ok: boolean;
108
- data: T;
109
- }>;
110
- /** 페이지네이션/정렬/필터 조건으로 엔티티 목록을 조회합니다. */
111
- list<T = unknown>(entity: string, params?: EntityListParams): Promise<{
112
- ok: boolean;
113
- data: EntityListResult<T>;
114
- }>;
115
- /**
116
- * 엔티티 총 건수를 조회합니다.
117
- *
118
- * @param conditions 필터 조건 (예: `{ status: "active" }`)
119
- */
120
- count(entity: string, conditions?: Record<string, unknown>): Promise<{
121
- ok: boolean;
122
- count: number;
123
- }>;
124
- /**
125
- * 커스텀 SQL로 엔티티를 조회합니다.
126
- *
127
- * SELECT 전용이며 인덱스 테이블만 조회 가능합니다. JOIN 지원.
128
- */
129
- query<T = unknown>(entity: string, req: EntityQueryRequest): Promise<{
130
- ok: boolean;
131
- data: {
132
- items: T[];
133
- count: number;
134
- };
135
- }>;
136
- /** 엔티티 데이터를 생성/수정(Submit)합니다. `seq`가 없으면 INSERT, 있으면 UPDATE입니다. */
137
- submit(entity: string, data: Record<string, unknown>, opts?: {
138
- transactionId?: string;
139
- skipHooks?: boolean;
140
- }): Promise<{
141
- ok: boolean;
142
- seq: number;
143
- }>;
144
- /** 시퀀스 ID로 엔티티를 삭제합니다(`hard=true`면 하드 삭제, 기본은 소프트 삭제). */
145
- delete(entity: string, seq: number, opts?: {
146
- transactionId?: string;
147
- hard?: boolean;
148
- skipHooks?: boolean;
149
- }): Promise<{
150
- ok: boolean;
151
- deleted: number;
152
- }>;
153
- /** 엔티티 단건의 변경 이력을 조회합니다. */
154
- history<T = unknown>(entity: string, seq: number, params?: Pick<EntityListParams, "page" | "limit">): Promise<{
155
- ok: boolean;
156
- data: EntityListResult<EntityHistoryRecord<T>>;
157
- }>;
158
- /** 특정 이력 시점으로 엔티티를 롤백합니다. */
159
- rollback(entity: string, historySeq: number): Promise<{
160
- ok: boolean;
161
- }>;
162
- /** 푸시 관련 엔티티로 payload를 전송(Submit)합니다. */
163
- push(pushEntity: string, payload: Record<string, unknown>, opts?: {
164
- transactionId?: string;
165
- }): Promise<{
166
- ok: boolean;
167
- seq: number;
168
- }>;
169
- /** 푸시 로그 엔티티 목록을 조회합니다. */
170
- pushLogList<T = unknown>(params?: EntityListParams): Promise<{
171
- ok: boolean;
172
- data: EntityListResult<T>;
173
- }>;
174
- /** 계정의 푸시 디바이스를 등록합니다. */
175
- registerPushDevice(accountSeq: number, deviceId: string, pushToken: string, opts?: RegisterPushDeviceOptions): Promise<{
176
- ok: boolean;
177
- seq: number;
178
- }>;
179
- /** 디바이스 레코드의 푸시 토큰을 갱신합니다. */
180
- updatePushDeviceToken(deviceSeq: number, pushToken: string, opts?: {
181
- pushEnabled?: boolean;
182
- transactionId?: string;
183
- }): Promise<{
184
- ok: boolean;
185
- seq: number;
186
- }>;
187
- /** 디바이스의 푸시 수신을 비활성화합니다. */
188
- disablePushDevice(deviceSeq: number, opts?: {
189
- transactionId?: string;
190
- }): Promise<{
191
- ok: boolean;
192
- seq: number;
193
- }>;
194
- /**
195
- * 요청 바디를 파싱합니다.
196
- * `application/octet-stream`이면 XChaCha20-Poly1305 복호화, 그 외는 JSON 파싱합니다.
197
- *
198
- * @param requireEncrypted `true`이면 암호화된 요청만 허용합니다.
199
- */
200
- readRequestBody<T = Record<string, unknown>>(body: ArrayBuffer | Uint8Array | string | T | null | undefined, contentType?: string, requireEncrypted?: boolean): T;
201
- private get _reqOpts();
202
- private request;
203
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * HMAC-SHA256 서명 헤더를 생성합니다.
3
- *
4
- * 서명 대상: `METHOD|PATH|TIMESTAMP|NONCE|BODY`
5
- *
6
- * @returns `X-API-Key`, `X-Timestamp`, `X-Nonce`, `X-Signature` 헤더 객체
7
- */
8
- export declare function buildHmacHeaders(method: string, path: string, bodyBytes: Uint8Array, apiKey: string, hmacSecret: string): Record<string, string>;
@@ -1,22 +0,0 @@
1
- /**
2
- * 패킷 암호화 키를 유도합니다.
3
- * - HMAC 모드 (`hmacSecret` 유효 시): HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
4
- * - JWT 모드: SHA-256(jwt_token)
5
- */
6
- export declare function derivePacketKey(hmacSecret: string, token: string): Uint8Array;
7
- /**
8
- * 평문 바이트를 XChaCha20-Poly1305로 암호화합니다.
9
- * 포맷: [random_magic:magicLen][random_nonce:24][ciphertext+tag]
10
- */
11
- export declare function encryptPacket(plaintext: Uint8Array, key: Uint8Array, magicLen: number): Uint8Array;
12
- /**
13
- * XChaCha20-Poly1305 패킷을 복호화해 JSON 객체로 변환합니다.
14
- * 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
15
- */
16
- export declare function decryptPacket<T>(buffer: ArrayBuffer, key: Uint8Array, magicLen: number): T;
17
- /**
18
- * 요청 바디를 파싱합니다. `application/octet-stream`이면 복호화, 그 외는 JSON 파싱합니다.
19
- *
20
- * @param requireEncrypted `true`이면 암호화된 요청만 허용합니다.
21
- */
22
- export declare function parseRequestBody<T>(body: ArrayBuffer | Uint8Array | string | T | null | undefined, contentType: string, requireEncrypted: boolean, key: Uint8Array, magicLen: number): T;
@@ -1,16 +0,0 @@
1
- export interface RequestOptions {
2
- baseUrl: string;
3
- token: string;
4
- apiKey: string;
5
- hmacSecret: string;
6
- packetMagicLen: number;
7
- encryptRequests: boolean;
8
- }
9
- /**
10
- * Entity Server에 HTTP 요청을 보냅니다.
11
- *
12
- * - `encryptRequests` 활성화 시 인증된 POST 바디를 자동 암호화합니다.
13
- * - 응답이 `application/octet-stream`이면 자동 복호화합니다.
14
- * - JSON 응답의 `ok`가 false이면 에러를 던집니다.
15
- */
16
- export declare function entityRequest<T>(opts: RequestOptions, method: string, path: string, body?: unknown, withAuth?: boolean, extraHeaders?: Record<string, string>): Promise<T>;
@@ -1,4 +0,0 @@
1
- /** Vite 환경변수(`import.meta.env`)에서 값을 읽습니다. */
2
- export declare function readEnv(name: string): string | undefined;
3
- /** 쿼리 파라미터 객체를 URL 쿼리 문자열로 변환합니다. `orderBy` 키는 `order_by`로 변환됩니다. */
4
- export declare function buildQuery(params: Record<string, unknown>): string;
@@ -1,63 +0,0 @@
1
- import { EntityServerClient, type EntityQueryRequest, type EntityServerClientOptions } from "../index";
2
- export interface UseEntityServerOptions extends EntityServerClientOptions {
3
- singleton?: boolean;
4
- tokenResolver?: () => string | undefined | null;
5
- /**
6
- * 페이지 새로고침 후 로그인 상태를 복원할 때 사용합니다.
7
- * 이 값이 있으면 마운트 시 `client.refreshToken()`을 호출해 새 access_token을 발급받습니다.
8
- * `keepSession: true`와 함께 사용하면 세션 유지 타이머도 재시작됩니다.
9
- * 갱신 성공 시 `onTokenRefreshed` 콜백이 호출됩니다.
10
- */
11
- resumeSession?: string;
12
- }
13
- export interface UseEntityServerResult {
14
- /** EntityServerClient 인스턴스 (read 전용 메서드 직접 호출 시 사용) */
15
- client: EntityServerClient;
16
- /** submit 또는 delete 진행 중 여부 */
17
- isPending: boolean;
18
- /** 마지막 mutation 에러 (없으면 null) */
19
- error: Error | null;
20
- /** 에러·결과 상태 초기화 */
21
- reset: () => void;
22
- /** entity 데이터 생성/수정 (seq 없으면 INSERT, 있으면 UPDATE) */
23
- submit: (entity: string, data: Record<string, unknown>, opts?: {
24
- transactionId?: string;
25
- skipHooks?: boolean;
26
- }) => Promise<{
27
- ok: boolean;
28
- seq: number;
29
- }>;
30
- /** entity 데이터 삭제 */
31
- del: (entity: string, seq: number, opts?: {
32
- transactionId?: string;
33
- hard?: boolean;
34
- skipHooks?: boolean;
35
- }) => Promise<{
36
- ok: boolean;
37
- deleted: number;
38
- }>;
39
- /** 커스텀 SQL 조회 */
40
- query: <T = unknown>(entity: string, req: EntityQueryRequest) => Promise<{
41
- ok: boolean;
42
- data: {
43
- items: T[];
44
- count: number;
45
- };
46
- }>;
47
- }
48
- /**
49
- * React 환경에서 EntityServerClient 인스턴스와 mutation 상태를 반환합니다.
50
- *
51
- * - `singleton=true`(기본): 패키지 전역 `entityServer` 인스턴스를 사용합니다.
52
- * - `singleton=false`: 컴포넌트 스코프의 새 인스턴스를 생성합니다.
53
- *
54
- * @example
55
- * ```tsx
56
- * const { submit, del, isPending, error, reset } = useEntityServer();
57
- *
58
- * const handleSave = async () => {
59
- * await submit("account", { name: "홍길동" });
60
- * };
61
- * ```
62
- */
63
- export declare function useEntityServer(options?: UseEntityServerOptions): UseEntityServerResult;
package/dist/index.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from "./types";
2
- export * from "./EntityServerClient";
3
- import { EntityServerClient } from "./EntityServerClient";
4
- export declare const entityServer: EntityServerClient;
package/dist/index.js DELETED
@@ -1,2 +0,0 @@
1
- function v(s){return import.meta?.env?.[s]}function R(s){return Object.entries(s).filter(([,e])=>e!=null).map(([e,t])=>`${encodeURIComponent(e==="orderBy"?"order_by":e)}=${encodeURIComponent(String(t))}`).join("&")}import{xchacha20poly1305 as E}from"@noble/ciphers/chacha";import{sha256 as S}from"@noble/hashes/sha2";import{hkdf as I}from"@noble/hashes/hkdf";function f(s,e){if(s){let t=new TextEncoder().encode("entity-server:hkdf:v1"),r=new TextEncoder().encode("entity-server:packet-encryption");return I(S,new TextEncoder().encode(s),t,r,32)}return S(new TextEncoder().encode(e))}function _(s,e,t){let r=new Uint8Array(t),n=new Uint8Array(24);crypto.getRandomValues(r),crypto.getRandomValues(n);let a=E(e,n).encrypt(s),o=new Uint8Array(t+24+a.length);return o.set(r,0),o.set(n,t),o.set(a,t+24),o}function k(s,e,t){let r=new Uint8Array(s);if(r.length<t+24+16)throw new Error("Encrypted packet too short");let n=r.slice(t,t+24),i=r.slice(t+24),o=E(e,n).decrypt(i);return JSON.parse(new TextDecoder().decode(o))}function P(s,e,t,r,n){let i=e.toLowerCase().includes("application/octet-stream");if(t&&!i)throw new Error("Encrypted request required: Content-Type must be application/octet-stream");if(i){if(s==null)throw new Error("Encrypted request body is empty");if(s instanceof ArrayBuffer)return k(s,r,n);if(s instanceof Uint8Array){let a=s.buffer.slice(s.byteOffset,s.byteOffset+s.byteLength);return k(a,r,n)}throw new Error("Encrypted request body must be ArrayBuffer or Uint8Array")}return s==null||s===""?{}:typeof s=="string"?JSON.parse(s):s}import{sha256 as U}from"@noble/hashes/sha2";import{hmac as $}from"@noble/hashes/hmac";function q(s,e,t,r,n){let i=String(Math.floor(Date.now()/1e3)),a=crypto.randomUUID(),o=new TextEncoder().encode(`${s}|${e}|${i}|${a}|`),c=new Uint8Array(o.length+t.length);c.set(o,0),c.set(t,o.length);let l=[...$(U,new TextEncoder().encode(n),c)].map(g=>g.toString(16).padStart(2,"0")).join("");return{"X-API-Key":r,"X-Timestamp":i,"X-Nonce":a,"X-Signature":l}}async function w(s,e,t,r,n=!0,i={}){let{baseUrl:a,token:o,apiKey:c,hmacSecret:p,packetMagicLen:l,encryptRequests:g}=s,T=n&&!!(c&&p),y={"Content-Type":"application/json",...i};!T&&n&&o&&(y.Authorization=`Bearer ${o}`);let u=null;if(r!=null)if(g&&n&&(o||T)&&e!=="GET"&&e!=="HEAD"){let x=f(p,o);u=_(new TextEncoder().encode(JSON.stringify(r)),x,l),y["Content-Type"]="application/octet-stream"}else u=JSON.stringify(r);if(T){let h=u instanceof Uint8Array?u:typeof u=="string"?new TextEncoder().encode(u):new Uint8Array(0);Object.assign(y,q(e,t,h,c,p))}let d=await fetch(a+t,{method:e,headers:y,...u!=null?{body:u}:{}});if((d.headers.get("Content-Type")??"").includes("application/octet-stream")){let h=f(p,o);return k(await d.arrayBuffer(),h,l)}let b=await d.json();if(!b.ok){let h=new Error(b.message??`EntityServer error (HTTP ${d.status})`);throw h.status=d.status,h}return b}var m=class{baseUrl;token;apiKey;hmacSecret;packetMagicLen;encryptRequests;activeTxId=null;keepSession;refreshBuffer;onTokenRefreshed;onSessionExpired;_sessionRefreshToken=null;_refreshTimer=null;constructor(e={}){let t=v("VITE_ENTITY_SERVER_URL"),r=v("VITE_ENTITY_SERVER_PACKET_MAGIC_LEN");this.baseUrl=(e.baseUrl??t??"http://localhost:47200").replace(/\/$/,""),this.token=e.token??"",this.apiKey=e.apiKey??"",this.hmacSecret=e.hmacSecret??"",this.packetMagicLen=e.packetMagicLen??(r?Number(r):4),this.encryptRequests=e.encryptRequests??!1,this.keepSession=e.keepSession??!1,this.refreshBuffer=e.refreshBuffer??60,this.onTokenRefreshed=e.onTokenRefreshed,this.onSessionExpired=e.onSessionExpired}configure(e){e.baseUrl&&(this.baseUrl=e.baseUrl.replace(/\/$/,"")),typeof e.token=="string"&&(this.token=e.token),typeof e.packetMagicLen=="number"&&(this.packetMagicLen=e.packetMagicLen),typeof e.encryptRequests=="boolean"&&(this.encryptRequests=e.encryptRequests),typeof e.apiKey=="string"&&(this.apiKey=e.apiKey),typeof e.hmacSecret=="string"&&(this.hmacSecret=e.hmacSecret),typeof e.keepSession=="boolean"&&(this.keepSession=e.keepSession),typeof e.refreshBuffer=="number"&&(this.refreshBuffer=e.refreshBuffer),e.onTokenRefreshed&&(this.onTokenRefreshed=e.onTokenRefreshed),e.onSessionExpired&&(this.onSessionExpired=e.onSessionExpired)}setToken(e){this.token=e}setApiKey(e){this.apiKey=e}setHmacSecret(e){this.hmacSecret=e}setPacketMagicLen(e){this.packetMagicLen=e}getPacketMagicLen(){return this.packetMagicLen}_scheduleKeepSession(e,t){this._clearRefreshTimer(),this._sessionRefreshToken=e;let r=Math.max((t-this.refreshBuffer)*1e3,0);this._refreshTimer=setTimeout(async()=>{if(this._sessionRefreshToken)try{let n=await this.refreshToken(this._sessionRefreshToken);this.onTokenRefreshed?.(n.access_token,n.expires_in),this._scheduleKeepSession(this._sessionRefreshToken,n.expires_in)}catch(n){this._clearRefreshTimer(),this.onSessionExpired?.(n instanceof Error?n:new Error(String(n)))}},r)}_clearRefreshTimer(){this._refreshTimer!==null&&(clearTimeout(this._refreshTimer),this._refreshTimer=null)}stopKeepSession(){this._clearRefreshTimer(),this._sessionRefreshToken=null}async checkHealth(){let t=await(await fetch(`${this.baseUrl}/v1/health`,{signal:AbortSignal.timeout(3e3)})).json();return t.packet_encryption&&(this.encryptRequests=!0),t}async login(e,t){let r=await this.request("POST","/v1/auth/login",{email:e,passwd:t},!1);return this.token=r.data.access_token,this.keepSession&&this._scheduleKeepSession(r.data.refresh_token,r.data.expires_in),r.data}async refreshToken(e){let t=await this.request("POST","/v1/auth/refresh",{refresh_token:e},!1);return this.token=t.data.access_token,this.keepSession&&this._scheduleKeepSession(e,t.data.expires_in),t.data}async logout(e){this.stopKeepSession();let t=await this.request("POST","/v1/auth/logout",{refresh_token:e},!1);return this.token="",t}async transStart(){let e=await this.request("POST","/v1/transaction/start",void 0,!1);return this.activeTxId=e.transaction_id,this.activeTxId}transRollback(e){let t=e??this.activeTxId;return t?(this.activeTxId=null,this.request("POST",`/v1/transaction/rollback/${t}`)):Promise.reject(new Error("No active transaction. Call transStart() first."))}transCommit(e){let t=e??this.activeTxId;return t?(this.activeTxId=null,this.request("POST",`/v1/transaction/commit/${t}`)):Promise.reject(new Error("No active transaction. Call transStart() first."))}get(e,t,r={}){let n=r.skipHooks?"?skipHooks=true":"";return this.request("GET",`/v1/entity/${e}/${t}${n}`)}find(e,t,r={}){let n=r.skipHooks?"?skipHooks=true":"";return this.request("POST",`/v1/entity/${e}/find${n}`,t??{})}list(e,t={}){let{conditions:r,fields:n,orderDir:i,orderBy:a,...o}=t,c={page:1,limit:20,...o};return a&&(c.orderBy=i==="DESC"?`-${a}`:a),n?.length&&(c.fields=n.join(",")),this.request("POST",`/v1/entity/${e}/list?${R(c)}`,r??{})}count(e,t){return this.request("POST",`/v1/entity/${e}/count`,t??{})}query(e,t){return this.request("POST",`/v1/entity/${e}/query`,t)}submit(e,t,r={}){let n=r.transactionId??this.activeTxId,i=n?{"X-Transaction-ID":n}:void 0,a=r.skipHooks?"?skipHooks=true":"";return this.request("POST",`/v1/entity/${e}/submit${a}`,t,!0,i)}delete(e,t,r={}){let n=new URLSearchParams;r.hard&&n.set("hard","true"),r.skipHooks&&n.set("skipHooks","true");let i=n.size?`?${n}`:"",a=r.transactionId??this.activeTxId,o=a?{"X-Transaction-ID":a}:void 0;return this.request("POST",`/v1/entity/${e}/delete/${t}${i}`,void 0,!0,o)}history(e,t,r={}){return this.request("GET",`/v1/entity/${e}/history/${t}?${R({page:1,limit:50,...r})}`)}rollback(e,t){return this.request("POST",`/v1/entity/${e}/rollback/${t}`)}push(e,t,r={}){return this.submit(e,t,r)}pushLogList(e={}){return this.list("push_log",e)}registerPushDevice(e,t,r,n={}){let{platform:i,deviceType:a,browser:o,browserVersion:c,pushEnabled:p=!0,transactionId:l}=n;return this.submit("account_device",{id:t,account_seq:e,push_token:r,push_enabled:p,...i?{platform:i}:{},...a?{device_type:a}:{},...o?{browser:o}:{},...c?{browser_version:c}:{}},{transactionId:l})}updatePushDeviceToken(e,t,r={}){let{pushEnabled:n=!0,transactionId:i}=r;return this.submit("account_device",{seq:e,push_token:t,push_enabled:n},{transactionId:i})}disablePushDevice(e,t={}){return this.submit("account_device",{seq:e,push_enabled:!1},{transactionId:t.transactionId})}readRequestBody(e,t="application/json",r=!1){let n=f(this.hmacSecret,this.token);return P(e,t,r,n,this.packetMagicLen)}get _reqOpts(){return{baseUrl:this.baseUrl,token:this.token,apiKey:this.apiKey,hmacSecret:this.hmacSecret,packetMagicLen:this.packetMagicLen,encryptRequests:this.encryptRequests}}request(e,t,r,n=!0,i){return w(this._reqOpts,e,t,r,n,i)}};var Y=new m;export{m as EntityServerClient,Y as entityServer};
2
- //# sourceMappingURL=index.js.map