entity-server-client 0.2.6 → 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.
Files changed (64) hide show
  1. package/README.md +32 -0
  2. package/build.mjs +7 -0
  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 +47 -0
  9. package/docs/api/identity.md +32 -0
  10. package/docs/api/import.md +45 -0
  11. package/docs/api/packet.md +90 -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/request.md +118 -0
  16. package/docs/api/setup.md +43 -0
  17. package/docs/api/sms.md +45 -0
  18. package/docs/api/smtp.md +33 -0
  19. package/docs/api/transaction.md +50 -0
  20. package/docs/api/utils.md +52 -0
  21. package/docs/apis.md +22 -779
  22. package/docs/react.md +58 -0
  23. package/package.json +6 -1
  24. package/src/EntityServerClient.ts +5 -31
  25. package/src/client/base.ts +114 -12
  26. package/src/client/packet.ts +8 -31
  27. package/src/client/request.ts +52 -6
  28. package/src/client/utils.ts +5 -6
  29. package/src/mixins/auth.ts +14 -158
  30. package/src/mixins/push.ts +0 -23
  31. package/src/mixins/utils.ts +32 -1
  32. package/src/packet.ts +84 -0
  33. package/src/types.ts +15 -125
  34. package/tests/packet.test.mjs +50 -0
  35. package/dist/EntityServerClient.d.ts +0 -709
  36. package/dist/client/base.d.ts +0 -59
  37. package/dist/client/hmac.d.ts +0 -8
  38. package/dist/client/packet.d.ts +0 -24
  39. package/dist/client/request.d.ts +0 -15
  40. package/dist/client/utils.d.ts +0 -8
  41. package/dist/hooks/useEntityServer.d.ts +0 -63
  42. package/dist/index.d.ts +0 -4
  43. package/dist/index.js +0 -2
  44. package/dist/index.js.map +0 -7
  45. package/dist/mixins/alimtalk.d.ts +0 -56
  46. package/dist/mixins/auth.d.ts +0 -167
  47. package/dist/mixins/email.d.ts +0 -51
  48. package/dist/mixins/entity.d.ts +0 -119
  49. package/dist/mixins/file.d.ts +0 -78
  50. package/dist/mixins/identity.d.ts +0 -52
  51. package/dist/mixins/pg.d.ts +0 -63
  52. package/dist/mixins/push.d.ts +0 -110
  53. package/dist/mixins/sms.d.ts +0 -55
  54. package/dist/mixins/smtp.d.ts +0 -44
  55. package/dist/mixins/utils.d.ts +0 -70
  56. package/dist/react.d.ts +0 -1
  57. package/dist/react.js +0 -2
  58. package/dist/react.js.map +0 -7
  59. package/dist/types.d.ts +0 -329
  60. package/src/mixins/alimtalk.ts +0 -35
  61. package/src/mixins/email.ts +0 -46
  62. package/src/mixins/identity.ts +0 -35
  63. package/src/mixins/pg.ts +0 -58
  64. package/src/mixins/sms.ts +0 -46
@@ -19,15 +19,23 @@ export function AuthMixin<TBase extends GConstructor<EntityServerClientBase>>(
19
19
  async checkHealth(): Promise<{
20
20
  ok: boolean;
21
21
  packet_encryption?: boolean;
22
+ packet_mode?: string;
23
+ packet_token?: string;
22
24
  }> {
23
25
  const res = await fetch(`${this.baseUrl}/v1/health`, {
24
26
  signal: AbortSignal.timeout(3000),
27
+ credentials: "include",
25
28
  });
26
29
  const data = (await res.json()) as {
27
30
  ok: boolean;
28
31
  packet_encryption?: boolean;
32
+ packet_mode?: string;
33
+ packet_token?: string;
29
34
  };
30
35
  if (data.packet_encryption) this.encryptRequests = true;
36
+ if (typeof data.packet_token === "string") {
37
+ this.anonymousPacketToken = data.packet_token;
38
+ }
31
39
  return data;
32
40
  }
33
41
 
@@ -39,12 +47,18 @@ export function AuthMixin<TBase extends GConstructor<EntityServerClientBase>>(
39
47
  access_token: string;
40
48
  refresh_token: string;
41
49
  expires_in: number;
50
+ force_password_change?: boolean;
51
+ password_expired?: boolean;
52
+ password_expires_in_days?: number;
42
53
  }> {
43
54
  const data = await this._request<{
44
55
  data: {
45
56
  access_token: string;
46
57
  refresh_token: string;
47
58
  expires_in: number;
59
+ force_password_change?: boolean;
60
+ password_expired?: boolean;
61
+ password_expires_in_days?: number;
48
62
  };
49
63
  }>("POST", "/v1/auth/login", { email, passwd: password }, false);
50
64
  this.token = data.data.access_token;
@@ -100,17 +114,6 @@ export function AuthMixin<TBase extends GConstructor<EntityServerClientBase>>(
100
114
  return this._request("GET", "/v1/auth/me");
101
115
  }
102
116
 
103
- /** 비밀번호를 변경합니다. */
104
- changePassword(
105
- currentPasswd: string,
106
- newPasswd: string,
107
- ): Promise<{ ok: boolean }> {
108
- return this._request("POST", "/v1/auth/change-password", {
109
- current_passwd: currentPasswd,
110
- new_passwd: newPasswd,
111
- });
112
- }
113
-
114
117
  /** 회원 탈퇴를 요청합니다. */
115
118
  withdraw(passwd?: string): Promise<{ ok: boolean }> {
116
119
  return this._request(
@@ -136,152 +139,5 @@ export function AuthMixin<TBase extends GConstructor<EntityServerClientBase>>(
136
139
  }> {
137
140
  return this._request("POST", "/v1/auth/reactivate", params, false);
138
141
  }
139
-
140
- /** 비밀번호 재설정 메일을 요청합니다. */
141
- passwordResetRequest(email: string): Promise<{ ok: boolean }> {
142
- return this._request(
143
- "POST",
144
- "/v1/auth/password-reset",
145
- { email },
146
- false,
147
- );
148
- }
149
-
150
- /** 이메일로 전달된 토큰으로 비밀번호를 재설정합니다. */
151
- passwordResetConfirm(
152
- token: string,
153
- newPasswd: string,
154
- ): Promise<{ ok: boolean }> {
155
- return this._request(
156
- "POST",
157
- "/v1/auth/password-reset/confirm",
158
- { token, new_passwd: newPasswd },
159
- false,
160
- );
161
- }
162
-
163
- // ─── OAuth 연동 ───────────────────────────────────────────────────────
164
-
165
- /** OAuth 프로바이더를 현재 계정에 연동합니다. */
166
- oauthLink(
167
- provider: string,
168
- code: string,
169
- state?: string,
170
- ): Promise<{ ok: boolean; message: string; provider: string }> {
171
- return this._request("POST", "/v1/auth/oauth/link", {
172
- provider,
173
- code,
174
- ...(state ? { state } : {}),
175
- });
176
- }
177
-
178
- /** OAuth 프로바이더 연동을 해제합니다. */
179
- oauthUnlink(
180
- provider: string,
181
- ): Promise<{ ok: boolean; message: string; provider: string }> {
182
- return this._request("DELETE", `/v1/auth/oauth/link/${provider}`);
183
- }
184
-
185
- /** 현재 계정에 연동된 OAuth 프로바이더 목록을 반환합니다. */
186
- oauthProviders(): Promise<{
187
- ok: boolean;
188
- data: Array<{
189
- provider: string;
190
- email?: string;
191
- linked_at?: string;
192
- }>;
193
- }> {
194
- return this._request("GET", "/v1/auth/oauth/providers");
195
- }
196
-
197
- /** 특정 OAuth 프로바이더의 액세스 토큰을 갱신합니다. */
198
- oauthTokenRefresh(provider: string): Promise<{
199
- ok: boolean;
200
- access_token: string;
201
- expires_at?: string;
202
- }> {
203
- return this._request("POST", `/v1/auth/oauth/refresh/${provider}`);
204
- }
205
-
206
- // ─── 2단계 인증 (2FA) ─────────────────────────────────────────────────
207
-
208
- /** 2FA 설정을 시작하고 QR 코드 / 시크릿을 반환합니다. */
209
- twoFactorSetup(): Promise<{
210
- ok: boolean;
211
- setup_token: string;
212
- qr_url: string;
213
- secret: string;
214
- }> {
215
- return this._request("POST", "/v1/auth/2fa/setup");
216
- }
217
-
218
- /** TOTP 코드로 2FA 설정을 완료합니다. */
219
- twoFactorSetupVerify(
220
- code: string,
221
- setupToken: string,
222
- ): Promise<{ ok: boolean; recovery_codes: string[] }> {
223
- return this._request("POST", "/v1/auth/2fa/setup/verify", {
224
- code,
225
- setup_token: setupToken,
226
- });
227
- }
228
-
229
- /** 2FA를 비활성화합니다. */
230
- twoFactorDisable(code: string): Promise<{ ok: boolean }> {
231
- return this._request("DELETE", "/v1/auth/2fa", { code });
232
- }
233
-
234
- /** 2FA 활성화 여부를 조회합니다. */
235
- twoFactorStatus(): Promise<{ ok: boolean; enabled: boolean }> {
236
- return this._request("GET", "/v1/auth/2fa/status");
237
- }
238
-
239
- /** 임시 토큰으로 TOTP 코드를 검증하여 최종 JWT를 발급받습니다. */
240
- twoFactorVerify(
241
- twoFactorToken: string,
242
- code: string,
243
- ): Promise<{
244
- ok: boolean;
245
- access_token: string;
246
- refresh_token: string;
247
- expires_in: number;
248
- }> {
249
- return this._request(
250
- "POST",
251
- "/v1/auth/2fa/verify",
252
- { two_factor_token: twoFactorToken, code },
253
- false,
254
- );
255
- }
256
-
257
- /** 복구 코드로 2FA를 우회하여 최종 JWT를 발급받습니다. */
258
- twoFactorRecovery(
259
- twoFactorToken: string,
260
- recoveryCode: string,
261
- ): Promise<{
262
- ok: boolean;
263
- access_token: string;
264
- refresh_token: string;
265
- expires_in: number;
266
- }> {
267
- return this._request(
268
- "POST",
269
- "/v1/auth/2fa/recovery",
270
- {
271
- two_factor_token: twoFactorToken,
272
- recovery_code: recoveryCode,
273
- },
274
- false,
275
- );
276
- }
277
-
278
- /** 복구 코드를 재생성합니다. */
279
- twoFactorRegenerateRecovery(
280
- code: string,
281
- ): Promise<{ ok: boolean; recovery_codes: string[] }> {
282
- return this._request("POST", "/v1/auth/2fa/recovery/regenerate", {
283
- code,
284
- });
285
- }
286
142
  };
287
143
  }
@@ -2,8 +2,6 @@ import type {
2
2
  EntityListParams,
3
3
  EntityListResult,
4
4
  RegisterPushDeviceOptions,
5
- PushSendRequest,
6
- PushSendAllRequest,
7
5
  } from "../types";
8
6
  import type { GConstructor, EntityServerClientBase } from "../client/base";
9
7
 
@@ -107,26 +105,5 @@ export function PushMixin<TBase extends GConstructor<WithSubmit>>(Base: TBase) {
107
105
  { transactionId: opts.transactionId },
108
106
  );
109
107
  }
110
-
111
- // ─── 푸시 발송 API ────────────────────────────────────────────────────
112
-
113
- /** 특정 계정에 푸시 알림을 발송합니다. */
114
- pushSend(req: PushSendRequest): Promise<{ ok: boolean; seq: number }> {
115
- return this._request("POST", "/v1/push/send", req);
116
- }
117
-
118
- /** 전체 사용자에게 푸시 알림을 발송합니다. */
119
- pushSendAll(req: PushSendAllRequest): Promise<{
120
- ok: boolean;
121
- sent: number;
122
- failed: number;
123
- }> {
124
- return this._request("POST", "/v1/push/send-all", req);
125
- }
126
-
127
- /** 푸시 발송 상태를 조회합니다. */
128
- pushStatus(seq: number): Promise<{ ok: boolean; status: string }> {
129
- return this._request("POST", `/v1/push/status/${seq}`, {});
130
- }
131
108
  };
132
109
  }
@@ -1,4 +1,4 @@
1
- import type { QRCodeOptions, BarcodeOptions } from "../types";
1
+ import type { QRCodeOptions, BarcodeOptions, Pdf2PngOptions } from "../types";
2
2
  import type { GConstructor, EntityServerClientBase } from "../client/base";
3
3
 
4
4
  export function UtilsMixin<TBase extends GConstructor<EntityServerClientBase>>(
@@ -71,5 +71,36 @@ export function UtilsMixin<TBase extends GConstructor<EntityServerClientBase>>(
71
71
  ...opts,
72
72
  });
73
73
  }
74
+
75
+ /**
76
+ * PDF를 PNG 이미지로 변환합니다.
77
+ *
78
+ * 단일 페이지 요청이면 `image/png` ArrayBuffer,
79
+ * 다중 페이지 요청이면 `application/zip` ArrayBuffer를 반환합니다.
80
+ *
81
+ * ```ts
82
+ * const buf = await client.pdf2png(pdfArrayBuffer, { dpi: 200 });
83
+ * ```
84
+ */
85
+ pdf2png(
86
+ pdfData: ArrayBuffer | Uint8Array<ArrayBuffer>,
87
+ opts: Pdf2PngOptions = {},
88
+ ): Promise<ArrayBuffer> {
89
+ const form = new FormData();
90
+ form.append(
91
+ "file",
92
+ new Blob([pdfData], { type: "application/pdf" }),
93
+ "document.pdf",
94
+ );
95
+ const params = new URLSearchParams();
96
+ if (opts.dpi != null) params.set("dpi", String(opts.dpi));
97
+ if (opts.firstPage != null)
98
+ params.set("first_page", String(opts.firstPage));
99
+ if (opts.lastPage != null)
100
+ params.set("last_page", String(opts.lastPage));
101
+ const qs = params.toString();
102
+ const path = "/v1/utils/pdf2png" + (qs ? `?${qs}` : "");
103
+ return this._requestFormBinary("POST", path, form);
104
+ }
74
105
  };
75
106
  }
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,6 +129,11 @@ export interface RegisterPushDeviceOptions {
129
129
  export interface EntityServerClientOptions {
130
130
  baseUrl?: string;
131
131
  token?: string;
132
+ /**
133
+ * 익명 패킷 암호화용 부트스트랩 토큰입니다.
134
+ * entity-app-server의 `/v1/health` 응답으로 설정되는 용도입니다.
135
+ */
136
+ anonymousPacketToken?: string;
132
137
  /**
133
138
  * `true`이면 인증된 POST/PUT 요청 바디를 XChaCha20-Poly1305로 암호화합니다.
134
139
  *
@@ -208,131 +213,6 @@ export interface SmtpSendRequest {
208
213
  ref_seq?: number;
209
214
  }
210
215
 
211
- // ─── SMS ──────────────────────────────────────────────────────────────────────
212
-
213
- /** `smsSend()` 요청 파라미터입니다. */
214
- export interface SmsSendRequest {
215
- provider?: string;
216
- sender?: string;
217
- /** 수신자 전화번호 (필수) */
218
- receiver: string;
219
- /** 메시지 내용 (필수) */
220
- content: string;
221
- /** MMS 제목 (LMS/MMS 전용) */
222
- subject?: string;
223
- image_url?: string;
224
- ref_entity?: string;
225
- ref_seq?: number;
226
- }
227
-
228
- // ─── Push 발송 API ──────────────────────────────────────────────────────────────
229
-
230
- /** `pushSend()` 요청 파라미터 (`POST /v1/push/send`) */
231
- export interface PushSendRequest {
232
- /** 수신 계정 seq 배열 (필수) */
233
- account_seqs: number[];
234
- title: string;
235
- body: string;
236
- /** 커스텀 페이로드 */
237
- data?: Record<string, string>;
238
- ref_entity?: string;
239
- ref_seq?: number;
240
- }
241
-
242
- /** `pushSendAll()` 요청 파라미터 (`POST /v1/push/send-all`) */
243
- export interface PushSendAllRequest {
244
- title: string;
245
- body: string;
246
- data?: Record<string, string>;
247
- ref_entity?: string;
248
- ref_seq?: number;
249
- }
250
-
251
- // ─── 알림톡 / 친구톡 ───────────────────────────────────────────────────────────
252
-
253
- /** 알림톡 / 친구톡 버튼 */
254
- export interface AlimtalkButton {
255
- name: string;
256
- type: string;
257
- url_mobile?: string;
258
- url_pc?: string;
259
- }
260
-
261
- /** `alimtalkSend()` 요청 파라미터 */
262
- export interface AlimtalkSendRequest {
263
- /** 알림톡 템플릿 코드 (필수) */
264
- template_code: string;
265
- /** 수신자 전화번호 (필수) */
266
- receiver: string;
267
- variables?: Record<string, string>;
268
- provider?: string;
269
- }
270
-
271
- /** `friendtalkSend()` 요청 파라미터 */
272
- export interface FriendtalkSendRequest {
273
- /** 수신자 전화번호 (필수) */
274
- receiver: string;
275
- /** 메시지 내용 (필수) */
276
- content: string;
277
- /** 메시지 타입 (`"text"` | `"image"` | `"wide_image"` 등, 기본 `"text"`) */
278
- msg_type?: string;
279
- image_url?: string;
280
- image_link?: string;
281
- /** 광고 여부 (기본 `true`) */
282
- is_ad?: boolean;
283
- buttons?: AlimtalkButton[];
284
- carousel_json?: string;
285
- items_json?: string;
286
- header?: string;
287
- provider?: string;
288
- }
289
-
290
- // ─── PG 결제 ──────────────────────────────────────────────────────────────────
291
-
292
- /** `pgCreateOrder()` 요청 파라미터 */
293
- export interface PgCreateOrderRequest {
294
- /** 결제 금액 (필수) */
295
- amount: number;
296
- /** 주문명 (필수) */
297
- order_name: string;
298
- currency?: string;
299
- customer_name?: string;
300
- customer_email?: string;
301
- provider?: string;
302
- metadata?: Record<string, unknown>;
303
- }
304
-
305
- /** `pgConfirmPayment()` 요청 파라미터 */
306
- export interface PgConfirmPaymentRequest {
307
- payment_key: string;
308
- order_id: string;
309
- amount: number;
310
- }
311
-
312
- /** `pgCancelPayment()` 요청 파라미터 */
313
- export interface PgCancelPaymentRequest {
314
- /** 취소 사유 (필수) */
315
- cancel_reason: string;
316
- /** 부분 취소 금액 (생략 시 전액 취소) */
317
- cancel_amount?: number;
318
- refund_account?: {
319
- bank: string;
320
- account_number: string;
321
- holder_name: string;
322
- };
323
- }
324
-
325
- // ─── 본인인증 ──────────────────────────────────────────────────────────────────
326
-
327
- /** `identityRequest()` 요청 파라미터 */
328
- export interface IdentityRequestOptions {
329
- /** 인증 목적 (필수, 예: `"signup"`, `"password_reset"`) */
330
- purpose: string;
331
- /** 인증 방법 (예: `"simple"`, `"pass"`) */
332
- method?: string;
333
- provider?: string;
334
- }
335
-
336
216
  // ─── Utils ────────────────────────────────────────────────────────────────────
337
217
 
338
218
  /** `qrcode()` / `qrcodeBase64()` / `qrcodeText()` 공통 옵션 */
@@ -364,6 +244,16 @@ export interface BarcodeOptions {
364
244
  height?: number;
365
245
  }
366
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
+
367
257
  // ─── 파일 ─────────────────────────────────────────────────────────────────────
368
258
 
369
259
  /** 파일 메타 정보 */
@@ -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
+ });