entity-server-client 0.2.2 → 0.2.4
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 +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +3 -3
- package/dist/react.js +1 -1
- package/dist/react.js.map +3 -3
- package/docs/apis.md +56 -22
- package/package.json +1 -1
- package/src/hooks/useEntityServer.ts +1 -1
- package/src/index.ts +114 -5
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -117,6 +117,21 @@ export interface EntityServerClientOptions {
|
|
|
117
117
|
* 앱은 이 콜백에서 로그인 페이지로 이동하는 등의 처리를 해야 합니다.
|
|
118
118
|
*/
|
|
119
119
|
onSessionExpired?: (error: Error) => void;
|
|
120
|
+
/**
|
|
121
|
+
* HMAC 인증용 API Key (`X-API-Key` 헤더).
|
|
122
|
+
* `hmacSecret`과 함께 설정하면 HMAC 인증 모드로 동작합니다.
|
|
123
|
+
* **서버 사이드(Node.js 등) 전용. 브라우저에서는 사용하지 마세요.**
|
|
124
|
+
*/
|
|
125
|
+
apiKey?: string;
|
|
126
|
+
/**
|
|
127
|
+
* HMAC 인증 시크릿. `apiKey`와 함께 설정하면 HMAC 인증 모드로 동작합니다.
|
|
128
|
+
*
|
|
129
|
+
* 패킷 암호화 키도 이 값에서 HKDF-SHA256으로 유도합니다:
|
|
130
|
+
* `key = HKDF-SHA256(hmac_secret, info="entity-server:packet-encryption", salt="entity-server:hkdf:v1")`
|
|
131
|
+
*
|
|
132
|
+
* **서버 사이드(Node.js 등) 전용. 브라우저에서는 사용하지 마세요.**
|
|
133
|
+
*/
|
|
134
|
+
hmacSecret?: string;
|
|
120
135
|
}
|
|
121
136
|
/**
|
|
122
137
|
* `list()`, `history()` 응답의 `data` 필드 구조입니다.
|
|
@@ -151,6 +166,8 @@ export interface EntityHistoryRecord<T = unknown> {
|
|
|
151
166
|
export declare class EntityServerClient {
|
|
152
167
|
private baseUrl;
|
|
153
168
|
private token;
|
|
169
|
+
private apiKey;
|
|
170
|
+
private hmacSecret;
|
|
154
171
|
private packetMagicLen;
|
|
155
172
|
private encryptRequests;
|
|
156
173
|
private activeTxId;
|
|
@@ -172,6 +189,10 @@ export declare class EntityServerClient {
|
|
|
172
189
|
configure(options: Partial<EntityServerClientOptions>): void;
|
|
173
190
|
/** 인증 요청에 사용할 JWT Access Token을 설정합니다. */
|
|
174
191
|
setToken(token: string): void;
|
|
192
|
+
/** HMAC 인증용 API Key를 설정합니다. */
|
|
193
|
+
setApiKey(apiKey: string): void;
|
|
194
|
+
/** HMAC 인증용 시크릿을 설정합니다. */
|
|
195
|
+
setHmacSecret(secret: string): void;
|
|
175
196
|
/** 암호화 패킷 magic 길이(`packet_magic_len`)를 설정합니다. */
|
|
176
197
|
setPacketMagicLen(length: number): void;
|
|
177
198
|
/** 현재 암호화 패킷 magic 길이를 반환합니다. */
|
|
@@ -249,6 +270,13 @@ export declare class EntityServerClient {
|
|
|
249
270
|
ok: boolean;
|
|
250
271
|
data: T;
|
|
251
272
|
}>;
|
|
273
|
+
/** 조건으로 엔티티 단건을 조회합니다. data 컬럼을 완전히 복호화하여 반환합니다. */
|
|
274
|
+
find<T = unknown>(entity: string, conditions?: Record<string, unknown>, opts?: {
|
|
275
|
+
skipHooks?: boolean;
|
|
276
|
+
}): Promise<{
|
|
277
|
+
ok: boolean;
|
|
278
|
+
data: T;
|
|
279
|
+
}>;
|
|
252
280
|
/** 페이지네이션/정렬/필터 조건으로 엔티티 목록을 조회합니다. */
|
|
253
281
|
list<T = unknown>(entity: string, params?: EntityListParams): Promise<{
|
|
254
282
|
ok: boolean;
|
|
@@ -349,6 +377,12 @@ export declare class EntityServerClient {
|
|
|
349
377
|
* - JSON 응답의 `ok`가 false이면 에러를 던집니다.
|
|
350
378
|
*/
|
|
351
379
|
private request;
|
|
380
|
+
/**
|
|
381
|
+
* 패킷 암호화 키를 유도합니다.
|
|
382
|
+
* - HMAC 모드 (`hmacSecret` 설정 시): HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
|
|
383
|
+
* - JWT 모드: sha256(jwt_token)
|
|
384
|
+
*/
|
|
385
|
+
private derivePacketKey;
|
|
352
386
|
/**
|
|
353
387
|
* 평문 바이트를 XChaCha20-Poly1305로 암호화합니다.
|
|
354
388
|
* 포맷: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{xchacha20poly1305 as
|
|
1
|
+
import{xchacha20poly1305 as T}from"@noble/ciphers/chacha";import{sha256 as m}from"@noble/hashes/sha2";import{hkdf as _}from"@noble/hashes/hkdf";import{hmac as w}from"@noble/hashes/hmac";function b(h){return import.meta?.env?.[h]}var g=class{baseUrl;token;apiKey;hmacSecret;packetMagicLen;encryptRequests;activeTxId=null;keepSession;refreshBuffer;onTokenRefreshed;onSessionExpired;_sessionRefreshToken=null;_refreshTimer=null;constructor(e={}){let t=b("VITE_ENTITY_SERVER_URL"),n=b("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??(n?Number(n):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 n=Math.max((t-this.refreshBuffer)*1e3,0);this._refreshTimer=setTimeout(async()=>{if(this._sessionRefreshToken)try{let r=await this.refreshToken(this._sessionRefreshToken);this.onTokenRefreshed?.(r.access_token,r.expires_in),this._scheduleKeepSession(this._sessionRefreshToken,r.expires_in)}catch(r){this._clearRefreshTimer(),this.onSessionExpired?.(r instanceof Error?r:new Error(String(r)))}},n)}_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 n=await this.request("POST","/v1/auth/login",{email:e,passwd:t},!1);return this.token=n.data.access_token,this.keepSession&&this._scheduleKeepSession(n.data.refresh_token,n.data.expires_in),n.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,n={}){let r=n.skipHooks?"?skipHooks=true":"";return this.request("GET",`/v1/entity/${e}/${t}${r}`)}find(e,t,n={}){let r=n.skipHooks?"?skipHooks=true":"";return this.request("POST",`/v1/entity/${e}/find${r}`,t??{})}list(e,t={}){let{conditions:n,fields:r,orderDir:o,orderBy:s,...i}=t,a={page:1,limit:20,...i};s&&(a.orderBy=o==="DESC"?`-${s}`:s),r?.length&&(a.fields=r.join(","));let c=v(a);return this.request("POST",`/v1/entity/${e}/list?${c}`,n??{})}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,n={}){let r=n.transactionId??this.activeTxId,o=r?{"X-Transaction-ID":r}:void 0,s=n.skipHooks?"?skipHooks=true":"";return this.request("POST",`/v1/entity/${e}/submit${s}`,t,!0,o)}delete(e,t,n={}){let r=new URLSearchParams;n.hard&&r.set("hard","true"),n.skipHooks&&r.set("skipHooks","true");let o=r.size?`?${r}`:"",s=n.transactionId??this.activeTxId,i=s?{"X-Transaction-ID":s}:void 0;return this.request("POST",`/v1/entity/${e}/delete/${t}${o}`,void 0,!0,i)}history(e,t,n={}){let r=v({page:1,limit:50,...n});return this.request("GET",`/v1/entity/${e}/history/${t}?${r}`)}rollback(e,t){return this.request("POST",`/v1/entity/${e}/rollback/${t}`)}push(e,t,n={}){return this.submit(e,t,n)}pushLogList(e={}){return this.list("push_log",e)}registerPushDevice(e,t,n,r={}){let{platform:o,deviceType:s,browser:i,browserVersion:a,pushEnabled:c=!0,transactionId:y}=r;return this.submit("account_device",{id:t,account_seq:e,push_token:n,push_enabled:c,...o?{platform:o}:{},...s?{device_type:s}:{},...i?{browser:i}:{},...a?{browser_version:a}:{}},{transactionId:y})}updatePushDeviceToken(e,t,n={}){let{pushEnabled:r=!0,transactionId:o}=n;return this.submit("account_device",{seq:e,push_token:t,push_enabled:r},{transactionId:o})}disablePushDevice(e,t={}){return this.submit("account_device",{seq:e,push_enabled:!1},{transactionId:t.transactionId})}readRequestBody(e,t="application/json",n=!1){let o=t.toLowerCase().includes("application/octet-stream");if(n&&!o)throw new Error("Encrypted request required: Content-Type must be application/octet-stream");if(o){if(e==null)throw new Error("Encrypted request body is empty");if(e instanceof ArrayBuffer)return this.decryptPacket(e);if(e instanceof Uint8Array){let s=e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength);return this.decryptPacket(s)}throw new Error("Encrypted request body must be ArrayBuffer or Uint8Array")}return e==null||e===""?{}:typeof e=="string"?JSON.parse(e):e}async request(e,t,n,r=!0,o={}){let s=r&&!!(this.apiKey&&this.hmacSecret),i={"Content-Type":"application/json",...o};!s&&r&&this.token&&(i.Authorization=`Bearer ${this.token}`);let a=null;if(n!=null)if(this.encryptRequests&&r&&(this.token||s)&&e!=="GET"&&e!=="HEAD"){let l=new TextEncoder().encode(JSON.stringify(n)),d=this.encryptPacket(l);i["Content-Type"]="application/octet-stream",a=d}else a=JSON.stringify(n);if(s){let u=String(Math.floor(Date.now()/1e3)),l=crypto.randomUUID(),d=a instanceof Uint8Array?a:typeof a=="string"?new TextEncoder().encode(a):new Uint8Array(0),f=new TextEncoder().encode(`${e}|${t}|${u}|${l}|`),k=new Uint8Array(f.length+d.length);k.set(f,0),k.set(d,f.length);let E=[...w(m,new TextEncoder().encode(this.hmacSecret),k)].map(S=>S.toString(16).padStart(2,"0")).join("");i["X-API-Key"]=this.apiKey,i["X-Timestamp"]=u,i["X-Nonce"]=l,i["X-Signature"]=E}let c=await fetch(this.baseUrl+t,{method:e,headers:i,...a!=null?{body:a}:{}});if((c.headers.get("Content-Type")??"").includes("application/octet-stream")){let u=await c.arrayBuffer();return this.decryptPacket(u)}let p=await c.json();if(!p.ok){let u=new Error(p.message??`EntityServer error (HTTP ${c.status})`);throw u.status=c.status,u}return p}derivePacketKey(){if(this.hmacSecret){let e=new TextEncoder().encode("entity-server:hkdf:v1"),t=new TextEncoder().encode("entity-server:packet-encryption");return _(m,new TextEncoder().encode(this.hmacSecret),e,t,32)}return m(new TextEncoder().encode(this.token))}encryptPacket(e){let t=this.derivePacketKey(),n=new Uint8Array(this.packetMagicLen),r=new Uint8Array(24);crypto.getRandomValues(n),crypto.getRandomValues(r);let s=T(t,r).encrypt(e),i=new Uint8Array(this.packetMagicLen+24+s.length);return i.set(n,0),i.set(r,this.packetMagicLen),i.set(s,this.packetMagicLen+24),i}decryptPacket(e){let t=this.derivePacketKey(),n=new Uint8Array(e);if(n.length<this.packetMagicLen+24+16)throw new Error("Encrypted packet too short");let r=n.slice(this.packetMagicLen,this.packetMagicLen+24),o=n.slice(this.packetMagicLen+24),i=T(t,r).decrypt(o);return JSON.parse(new TextDecoder().decode(i))}};function v(h){return Object.entries(h).filter(([,e])=>e!=null).map(([e,t])=>`${encodeURIComponent(e==="orderBy"?"order_by":e)}=${encodeURIComponent(String(t))}`).join("&")}var L=new g;export{g as EntityServerClient,L as entityServer};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["// @ts-ignore\nimport { xchacha20poly1305 } from \"@noble/ciphers/chacha\";\n// @ts-ignore\nimport { sha256 } from \"@noble/hashes/sha2\";\n\n/**\n * \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D \uC870\uD68C \uD30C\uB77C\uBBF8\uD130\uC785\uB2C8\uB2E4.\n *\n * ```ts\n * client.list(\"post\", {\n * page: 1, limit: 10,\n * orderBy: \"created_time\", orderDir: \"DESC\",\n * fields: [\"seq\", \"title\", \"created_time\"],\n * conditions: { status: \"active\" },\n * });\n * ```\n */\nexport interface EntityListParams {\n /** \uC870\uD68C \uD398\uC774\uC9C0 \uBC88\uD638. \uAE30\uBCF8\uAC12: `1` */\n page?: number;\n /** \uD398\uC774\uC9C0\uB2F9 \uB808\uCF54\uB4DC \uC218. \uAE30\uBCF8\uAC12: `20` */\n limit?: number;\n /** \uC815\uB82C \uAE30\uC900 \uD544\uB4DC\uBA85 */\n orderBy?: string;\n /** \uC815\uB82C \uBC29\uD5A5. \uAE30\uBCF8\uAC12: `\"ASC\"` */\n orderDir?: \"ASC\" | \"DESC\";\n /**\n * \uBC18\uD658\uD560 \uD544\uB4DC \uBAA9\uB85D.\n *\n * - **\uBBF8\uC9C0\uC815 (\uAE30\uBCF8\uAC12)**: \uC5D4\uD2F0\uD2F0\uC758 \uC778\uB371\uC2A4 \uD544\uB4DC\uB9CC \uBC18\uD658\uD569\uB2C8\uB2E4.\n * \uBCF5\uD638\uD654\uB97C \uAC74\uB108\uB6F0\uAE30 \uB54C\uBB38\uC5D0 **\uAC00\uC7A5 \uBE60\uB985\uB2C8\uB2E4**.\n * - `[\"*\"]`: \uC804\uCCB4 \uD544\uB4DC \uBC18\uD658 (\uBCF5\uD638\uD654 \uC218\uD589).\n * - \uD544\uB4DC\uBA85 \uBAA9\uB85D: \uD574\uB2F9 \uD544\uB4DC\uB9CC \uBC18\uD658\uD569\uB2C8\uB2E4.\n * \uC5D4\uD2F0\uD2F0 \uC124\uC815\uC5D0 `index`\uB85C \uC120\uC5B8\uB41C \uD544\uB4DC\uB9CC \uC9C0\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * \uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 \uD544\uB4DC\uBA85\uC744 \uC9C0\uC815\uD558\uBA74 \uC11C\uBC84 \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD569\uB2C8\uB2E4.\n * - `seq`, `created_time`, `updated_time`, `license_seq`\uB294 \uD544\uB4DC\uC5D0 \uAD00\uACC4\uC5C6\uC774 \uD56D\uC0C1 \uD3EC\uD568\uB429\uB2C8\uB2E4.\n *\n * ```ts\n * // \uAE30\uBCF8\uAC12 (\uC778\uB371\uC2A4 \uD544\uB4DC\uB9CC, \uAC00\uC7A5 \uBE60\uB984)\n * client.list(\"account\")\n * // \uC804\uCCB4 \uD544\uB4DC\n * client.list(\"account\", { fields: [\"*\"] })\n * // seq, name, email\uB9CC\n * client.list(\"account\", { fields: [\"seq\", \"name\", \"email\"] })\n * ```\n */\n fields?: string[];\n /** \uD544\uD130 \uC870\uAC74. POST body\uB85C \uC804\uB2EC\uB429\uB2C8\uB2E4. (\uC608: `{ status: \"active\" }`) */\n conditions?: Record<string, unknown>;\n}\n\n/**\n * `query()` \uBA54\uC11C\uB4DC\uC5D0 \uC804\uB2EC\uD558\uB294 SQL \uCFFC\uB9AC \uC694\uCCAD\uC785\uB2C8\uB2E4.\n *\n * - `sql`: SELECT \uC804\uC6A9 SQL. \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD558\uBA70 JOIN \uC9C0\uC6D0.\n * - `params`: SQL \uBC14\uC778\uB529 \uD30C\uB77C\uBBF8\uD130 (`?` \uD50C\uB808\uC774\uC2A4\uD640\uB354 \uB300\uC751).\n * - `limit`: \uCD5C\uB300 \uBC18\uD658 \uAC74\uC218 (\uCD5C\uB300 1000. \uBBF8\uC9C0\uC815 \uC2DC \uC11C\uBC84 \uAE30\uBCF8\uAC12 \uC801\uC6A9).\n *\n * ```ts\n * client.query(\"order\", {\n * sql: `SELECT o.seq, o.status, u.name\n * FROM order o\n * JOIN account u ON u.data_seq = o.account_seq\n * WHERE o.status = ?`,\n * params: [\"pending\"],\n * limit: 100,\n * });\n * ```\n */\nexport interface EntityQueryRequest {\n sql: string;\n params?: unknown[];\n limit?: number;\n}\n\nexport interface RegisterPushDeviceOptions {\n platform?: string;\n deviceType?: string;\n browser?: string;\n browserVersion?: string;\n pushEnabled?: boolean;\n transactionId?: string;\n}\n\n/** EntityServerClient \uC0DD\uC131/\uC124\uC815 \uC635\uC158\uC785\uB2C8\uB2E4. */\nexport interface EntityServerClientOptions {\n baseUrl?: string;\n token?: string;\n packetMagicLen?: number;\n /**\n * `true`\uC774\uBA74 \uC778\uC99D\uB41C POST/PUT \uC694\uCCAD \uBC14\uB514\uB97C XChaCha20-Poly1305\uB85C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uC758 `EnablePacketEncryption`\uC774 \uD65C\uC131\uD654\uB41C \uACBD\uC6B0 \uD544\uC218\uB85C \uC124\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.\n * \uB85C\uADF8\uC778(`login()`)\u00B7\uD1A0\uD070 \uAC31\uC2E0(`refreshToken()`)\uC740 \uC778\uC99D \uC804 \uC694\uCCAD\uC774\uBBC0\uB85C \uC790\uB3D9\uC73C\uB85C \uAC74\uB108\uB701\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12: `false`\n */\n encryptRequests?: boolean;\n /**\n * `true`\uC774\uBA74 `login()` \uC131\uACF5 \uD6C4 Access Token \uB9CC\uB8CC \uC804\uC5D0 \uC790\uB3D9\uC73C\uB85C \uAC31\uC2E0(silent refresh)\uD569\uB2C8\uB2E4.\n * \uAC31\uC2E0 \uC2DC\uC810\uC740 `expires_in - refreshBuffer` \uCD08\uC785\uB2C8\uB2E4.\n *\n * \uAC31\uC2E0 \uC131\uACF5 \uC2DC `onTokenRefreshed`, \uC2E4\uD328 \uC2DC `onSessionExpired` \uCF5C\uBC31\uC774 \uD638\uCD9C\uB429\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12: `false`\n */\n keepSession?: boolean;\n /**\n * \uB9CC\uB8CC \uBA87 \uCD08 \uC804\uC5D0 \uC790\uB3D9 \uAC31\uC2E0\uC744 \uC2DC\uB3C4\uD560\uC9C0 \uC124\uC815\uD569\uB2C8\uB2E4.\n *\n * \uC608: `expires_in = 3600`, `refreshBuffer = 60` \u2192 3540\uCD08 \uD6C4 \uAC31\uC2E0\n *\n * \uAE30\uBCF8\uAC12: `60`\n */\n refreshBuffer?: number;\n /**\n * \uC790\uB3D9 \uAC31\uC2E0 \uC131\uACF5 \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\uC785\uB2C8\uB2E4.\n * \uC0C8 `access_token`\uACFC `expires_in`\uC774 \uC804\uB2EC\uB429\uB2C8\uB2E4.\n * \uC571\uC740 \uC774 \uCF5C\uBC31\uC5D0\uC11C localStorage \uB4F1\uC5D0 \uD1A0\uD070\uC744 \uC800\uC7A5\uD574\uC57C \uD569\uB2C8\uB2E4.\n */\n onTokenRefreshed?: (accessToken: string, expiresIn: number) => void;\n /**\n * \uC138\uC158 \uC720\uC9C0 \uAC31\uC2E0 \uC2E4\uD328 \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\uC785\uB2C8\uB2E4.\n * refresh_token \uB9CC\uB8CC \uB4F1\uC73C\uB85C \uC7AC\uBC1C\uAE09\uC774 \uBD88\uAC00\uB2A5\uD55C \uACBD\uC6B0\uC785\uB2C8\uB2E4.\n * \uC571\uC740 \uC774 \uCF5C\uBC31\uC5D0\uC11C \uB85C\uADF8\uC778 \uD398\uC774\uC9C0\uB85C \uC774\uB3D9\uD558\uB294 \uB4F1\uC758 \uCC98\uB9AC\uB97C \uD574\uC57C \uD569\uB2C8\uB2E4.\n */\n onSessionExpired?: (error: Error) => void;\n}\n\n/**\n * `list()`, `history()` \uC751\uB2F5\uC758 `data` \uD544\uB4DC \uAD6C\uC870\uC785\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uB294 \uD56D\uC0C1 \uC774 \uAD6C\uC870\uB85C \uBC18\uD658\uD569\uB2C8\uB2E4:\n * ```json\n * { \"ok\": true, \"data\": { \"items\": [...], \"total\": 100, \"page\": 1, \"limit\": 20 } }\n * ```\n */\nexport interface EntityListResult<T = unknown> {\n items: T[];\n /** \uC804\uCCB4 \uB808\uCF54\uB4DC \uC218 */\n total: number;\n /** \uD604\uC7AC \uD398\uC774\uC9C0 \uBC88\uD638 */\n page: number;\n /** \uD398\uC774\uC9C0\uB2F9 \uB808\uCF54\uB4DC \uC218 */\n limit: number;\n}\n\n/**\n * `history()` \uC751\uB2F5\uC758 \uAC1C\uBCC4 \uC774\uB825 \uB808\uCF54\uB4DC \uAD6C\uC870\uC785\uB2C8\uB2E4.\n *\n * - `action`: `\"INSERT\"` | `\"UPDATE\"` | `\"DELETE_SOFT\"` | `\"DELETE_HARD\"` | `\"ROLLBACK\"`\n * - `data_snapshot`: \uBCC0\uACBD \uB2F9\uC2DC \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130 \uC2A4\uB0C5\uC0F7\n */\nexport interface EntityHistoryRecord<T = unknown> {\n seq: number;\n action:\n | \"INSERT\"\n | \"UPDATE\"\n | \"DELETE_SOFT\"\n | \"DELETE_HARD\"\n | \"ROLLBACK\"\n | string;\n data_snapshot: T | null;\n changed_by: number | null;\n changed_time: string;\n}\n\n/** Vite \uD658\uACBD\uBCC0\uC218(`import.meta.env`)\uC5D0\uC11C \uAC12\uC744 \uC77D\uC2B5\uB2C8\uB2E4. */\nfunction readEnv(name: string): string | undefined {\n const meta = import.meta as unknown as {\n env?: Record<string, string | undefined>;\n };\n return meta?.env?.[name];\n}\n\nexport class EntityServerClient {\n private baseUrl: string;\n private token: string;\n private packetMagicLen: number;\n private encryptRequests: boolean;\n private activeTxId: string | null = null;\n\n // \uC138\uC158 \uC720\uC9C0 \uAD00\uB828\n private keepSession: boolean;\n private refreshBuffer: number;\n private onTokenRefreshed?: (accessToken: string, expiresIn: number) => void;\n private onSessionExpired?: (error: Error) => void;\n private _sessionRefreshToken: string | null = null;\n private _refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * EntityServerClient \uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12:\n * - `baseUrl`: `VITE_ENTITY_SERVER_URL` \uB610\uB294 `http://localhost:47200`\n * - `packetMagicLen`: `VITE_ENTITY_SERVER_PACKET_MAGIC_LEN` \uB610\uB294 `4`\n */\n constructor(options: EntityServerClientOptions = {}) {\n const envBaseUrl = readEnv(\"VITE_ENTITY_SERVER_URL\");\n const envMagicLen = readEnv(\"VITE_ENTITY_SERVER_PACKET_MAGIC_LEN\");\n\n this.baseUrl = (\n options.baseUrl ??\n envBaseUrl ??\n \"http://localhost:47200\"\n ).replace(/\\/$/, \"\");\n\n this.token = options.token ?? \"\";\n this.packetMagicLen =\n options.packetMagicLen ?? (envMagicLen ? Number(envMagicLen) : 4);\n this.encryptRequests = options.encryptRequests ?? false;\n this.keepSession = options.keepSession ?? false;\n this.refreshBuffer = options.refreshBuffer ?? 60;\n this.onTokenRefreshed = options.onTokenRefreshed;\n this.onSessionExpired = options.onSessionExpired;\n }\n\n /** baseUrl, token, packetMagicLen, encryptRequests \uAC12\uC744 \uB7F0\uD0C0\uC784\uC5D0 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n configure(options: Partial<EntityServerClientOptions>): void {\n if (options.baseUrl) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n }\n if (typeof options.token === \"string\") {\n this.token = options.token;\n }\n if (typeof options.packetMagicLen === \"number\") {\n this.packetMagicLen = options.packetMagicLen;\n }\n if (typeof options.encryptRequests === \"boolean\") {\n this.encryptRequests = options.encryptRequests;\n }\n if (typeof options.keepSession === \"boolean\") {\n this.keepSession = options.keepSession;\n }\n if (typeof options.refreshBuffer === \"number\") {\n this.refreshBuffer = options.refreshBuffer;\n }\n if (options.onTokenRefreshed) {\n this.onTokenRefreshed = options.onTokenRefreshed;\n }\n if (options.onSessionExpired) {\n this.onSessionExpired = options.onSessionExpired;\n }\n }\n\n /** \uC778\uC99D \uC694\uCCAD\uC5D0 \uC0AC\uC6A9\uD560 JWT Access Token\uC744 \uC124\uC815\uD569\uB2C8\uB2E4. */\n setToken(token: string): void {\n this.token = token;\n }\n\n /** \uC554\uD638\uD654 \uD328\uD0B7 magic \uAE38\uC774(`packet_magic_len`)\uB97C \uC124\uC815\uD569\uB2C8\uB2E4. */\n setPacketMagicLen(length: number): void {\n this.packetMagicLen = length;\n }\n\n /** \uD604\uC7AC \uC554\uD638\uD654 \uD328\uD0B7 magic \uAE38\uC774\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4. */\n getPacketMagicLen(): number {\n return this.packetMagicLen;\n }\n\n /**\n * \uC790\uB3D9 \uD1A0\uD070 \uAC31\uC2E0 \uD0C0\uC774\uBA38\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4.\n * @param refreshToken \uAC31\uC2E0\uC5D0 \uC0AC\uC6A9\uD560 Refresh Token\n * @param expiresIn Access Token\uC758 \uC720\uD6A8 \uAE30\uAC04 (\uCD08)\n */\n private _scheduleKeepSession(\n refreshToken: string,\n expiresIn: number,\n ): void {\n this._clearRefreshTimer();\n this._sessionRefreshToken = refreshToken;\n\n const delayMs = Math.max((expiresIn - this.refreshBuffer) * 1000, 0);\n this._refreshTimer = setTimeout(async () => {\n if (!this._sessionRefreshToken) return;\n try {\n const result = await this.refreshToken(this._sessionRefreshToken);\n this.onTokenRefreshed?.(result.access_token, result.expires_in);\n // \uAC31\uC2E0 \uC131\uACF5 \uC2DC \uB2E4\uC74C \uB9CC\uB8CC \uC804 \uD0C0\uC774\uBA38 \uC7AC\uC608\uC57D\n this._scheduleKeepSession(\n this._sessionRefreshToken,\n result.expires_in,\n );\n } catch (err) {\n this._clearRefreshTimer();\n this.onSessionExpired?.(\n err instanceof Error ? err : new Error(String(err)),\n );\n }\n }, delayMs);\n }\n\n /** \uC790\uB3D9 \uAC31\uC2E0 \uD0C0\uC774\uBA38\uB97C \uC815\uB9AC\uD569\uB2C8\uB2E4. */\n private _clearRefreshTimer(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n /**\n * \uC138\uC158 \uC720\uC9C0 \uD0C0\uC774\uBA38\uB97C \uC911\uC9C0\uD569\uB2C8\uB2E4.\n * `logout()` \uD638\uCD9C \uC2DC \uC790\uB3D9\uC73C\uB85C \uC911\uC9C0\uB418\uBBC0\uB85C \uC9C1\uC811 \uD638\uCD9C\uC774 \uD544\uC694\uD55C \uACBD\uC6B0\uB294 \uB4DC\uBB45\uB2C8\uB2E4.\n */\n stopKeepSession(): void {\n this._clearRefreshTimer();\n this._sessionRefreshToken = null;\n }\n\n /**\n * \uC11C\uBC84 \uD5EC\uC2A4 \uCCB4\uD06C\uB97C \uC218\uD589\uD558\uACE0 \uD328\uD0B7 \uC554\uD638\uD654 \uD65C\uC131 \uC5EC\uBD80\uB97C \uC790\uB3D9\uC73C\uB85C \uAC10\uC9C0\uD569\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uAC00 `packet_encryption: true`\uB97C \uC751\uB2F5\uD558\uBA74 \uC774\uD6C4 \uBAA8\uB4E0 \uC694\uCCAD\uC5D0 \uC554\uD638\uD654\uAC00 \uC790\uB3D9 \uC801\uC6A9\uB429\uB2C8\uB2E4.\n * \uCD08\uAE30\uD654 \uC9C1\uD6C4 \uB610\uB294 \uB85C\uADF8\uC778 \uC804\uC5D0 \uD638\uCD9C\uD558\uBA74 \uC554\uD638\uD654 \uC124\uC815\uC744 \uC790\uB3D9\uC73C\uB85C \uAD6C\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n *\n * ```ts\n * await client.checkHealth();\n * await client.login(email, password); // \uC774\uD6C4 \uC694\uCCAD\uC740 \uC554\uD638\uD654 \uC790\uB3D9 \uC801\uC6A9\n * ```\n *\n * @returns `{ ok: true }` \uB610\uB294 `{ ok: true, packet_encryption: true }`\n */\n async checkHealth(): Promise<{ ok: boolean; packet_encryption?: boolean }> {\n const res = await fetch(`${this.baseUrl}/v1/health`, {\n signal: AbortSignal.timeout(3000),\n });\n const data = (await res.json()) as {\n ok: boolean;\n packet_encryption?: boolean;\n };\n if (data.packet_encryption) {\n this.encryptRequests = true;\n }\n return data;\n }\n\n /** \uB85C\uADF8\uC778 \uD6C4 `access_token`\uC744 \uB0B4\uBD80 \uC0C1\uD0DC\uC5D0 \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async login(\n email: string,\n password: string,\n ): Promise<{\n access_token: string;\n refresh_token: string;\n expires_in: number;\n }> {\n const data = await this.request<{\n data: {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n };\n }>(\"POST\", \"/v1/auth/login\", { email, passwd: password }, false);\n this.token = data.data.access_token;\n if (this.keepSession) {\n this._scheduleKeepSession(\n data.data.refresh_token,\n data.data.expires_in,\n );\n }\n return data.data;\n }\n\n /** Refresh Token\uC73C\uB85C Access Token\uC744 \uC7AC\uBC1C\uAE09\uBC1B\uC544 \uB0B4\uBD80 \uD1A0\uD070\uC744 \uAD50\uCCB4\uD569\uB2C8\uB2E4. */\n async refreshToken(\n refreshToken: string,\n ): Promise<{ access_token: string; expires_in: number }> {\n const data = await this.request<{\n data: { access_token: string; expires_in: number };\n }>(\"POST\", \"/v1/auth/refresh\", { refresh_token: refreshToken }, false);\n this.token = data.data.access_token;\n if (this.keepSession) {\n this._scheduleKeepSession(refreshToken, data.data.expires_in);\n }\n return data.data;\n }\n\n /**\n * \uC11C\uBC84\uC5D0 \uB85C\uADF8\uC544\uC6C3\uC744 \uC694\uCCAD\uD558\uACE0 \uB0B4\uBD80 \uD1A0\uD070\uC744 \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4.\n * refresh_token\uC744 \uC11C\uBC84\uC5D0 \uC804\uB2EC\uD574 \uBB34\uD6A8\uD654\uD569\uB2C8\uB2E4.\n */\n async logout(refreshToken: string): Promise<{ ok: boolean }> {\n this.stopKeepSession();\n const data = await this.request<{ ok: boolean }>(\n \"POST\",\n \"/v1/auth/logout\",\n { refresh_token: refreshToken },\n false,\n );\n this.token = \"\";\n return data;\n }\n\n /** \uD2B8\uB79C\uC7AD\uC158\uC744 \uC2DC\uC791\uD558\uACE0 \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158 ID\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async transStart(): Promise<string> {\n const res = await this.request<{ ok: boolean; transaction_id: string }>(\n \"POST\",\n \"/v1/transaction/start\",\n undefined,\n false,\n );\n this.activeTxId = res.transaction_id;\n return this.activeTxId;\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uB864\uBC31\uD569\uB2C8\uB2E4. */\n transRollback(transactionId?: string): Promise<{ ok: boolean }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/rollback/${txId}`);\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uCEE4\uBC0B\uD569\uB2C8\uB2E4.\n *\n * @returns `results` \uBC30\uC5F4: commit\uB41C \uAC01 \uC791\uC5C5\uC758 `entity`, `action`, `seq`\n */\n transCommit(transactionId?: string): Promise<{\n ok: boolean;\n results: Array<{ entity: string; action: string; seq: number }>;\n }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/commit/${txId}`);\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n get<T = unknown>(\n entity: string,\n seq: number,\n opts: { skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; data: T }> {\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n return this.request(\"GET\", `/v1/entity/${entity}/${seq}${q}`);\n }\n\n /** \uD398\uC774\uC9C0\uB124\uC774\uC158/\uC815\uB82C/\uD544\uD130 \uC870\uAC74\uC73C\uB85C \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n list<T = unknown>(\n entity: string,\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n const { conditions, fields, orderDir, orderBy, ...rest } = params;\n\n const queryObj: Record<string, unknown> = {\n page: 1,\n limit: 20,\n ...rest,\n };\n if (orderBy) {\n queryObj.orderBy = orderDir === \"DESC\" ? `-${orderBy}` : orderBy;\n }\n if (fields?.length) {\n queryObj.fields = fields.join(\",\");\n }\n\n const q = buildQuery(queryObj);\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/list?${q}`,\n conditions ?? {},\n );\n }\n\n /**\n * \uC5D4\uD2F0\uD2F0 \uCD1D \uAC74\uC218\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * @param conditions \uD544\uD130 \uC870\uAC74 (\uC608: `{ status: \"active\" }`)\n */\n count(\n entity: string,\n conditions?: Record<string, unknown>,\n ): Promise<{ ok: boolean; count: number }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/count`,\n conditions ?? {},\n );\n }\n\n /**\n * \uCEE4\uC2A4\uD140 SQL\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * SELECT \uC804\uC6A9\uC774\uBA70 \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * JOIN\uC744 \uC0AC\uC6A9\uD574 \uC5EC\uB7EC \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD569\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n * `entity`\uB294 SQL\uC5D0 \uD3EC\uD568\uB41C \uAE30\uBCF8 \uC5D4\uD2F0\uD2F0\uBA85(\uB77C\uC6B0\uD2B8 \uACBD\uB85C\uC6A9)\uC785\uB2C8\uB2E4.\n */\n query<T = unknown>(\n entity: string,\n req: EntityQueryRequest,\n ): Promise<{ ok: boolean; data: { items: T[]; count: number } }> {\n return this.request(\"POST\", `/v1/entity/${entity}/query`, req);\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130\uB97C \uC0DD\uC131/\uC218\uC815(Submit)\uD569\uB2C8\uB2E4. `seq`\uAC00 \uC5C6\uC73C\uBA74 INSERT, \uC788\uC73C\uBA74 UPDATE\uC785\uB2C8\uB2E4. */\n submit(\n entity: string,\n data: Record<string, unknown>,\n opts: { transactionId?: string; skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/submit${q}`,\n data,\n true,\n extraHeaders,\n );\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4(`hard=true`\uBA74 \uD558\uB4DC \uC0AD\uC81C, \uAE30\uBCF8\uC740 \uC18C\uD504\uD2B8 \uC0AD\uC81C). */\n delete(\n entity: string,\n seq: number,\n opts: {\n transactionId?: string;\n hard?: boolean;\n skipHooks?: boolean;\n } = {},\n ): Promise<{ ok: boolean; deleted: number }> {\n const params = new URLSearchParams();\n if (opts.hard) params.set(\"hard\", \"true\");\n if (opts.skipHooks) params.set(\"skipHooks\", \"true\");\n const q = params.size ? `?${params}` : \"\";\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/delete/${seq}${q}`,\n undefined,\n true,\n extraHeaders,\n );\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC758 \uBCC0\uACBD \uC774\uB825\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uC774\uB825 \uD56D\uBAA9\uB2F9 `action`, `data_snapshot`, `changed_by`, `changed_time`\uC744 \uD3EC\uD568\uD569\uB2C8\uB2E4. */\n history<T = unknown>(\n entity: string,\n seq: number,\n params: Pick<EntityListParams, \"page\" | \"limit\"> = {},\n ): Promise<{\n ok: boolean;\n data: EntityListResult<EntityHistoryRecord<T>>;\n }> {\n const q = buildQuery({ page: 1, limit: 50, ...params });\n return this.request(\"GET\", `/v1/entity/${entity}/history/${seq}?${q}`);\n }\n\n /** \uD2B9\uC815 \uC774\uB825 \uC2DC\uC810\uC73C\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uB864\uBC31\uD569\uB2C8\uB2E4. */\n rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/rollback/${historySeq}`,\n );\n }\n\n /** \uD478\uC2DC \uAD00\uB828 \uC5D4\uD2F0\uD2F0\uB85C payload\uB97C \uC804\uC1A1(Submit)\uD569\uB2C8\uB2E4. */\n push(\n pushEntity: string,\n payload: Record<string, unknown>,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(pushEntity, payload, opts);\n }\n\n /** \uD478\uC2DC \uB85C\uADF8 \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n pushLogList<T = unknown>(\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n return this.list<T>(\"push_log\", params);\n }\n\n /** \uACC4\uC815\uC758 \uD478\uC2DC \uB514\uBC14\uC774\uC2A4\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4. */\n registerPushDevice(\n accountSeq: number,\n deviceId: string,\n pushToken: string,\n opts: RegisterPushDeviceOptions = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const {\n platform,\n deviceType,\n browser,\n browserVersion,\n pushEnabled = true,\n transactionId,\n } = opts;\n\n return this.submit(\n \"account_device\",\n {\n id: deviceId,\n account_seq: accountSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n ...(platform ? { platform } : {}),\n ...(deviceType ? { device_type: deviceType } : {}),\n ...(browser ? { browser } : {}),\n ...(browserVersion ? { browser_version: browserVersion } : {}),\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4 \uB808\uCF54\uB4DC\uC758 \uD478\uC2DC \uD1A0\uD070\uC744 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n updatePushDeviceToken(\n deviceSeq: number,\n pushToken: string,\n opts: { pushEnabled?: boolean; transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const { pushEnabled = true, transactionId } = opts;\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4\uC758 \uD478\uC2DC \uC218\uC2E0\uC744 \uBE44\uD65C\uC131\uD654\uD569\uB2C8\uB2E4. */\n disablePushDevice(\n deviceSeq: number,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_enabled: false,\n },\n { transactionId: opts.transactionId },\n );\n }\n\n /**\n * \uC694\uCCAD \uBC14\uB514\uB97C \uD30C\uC2F1\uD558\uACE0 `application/octet-stream`\uC778 \uACBD\uC6B0 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC6D0\uC2DC \uC554\uD638\uD654 payload\uB97C \uC9C1\uC811 \uB2E4\uB8E8\uB294 \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n */\n readRequestBody<T = Record<string, unknown>>(\n body: ArrayBuffer | Uint8Array | string | T | null | undefined,\n contentType = \"application/json\",\n requireEncrypted = false,\n ): T {\n const lowered = contentType.toLowerCase();\n const isEncrypted = lowered.includes(\"application/octet-stream\");\n\n if (requireEncrypted && !isEncrypted) {\n throw new Error(\n \"Encrypted request required: Content-Type must be application/octet-stream\",\n );\n }\n\n if (isEncrypted) {\n if (body == null) {\n throw new Error(\"Encrypted request body is empty\");\n }\n if (body instanceof ArrayBuffer) {\n return this.decryptPacket<T>(body);\n }\n if (body instanceof Uint8Array) {\n const sliced = body.buffer.slice(\n body.byteOffset,\n body.byteOffset + body.byteLength,\n );\n return this.decryptPacket<T>(sliced as ArrayBuffer);\n }\n throw new Error(\n \"Encrypted request body must be ArrayBuffer or Uint8Array\",\n );\n }\n\n if (body == null || body === \"\") return {} as T;\n if (typeof body === \"string\") return JSON.parse(body) as T;\n return body as T;\n }\n\n /**\n * \uACF5\uD1B5 HTTP \uC694\uCCAD \uD568\uC218\uC785\uB2C8\uB2E4.\n *\n * - `encryptRequests`\uAC00 \uD65C\uC131\uD654\uB41C \uC778\uC99D \uC694\uCCAD\uC758 POST \uBC14\uB514\uB97C \uC790\uB3D9 \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * - \uC751\uB2F5\uC774 `application/octet-stream`\uC774\uBA74 \uC790\uB3D9 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n * - JSON \uC751\uB2F5\uC758 `ok`\uAC00 false\uC774\uBA74 \uC5D0\uB7EC\uB97C \uB358\uC9D1\uB2C8\uB2E4.\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n withAuth = true,\n extraHeaders: Record<string, string> = {},\n ): Promise<T> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...extraHeaders,\n };\n if (withAuth && this.token) {\n headers.Authorization = `Bearer ${this.token}`;\n }\n\n // \uC694\uCCAD \uBC14\uB514 \uACB0\uC815: encryptRequests \uD65C\uC131\uD654 \uC2DC POST \uBC14\uB514\uB97C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n // - \uB85C\uADF8\uC778/\uD1A0\uD070 \uAC31\uC2E0(withAuth=false)\uC740 \uC554\uD638\uD654\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.\n // - GET \uC740 \uBC14\uB514\uAC00 \uC5C6\uC73C\uBBC0\uB85C \uAC74\uB108\uB701\uB2C8\uB2E4.\n let fetchBody: string | Uint8Array | null = null;\n if (body != null) {\n const shouldEncrypt =\n this.encryptRequests &&\n withAuth &&\n this.token &&\n method !== \"GET\" &&\n method !== \"HEAD\";\n\n if (shouldEncrypt) {\n const plaintext = new TextEncoder().encode(\n JSON.stringify(body),\n );\n const encrypted = this.encryptPacket(plaintext);\n headers[\"Content-Type\"] = \"application/octet-stream\";\n fetchBody = encrypted;\n } else {\n fetchBody = JSON.stringify(body);\n }\n }\n\n const res = await fetch(this.baseUrl + path, {\n method,\n headers,\n ...(fetchBody != null ? { body: fetchBody as BodyInit } : {}),\n });\n\n const contentType = res.headers.get(\"Content-Type\") ?? \"\";\n\n if (contentType.includes(\"application/octet-stream\")) {\n const buffer = await res.arrayBuffer();\n return this.decryptPacket<T>(buffer);\n }\n\n const data = await res.json();\n if (!data.ok) {\n const err = new Error(\n data.message ?? `EntityServer error (HTTP ${res.status})`,\n );\n (err as { status?: number }).status = res.status;\n throw err;\n }\n return data as T;\n }\n\n /**\n * \uD3C9\uBB38 \uBC14\uC774\uD2B8\uB97C XChaCha20-Poly1305\uB85C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * \uD3EC\uB9F7: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]\n */\n private encryptPacket(plaintext: Uint8Array): Uint8Array {\n const key = sha256(new TextEncoder().encode(this.token));\n const magic = new Uint8Array(this.packetMagicLen);\n const nonce = new Uint8Array(24);\n crypto.getRandomValues(magic);\n crypto.getRandomValues(nonce);\n const cipher = xchacha20poly1305(key, nonce);\n const ciphertext = cipher.encrypt(plaintext);\n const result = new Uint8Array(\n this.packetMagicLen + 24 + ciphertext.length,\n );\n result.set(magic, 0);\n result.set(nonce, this.packetMagicLen);\n result.set(ciphertext, this.packetMagicLen + 24);\n return result;\n }\n\n /** \uC11C\uBC84\uC758 \uC554\uD638\uD654 \uD328\uD0B7\uC744 \uBCF5\uD638\uD654\uD574 JSON \uAC1D\uCCB4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\n private decryptPacket<T>(buffer: ArrayBuffer): T {\n const key = sha256(new TextEncoder().encode(this.token));\n const data = new Uint8Array(buffer);\n\n if (data.length < this.packetMagicLen + 24 + 16) {\n throw new Error(\"Encrypted packet too short\");\n }\n\n const nonce = data.slice(this.packetMagicLen, this.packetMagicLen + 24);\n const ciphertext = data.slice(this.packetMagicLen + 24);\n const cipher = xchacha20poly1305(key, nonce);\n const plaintext = cipher.decrypt(ciphertext);\n return JSON.parse(new TextDecoder().decode(plaintext)) as T;\n }\n}\n\n/** \uCFFC\uB9AC \uD30C\uB77C\uBBF8\uD130 \uAC1D\uCCB4\uB97C URL \uCFFC\uB9AC \uBB38\uC790\uC5F4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\nfunction buildQuery(params: Record<string, unknown>): string {\n return Object.entries(params)\n .filter(([, value]) => value != null)\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key === \"orderBy\" ? \"order_by\" : key)}=${encodeURIComponent(String(value))}`,\n )\n .join(\"&\");\n}\n\nexport const entityServer = new EntityServerClient();\n"],
|
|
5
|
-
"mappings": "AACA,OAAS,qBAAAA,MAAyB,wBAElC,OAAS,UAAAC,MAAc,
|
|
6
|
-
"names": ["xchacha20poly1305", "sha256", "readEnv", "name", "EntityServerClient", "options", "envBaseUrl", "envMagicLen", "token", "length", "refreshToken", "expiresIn", "delayMs", "result", "err", "data", "email", "password", "res", "transactionId", "txId", "entity", "seq", "opts", "q", "
|
|
4
|
+
"sourcesContent": ["// @ts-ignore\nimport { xchacha20poly1305 } from \"@noble/ciphers/chacha\";\n// @ts-ignore\nimport { sha256 } from \"@noble/hashes/sha2\";\n// @ts-ignore\nimport { hkdf } from \"@noble/hashes/hkdf\";\n// @ts-ignore\nimport { hmac } from \"@noble/hashes/hmac\";\n\n/**\n * \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D \uC870\uD68C \uD30C\uB77C\uBBF8\uD130\uC785\uB2C8\uB2E4.\n *\n * ```ts\n * client.list(\"post\", {\n * page: 1, limit: 10,\n * orderBy: \"created_time\", orderDir: \"DESC\",\n * fields: [\"seq\", \"title\", \"created_time\"],\n * conditions: { status: \"active\" },\n * });\n * ```\n */\nexport interface EntityListParams {\n /** \uC870\uD68C \uD398\uC774\uC9C0 \uBC88\uD638. \uAE30\uBCF8\uAC12: `1` */\n page?: number;\n /** \uD398\uC774\uC9C0\uB2F9 \uB808\uCF54\uB4DC \uC218. \uAE30\uBCF8\uAC12: `20` */\n limit?: number;\n /** \uC815\uB82C \uAE30\uC900 \uD544\uB4DC\uBA85 */\n orderBy?: string;\n /** \uC815\uB82C \uBC29\uD5A5. \uAE30\uBCF8\uAC12: `\"ASC\"` */\n orderDir?: \"ASC\" | \"DESC\";\n /**\n * \uBC18\uD658\uD560 \uD544\uB4DC \uBAA9\uB85D.\n *\n * - **\uBBF8\uC9C0\uC815 (\uAE30\uBCF8\uAC12)**: \uC5D4\uD2F0\uD2F0\uC758 \uC778\uB371\uC2A4 \uD544\uB4DC\uB9CC \uBC18\uD658\uD569\uB2C8\uB2E4.\n * \uBCF5\uD638\uD654\uB97C \uAC74\uB108\uB6F0\uAE30 \uB54C\uBB38\uC5D0 **\uAC00\uC7A5 \uBE60\uB985\uB2C8\uB2E4**.\n * - `[\"*\"]`: \uC804\uCCB4 \uD544\uB4DC \uBC18\uD658 (\uBCF5\uD638\uD654 \uC218\uD589).\n * - \uD544\uB4DC\uBA85 \uBAA9\uB85D: \uD574\uB2F9 \uD544\uB4DC\uB9CC \uBC18\uD658\uD569\uB2C8\uB2E4.\n * \uC5D4\uD2F0\uD2F0 \uC124\uC815\uC5D0 `index`\uB85C \uC120\uC5B8\uB41C \uD544\uB4DC\uB9CC \uC9C0\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * \uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 \uD544\uB4DC\uBA85\uC744 \uC9C0\uC815\uD558\uBA74 \uC11C\uBC84 \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD569\uB2C8\uB2E4.\n * - `seq`, `created_time`, `updated_time`, `license_seq`\uB294 \uD544\uB4DC\uC5D0 \uAD00\uACC4\uC5C6\uC774 \uD56D\uC0C1 \uD3EC\uD568\uB429\uB2C8\uB2E4.\n *\n * ```ts\n * // \uAE30\uBCF8\uAC12 (\uC778\uB371\uC2A4 \uD544\uB4DC\uB9CC, \uAC00\uC7A5 \uBE60\uB984)\n * client.list(\"account\")\n * // \uC804\uCCB4 \uD544\uB4DC\n * client.list(\"account\", { fields: [\"*\"] })\n * // seq, name, email\uB9CC\n * client.list(\"account\", { fields: [\"seq\", \"name\", \"email\"] })\n * ```\n */\n fields?: string[];\n /** \uD544\uD130 \uC870\uAC74. POST body\uB85C \uC804\uB2EC\uB429\uB2C8\uB2E4. (\uC608: `{ status: \"active\" }`) */\n conditions?: Record<string, unknown>;\n}\n\n/**\n * `query()` \uBA54\uC11C\uB4DC\uC5D0 \uC804\uB2EC\uD558\uB294 SQL \uCFFC\uB9AC \uC694\uCCAD\uC785\uB2C8\uB2E4.\n *\n * - `sql`: SELECT \uC804\uC6A9 SQL. \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD558\uBA70 JOIN \uC9C0\uC6D0.\n * - `params`: SQL \uBC14\uC778\uB529 \uD30C\uB77C\uBBF8\uD130 (`?` \uD50C\uB808\uC774\uC2A4\uD640\uB354 \uB300\uC751).\n * - `limit`: \uCD5C\uB300 \uBC18\uD658 \uAC74\uC218 (\uCD5C\uB300 1000. \uBBF8\uC9C0\uC815 \uC2DC \uC11C\uBC84 \uAE30\uBCF8\uAC12 \uC801\uC6A9).\n *\n * ```ts\n * client.query(\"order\", {\n * sql: `SELECT o.seq, o.status, u.name\n * FROM order o\n * JOIN account u ON u.data_seq = o.account_seq\n * WHERE o.status = ?`,\n * params: [\"pending\"],\n * limit: 100,\n * });\n * ```\n */\nexport interface EntityQueryRequest {\n sql: string;\n params?: unknown[];\n limit?: number;\n}\n\nexport interface RegisterPushDeviceOptions {\n platform?: string;\n deviceType?: string;\n browser?: string;\n browserVersion?: string;\n pushEnabled?: boolean;\n transactionId?: string;\n}\n\n/** EntityServerClient \uC0DD\uC131/\uC124\uC815 \uC635\uC158\uC785\uB2C8\uB2E4. */\nexport interface EntityServerClientOptions {\n baseUrl?: string;\n token?: string;\n packetMagicLen?: number;\n /**\n * `true`\uC774\uBA74 \uC778\uC99D\uB41C POST/PUT \uC694\uCCAD \uBC14\uB514\uB97C XChaCha20-Poly1305\uB85C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uC758 `EnablePacketEncryption`\uC774 \uD65C\uC131\uD654\uB41C \uACBD\uC6B0 \uD544\uC218\uB85C \uC124\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.\n * \uB85C\uADF8\uC778(`login()`)\u00B7\uD1A0\uD070 \uAC31\uC2E0(`refreshToken()`)\uC740 \uC778\uC99D \uC804 \uC694\uCCAD\uC774\uBBC0\uB85C \uC790\uB3D9\uC73C\uB85C \uAC74\uB108\uB701\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12: `false`\n */\n encryptRequests?: boolean;\n /**\n * `true`\uC774\uBA74 `login()` \uC131\uACF5 \uD6C4 Access Token \uB9CC\uB8CC \uC804\uC5D0 \uC790\uB3D9\uC73C\uB85C \uAC31\uC2E0(silent refresh)\uD569\uB2C8\uB2E4.\n * \uAC31\uC2E0 \uC2DC\uC810\uC740 `expires_in - refreshBuffer` \uCD08\uC785\uB2C8\uB2E4.\n *\n * \uAC31\uC2E0 \uC131\uACF5 \uC2DC `onTokenRefreshed`, \uC2E4\uD328 \uC2DC `onSessionExpired` \uCF5C\uBC31\uC774 \uD638\uCD9C\uB429\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12: `false`\n */\n keepSession?: boolean;\n /**\n * \uB9CC\uB8CC \uBA87 \uCD08 \uC804\uC5D0 \uC790\uB3D9 \uAC31\uC2E0\uC744 \uC2DC\uB3C4\uD560\uC9C0 \uC124\uC815\uD569\uB2C8\uB2E4.\n *\n * \uC608: `expires_in = 3600`, `refreshBuffer = 60` \u2192 3540\uCD08 \uD6C4 \uAC31\uC2E0\n *\n * \uAE30\uBCF8\uAC12: `60`\n */\n refreshBuffer?: number;\n /**\n * \uC790\uB3D9 \uAC31\uC2E0 \uC131\uACF5 \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\uC785\uB2C8\uB2E4.\n * \uC0C8 `access_token`\uACFC `expires_in`\uC774 \uC804\uB2EC\uB429\uB2C8\uB2E4.\n * \uC571\uC740 \uC774 \uCF5C\uBC31\uC5D0\uC11C localStorage \uB4F1\uC5D0 \uD1A0\uD070\uC744 \uC800\uC7A5\uD574\uC57C \uD569\uB2C8\uB2E4.\n */\n onTokenRefreshed?: (accessToken: string, expiresIn: number) => void;\n /**\n * \uC138\uC158 \uC720\uC9C0 \uAC31\uC2E0 \uC2E4\uD328 \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\uC785\uB2C8\uB2E4.\n * refresh_token \uB9CC\uB8CC \uB4F1\uC73C\uB85C \uC7AC\uBC1C\uAE09\uC774 \uBD88\uAC00\uB2A5\uD55C \uACBD\uC6B0\uC785\uB2C8\uB2E4.\n * \uC571\uC740 \uC774 \uCF5C\uBC31\uC5D0\uC11C \uB85C\uADF8\uC778 \uD398\uC774\uC9C0\uB85C \uC774\uB3D9\uD558\uB294 \uB4F1\uC758 \uCC98\uB9AC\uB97C \uD574\uC57C \uD569\uB2C8\uB2E4.\n */\n onSessionExpired?: (error: Error) => void;\n /**\n * HMAC \uC778\uC99D\uC6A9 API Key (`X-API-Key` \uD5E4\uB354).\n * `hmacSecret`\uACFC \uD568\uAED8 \uC124\uC815\uD558\uBA74 HMAC \uC778\uC99D \uBAA8\uB4DC\uB85C \uB3D9\uC791\uD569\uB2C8\uB2E4.\n * **\uC11C\uBC84 \uC0AC\uC774\uB4DC(Node.js \uB4F1) \uC804\uC6A9. \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C\uB294 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.**\n */\n apiKey?: string;\n /**\n * HMAC \uC778\uC99D \uC2DC\uD06C\uB9BF. `apiKey`\uC640 \uD568\uAED8 \uC124\uC815\uD558\uBA74 HMAC \uC778\uC99D \uBAA8\uB4DC\uB85C \uB3D9\uC791\uD569\uB2C8\uB2E4.\n *\n * \uD328\uD0B7 \uC554\uD638\uD654 \uD0A4\uB3C4 \uC774 \uAC12\uC5D0\uC11C HKDF-SHA256\uC73C\uB85C \uC720\uB3C4\uD569\uB2C8\uB2E4:\n * `key = HKDF-SHA256(hmac_secret, info=\"entity-server:packet-encryption\", salt=\"entity-server:hkdf:v1\")`\n *\n * **\uC11C\uBC84 \uC0AC\uC774\uB4DC(Node.js \uB4F1) \uC804\uC6A9. \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C\uB294 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.**\n */\n hmacSecret?: string;\n}\n\n/**\n * `list()`, `history()` \uC751\uB2F5\uC758 `data` \uD544\uB4DC \uAD6C\uC870\uC785\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uB294 \uD56D\uC0C1 \uC774 \uAD6C\uC870\uB85C \uBC18\uD658\uD569\uB2C8\uB2E4:\n * ```json\n * { \"ok\": true, \"data\": { \"items\": [...], \"total\": 100, \"page\": 1, \"limit\": 20 } }\n * ```\n */\nexport interface EntityListResult<T = unknown> {\n items: T[];\n /** \uC804\uCCB4 \uB808\uCF54\uB4DC \uC218 */\n total: number;\n /** \uD604\uC7AC \uD398\uC774\uC9C0 \uBC88\uD638 */\n page: number;\n /** \uD398\uC774\uC9C0\uB2F9 \uB808\uCF54\uB4DC \uC218 */\n limit: number;\n}\n\n/**\n * `history()` \uC751\uB2F5\uC758 \uAC1C\uBCC4 \uC774\uB825 \uB808\uCF54\uB4DC \uAD6C\uC870\uC785\uB2C8\uB2E4.\n *\n * - `action`: `\"INSERT\"` | `\"UPDATE\"` | `\"DELETE_SOFT\"` | `\"DELETE_HARD\"` | `\"ROLLBACK\"`\n * - `data_snapshot`: \uBCC0\uACBD \uB2F9\uC2DC \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130 \uC2A4\uB0C5\uC0F7\n */\nexport interface EntityHistoryRecord<T = unknown> {\n seq: number;\n action:\n | \"INSERT\"\n | \"UPDATE\"\n | \"DELETE_SOFT\"\n | \"DELETE_HARD\"\n | \"ROLLBACK\"\n | string;\n data_snapshot: T | null;\n changed_by: number | null;\n changed_time: string;\n}\n\n/** Vite \uD658\uACBD\uBCC0\uC218(`import.meta.env`)\uC5D0\uC11C \uAC12\uC744 \uC77D\uC2B5\uB2C8\uB2E4. */\nfunction readEnv(name: string): string | undefined {\n const meta = import.meta as unknown as {\n env?: Record<string, string | undefined>;\n };\n return meta?.env?.[name];\n}\n\nexport class EntityServerClient {\n private baseUrl: string;\n private token: string;\n private apiKey: string;\n private hmacSecret: string;\n private packetMagicLen: number;\n private encryptRequests: boolean;\n private activeTxId: string | null = null;\n\n // \uC138\uC158 \uC720\uC9C0 \uAD00\uB828\n private keepSession: boolean;\n private refreshBuffer: number;\n private onTokenRefreshed?: (accessToken: string, expiresIn: number) => void;\n private onSessionExpired?: (error: Error) => void;\n private _sessionRefreshToken: string | null = null;\n private _refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * EntityServerClient \uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12:\n * - `baseUrl`: `VITE_ENTITY_SERVER_URL` \uB610\uB294 `http://localhost:47200`\n * - `packetMagicLen`: `VITE_ENTITY_SERVER_PACKET_MAGIC_LEN` \uB610\uB294 `4`\n */\n constructor(options: EntityServerClientOptions = {}) {\n const envBaseUrl = readEnv(\"VITE_ENTITY_SERVER_URL\");\n const envMagicLen = readEnv(\"VITE_ENTITY_SERVER_PACKET_MAGIC_LEN\");\n\n this.baseUrl = (\n options.baseUrl ??\n envBaseUrl ??\n \"http://localhost:47200\"\n ).replace(/\\/$/, \"\");\n\n this.token = options.token ?? \"\";\n this.apiKey = options.apiKey ?? \"\";\n this.hmacSecret = options.hmacSecret ?? \"\";\n this.packetMagicLen =\n options.packetMagicLen ?? (envMagicLen ? Number(envMagicLen) : 4);\n this.encryptRequests = options.encryptRequests ?? false;\n this.keepSession = options.keepSession ?? false;\n this.refreshBuffer = options.refreshBuffer ?? 60;\n this.onTokenRefreshed = options.onTokenRefreshed;\n this.onSessionExpired = options.onSessionExpired;\n }\n\n /** baseUrl, token, packetMagicLen, encryptRequests \uAC12\uC744 \uB7F0\uD0C0\uC784\uC5D0 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n configure(options: Partial<EntityServerClientOptions>): void {\n if (options.baseUrl) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n }\n if (typeof options.token === \"string\") {\n this.token = options.token;\n }\n if (typeof options.packetMagicLen === \"number\") {\n this.packetMagicLen = options.packetMagicLen;\n }\n if (typeof options.encryptRequests === \"boolean\") {\n this.encryptRequests = options.encryptRequests;\n }\n if (typeof options.apiKey === \"string\") {\n this.apiKey = options.apiKey;\n }\n if (typeof options.hmacSecret === \"string\") {\n this.hmacSecret = options.hmacSecret;\n }\n if (typeof options.keepSession === \"boolean\") {\n this.keepSession = options.keepSession;\n }\n if (typeof options.refreshBuffer === \"number\") {\n this.refreshBuffer = options.refreshBuffer;\n }\n if (options.onTokenRefreshed) {\n this.onTokenRefreshed = options.onTokenRefreshed;\n }\n if (options.onSessionExpired) {\n this.onSessionExpired = options.onSessionExpired;\n }\n }\n\n /** \uC778\uC99D \uC694\uCCAD\uC5D0 \uC0AC\uC6A9\uD560 JWT Access Token\uC744 \uC124\uC815\uD569\uB2C8\uB2E4. */\n setToken(token: string): void {\n this.token = token;\n }\n\n /** HMAC \uC778\uC99D\uC6A9 API Key\uB97C \uC124\uC815\uD569\uB2C8\uB2E4. */\n setApiKey(apiKey: string): void {\n this.apiKey = apiKey;\n }\n\n /** HMAC \uC778\uC99D\uC6A9 \uC2DC\uD06C\uB9BF\uC744 \uC124\uC815\uD569\uB2C8\uB2E4. */\n setHmacSecret(secret: string): void {\n this.hmacSecret = secret;\n }\n\n /** \uC554\uD638\uD654 \uD328\uD0B7 magic \uAE38\uC774(`packet_magic_len`)\uB97C \uC124\uC815\uD569\uB2C8\uB2E4. */\n setPacketMagicLen(length: number): void {\n this.packetMagicLen = length;\n }\n\n /** \uD604\uC7AC \uC554\uD638\uD654 \uD328\uD0B7 magic \uAE38\uC774\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4. */\n getPacketMagicLen(): number {\n return this.packetMagicLen;\n }\n\n /**\n * \uC790\uB3D9 \uD1A0\uD070 \uAC31\uC2E0 \uD0C0\uC774\uBA38\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4.\n * @param refreshToken \uAC31\uC2E0\uC5D0 \uC0AC\uC6A9\uD560 Refresh Token\n * @param expiresIn Access Token\uC758 \uC720\uD6A8 \uAE30\uAC04 (\uCD08)\n */\n private _scheduleKeepSession(\n refreshToken: string,\n expiresIn: number,\n ): void {\n this._clearRefreshTimer();\n this._sessionRefreshToken = refreshToken;\n\n const delayMs = Math.max((expiresIn - this.refreshBuffer) * 1000, 0);\n this._refreshTimer = setTimeout(async () => {\n if (!this._sessionRefreshToken) return;\n try {\n const result = await this.refreshToken(\n this._sessionRefreshToken,\n );\n this.onTokenRefreshed?.(result.access_token, result.expires_in);\n // \uAC31\uC2E0 \uC131\uACF5 \uC2DC \uB2E4\uC74C \uB9CC\uB8CC \uC804 \uD0C0\uC774\uBA38 \uC7AC\uC608\uC57D\n this._scheduleKeepSession(\n this._sessionRefreshToken,\n result.expires_in,\n );\n } catch (err) {\n this._clearRefreshTimer();\n this.onSessionExpired?.(\n err instanceof Error ? err : new Error(String(err)),\n );\n }\n }, delayMs);\n }\n\n /** \uC790\uB3D9 \uAC31\uC2E0 \uD0C0\uC774\uBA38\uB97C \uC815\uB9AC\uD569\uB2C8\uB2E4. */\n private _clearRefreshTimer(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n /**\n * \uC138\uC158 \uC720\uC9C0 \uD0C0\uC774\uBA38\uB97C \uC911\uC9C0\uD569\uB2C8\uB2E4.\n * `logout()` \uD638\uCD9C \uC2DC \uC790\uB3D9\uC73C\uB85C \uC911\uC9C0\uB418\uBBC0\uB85C \uC9C1\uC811 \uD638\uCD9C\uC774 \uD544\uC694\uD55C \uACBD\uC6B0\uB294 \uB4DC\uBB45\uB2C8\uB2E4.\n */\n stopKeepSession(): void {\n this._clearRefreshTimer();\n this._sessionRefreshToken = null;\n }\n\n /**\n * \uC11C\uBC84 \uD5EC\uC2A4 \uCCB4\uD06C\uB97C \uC218\uD589\uD558\uACE0 \uD328\uD0B7 \uC554\uD638\uD654 \uD65C\uC131 \uC5EC\uBD80\uB97C \uC790\uB3D9\uC73C\uB85C \uAC10\uC9C0\uD569\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uAC00 `packet_encryption: true`\uB97C \uC751\uB2F5\uD558\uBA74 \uC774\uD6C4 \uBAA8\uB4E0 \uC694\uCCAD\uC5D0 \uC554\uD638\uD654\uAC00 \uC790\uB3D9 \uC801\uC6A9\uB429\uB2C8\uB2E4.\n * \uCD08\uAE30\uD654 \uC9C1\uD6C4 \uB610\uB294 \uB85C\uADF8\uC778 \uC804\uC5D0 \uD638\uCD9C\uD558\uBA74 \uC554\uD638\uD654 \uC124\uC815\uC744 \uC790\uB3D9\uC73C\uB85C \uAD6C\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n *\n * ```ts\n * await client.checkHealth();\n * await client.login(email, password); // \uC774\uD6C4 \uC694\uCCAD\uC740 \uC554\uD638\uD654 \uC790\uB3D9 \uC801\uC6A9\n * ```\n *\n * @returns `{ ok: true }` \uB610\uB294 `{ ok: true, packet_encryption: true }`\n */\n async checkHealth(): Promise<{ ok: boolean; packet_encryption?: boolean }> {\n const res = await fetch(`${this.baseUrl}/v1/health`, {\n signal: AbortSignal.timeout(3000),\n });\n const data = (await res.json()) as {\n ok: boolean;\n packet_encryption?: boolean;\n };\n if (data.packet_encryption) {\n this.encryptRequests = true;\n }\n return data;\n }\n\n /** \uB85C\uADF8\uC778 \uD6C4 `access_token`\uC744 \uB0B4\uBD80 \uC0C1\uD0DC\uC5D0 \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async login(\n email: string,\n password: string,\n ): Promise<{\n access_token: string;\n refresh_token: string;\n expires_in: number;\n }> {\n const data = await this.request<{\n data: {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n };\n }>(\"POST\", \"/v1/auth/login\", { email, passwd: password }, false);\n this.token = data.data.access_token;\n if (this.keepSession) {\n this._scheduleKeepSession(\n data.data.refresh_token,\n data.data.expires_in,\n );\n }\n return data.data;\n }\n\n /** Refresh Token\uC73C\uB85C Access Token\uC744 \uC7AC\uBC1C\uAE09\uBC1B\uC544 \uB0B4\uBD80 \uD1A0\uD070\uC744 \uAD50\uCCB4\uD569\uB2C8\uB2E4. */\n async refreshToken(\n refreshToken: string,\n ): Promise<{ access_token: string; expires_in: number }> {\n const data = await this.request<{\n data: { access_token: string; expires_in: number };\n }>(\"POST\", \"/v1/auth/refresh\", { refresh_token: refreshToken }, false);\n this.token = data.data.access_token;\n if (this.keepSession) {\n this._scheduleKeepSession(refreshToken, data.data.expires_in);\n }\n return data.data;\n }\n\n /**\n * \uC11C\uBC84\uC5D0 \uB85C\uADF8\uC544\uC6C3\uC744 \uC694\uCCAD\uD558\uACE0 \uB0B4\uBD80 \uD1A0\uD070\uC744 \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4.\n * refresh_token\uC744 \uC11C\uBC84\uC5D0 \uC804\uB2EC\uD574 \uBB34\uD6A8\uD654\uD569\uB2C8\uB2E4.\n */\n async logout(refreshToken: string): Promise<{ ok: boolean }> {\n this.stopKeepSession();\n const data = await this.request<{ ok: boolean }>(\n \"POST\",\n \"/v1/auth/logout\",\n { refresh_token: refreshToken },\n false,\n );\n this.token = \"\";\n return data;\n }\n\n /** \uD2B8\uB79C\uC7AD\uC158\uC744 \uC2DC\uC791\uD558\uACE0 \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158 ID\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async transStart(): Promise<string> {\n const res = await this.request<{ ok: boolean; transaction_id: string }>(\n \"POST\",\n \"/v1/transaction/start\",\n undefined,\n false,\n );\n this.activeTxId = res.transaction_id;\n return this.activeTxId;\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uB864\uBC31\uD569\uB2C8\uB2E4. */\n transRollback(transactionId?: string): Promise<{ ok: boolean }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/rollback/${txId}`);\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uCEE4\uBC0B\uD569\uB2C8\uB2E4.\n *\n * @returns `results` \uBC30\uC5F4: commit\uB41C \uAC01 \uC791\uC5C5\uC758 `entity`, `action`, `seq`\n */\n transCommit(transactionId?: string): Promise<{\n ok: boolean;\n results: Array<{ entity: string; action: string; seq: number }>;\n }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/commit/${txId}`);\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n get<T = unknown>(\n entity: string,\n seq: number,\n opts: { skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; data: T }> {\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n return this.request(\"GET\", `/v1/entity/${entity}/${seq}${q}`);\n }\n\n /** \uC870\uAC74\uC73C\uB85C \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. data \uCEEC\uB7FC\uC744 \uC644\uC804\uD788 \uBCF5\uD638\uD654\uD558\uC5EC \uBC18\uD658\uD569\uB2C8\uB2E4. */\n find<T = unknown>(\n entity: string,\n conditions?: Record<string, unknown>,\n opts: { skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; data: T }> {\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/find${q}`,\n conditions ?? {},\n );\n }\n\n /** \uD398\uC774\uC9C0\uB124\uC774\uC158/\uC815\uB82C/\uD544\uD130 \uC870\uAC74\uC73C\uB85C \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n list<T = unknown>(\n entity: string,\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n const { conditions, fields, orderDir, orderBy, ...rest } = params;\n\n const queryObj: Record<string, unknown> = {\n page: 1,\n limit: 20,\n ...rest,\n };\n if (orderBy) {\n queryObj.orderBy = orderDir === \"DESC\" ? `-${orderBy}` : orderBy;\n }\n if (fields?.length) {\n queryObj.fields = fields.join(\",\");\n }\n\n const q = buildQuery(queryObj);\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/list?${q}`,\n conditions ?? {},\n );\n }\n\n /**\n * \uC5D4\uD2F0\uD2F0 \uCD1D \uAC74\uC218\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * @param conditions \uD544\uD130 \uC870\uAC74 (\uC608: `{ status: \"active\" }`)\n */\n count(\n entity: string,\n conditions?: Record<string, unknown>,\n ): Promise<{ ok: boolean; count: number }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/count`,\n conditions ?? {},\n );\n }\n\n /**\n * \uCEE4\uC2A4\uD140 SQL\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * SELECT \uC804\uC6A9\uC774\uBA70 \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * JOIN\uC744 \uC0AC\uC6A9\uD574 \uC5EC\uB7EC \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD569\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n * `entity`\uB294 SQL\uC5D0 \uD3EC\uD568\uB41C \uAE30\uBCF8 \uC5D4\uD2F0\uD2F0\uBA85(\uB77C\uC6B0\uD2B8 \uACBD\uB85C\uC6A9)\uC785\uB2C8\uB2E4.\n */\n query<T = unknown>(\n entity: string,\n req: EntityQueryRequest,\n ): Promise<{ ok: boolean; data: { items: T[]; count: number } }> {\n return this.request(\"POST\", `/v1/entity/${entity}/query`, req);\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130\uB97C \uC0DD\uC131/\uC218\uC815(Submit)\uD569\uB2C8\uB2E4. `seq`\uAC00 \uC5C6\uC73C\uBA74 INSERT, \uC788\uC73C\uBA74 UPDATE\uC785\uB2C8\uB2E4. */\n submit(\n entity: string,\n data: Record<string, unknown>,\n opts: { transactionId?: string; skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/submit${q}`,\n data,\n true,\n extraHeaders,\n );\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4(`hard=true`\uBA74 \uD558\uB4DC \uC0AD\uC81C, \uAE30\uBCF8\uC740 \uC18C\uD504\uD2B8 \uC0AD\uC81C). */\n delete(\n entity: string,\n seq: number,\n opts: {\n transactionId?: string;\n hard?: boolean;\n skipHooks?: boolean;\n } = {},\n ): Promise<{ ok: boolean; deleted: number }> {\n const params = new URLSearchParams();\n if (opts.hard) params.set(\"hard\", \"true\");\n if (opts.skipHooks) params.set(\"skipHooks\", \"true\");\n const q = params.size ? `?${params}` : \"\";\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/delete/${seq}${q}`,\n undefined,\n true,\n extraHeaders,\n );\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC758 \uBCC0\uACBD \uC774\uB825\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uC774\uB825 \uD56D\uBAA9\uB2F9 `action`, `data_snapshot`, `changed_by`, `changed_time`\uC744 \uD3EC\uD568\uD569\uB2C8\uB2E4. */\n history<T = unknown>(\n entity: string,\n seq: number,\n params: Pick<EntityListParams, \"page\" | \"limit\"> = {},\n ): Promise<{\n ok: boolean;\n data: EntityListResult<EntityHistoryRecord<T>>;\n }> {\n const q = buildQuery({ page: 1, limit: 50, ...params });\n return this.request(\"GET\", `/v1/entity/${entity}/history/${seq}?${q}`);\n }\n\n /** \uD2B9\uC815 \uC774\uB825 \uC2DC\uC810\uC73C\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uB864\uBC31\uD569\uB2C8\uB2E4. */\n rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/rollback/${historySeq}`,\n );\n }\n\n /** \uD478\uC2DC \uAD00\uB828 \uC5D4\uD2F0\uD2F0\uB85C payload\uB97C \uC804\uC1A1(Submit)\uD569\uB2C8\uB2E4. */\n push(\n pushEntity: string,\n payload: Record<string, unknown>,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(pushEntity, payload, opts);\n }\n\n /** \uD478\uC2DC \uB85C\uADF8 \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n pushLogList<T = unknown>(\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n return this.list<T>(\"push_log\", params);\n }\n\n /** \uACC4\uC815\uC758 \uD478\uC2DC \uB514\uBC14\uC774\uC2A4\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4. */\n registerPushDevice(\n accountSeq: number,\n deviceId: string,\n pushToken: string,\n opts: RegisterPushDeviceOptions = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const {\n platform,\n deviceType,\n browser,\n browserVersion,\n pushEnabled = true,\n transactionId,\n } = opts;\n\n return this.submit(\n \"account_device\",\n {\n id: deviceId,\n account_seq: accountSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n ...(platform ? { platform } : {}),\n ...(deviceType ? { device_type: deviceType } : {}),\n ...(browser ? { browser } : {}),\n ...(browserVersion ? { browser_version: browserVersion } : {}),\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4 \uB808\uCF54\uB4DC\uC758 \uD478\uC2DC \uD1A0\uD070\uC744 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n updatePushDeviceToken(\n deviceSeq: number,\n pushToken: string,\n opts: { pushEnabled?: boolean; transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const { pushEnabled = true, transactionId } = opts;\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4\uC758 \uD478\uC2DC \uC218\uC2E0\uC744 \uBE44\uD65C\uC131\uD654\uD569\uB2C8\uB2E4. */\n disablePushDevice(\n deviceSeq: number,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_enabled: false,\n },\n { transactionId: opts.transactionId },\n );\n }\n\n /**\n * \uC694\uCCAD \uBC14\uB514\uB97C \uD30C\uC2F1\uD558\uACE0 `application/octet-stream`\uC778 \uACBD\uC6B0 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC6D0\uC2DC \uC554\uD638\uD654 payload\uB97C \uC9C1\uC811 \uB2E4\uB8E8\uB294 \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n */\n readRequestBody<T = Record<string, unknown>>(\n body: ArrayBuffer | Uint8Array | string | T | null | undefined,\n contentType = \"application/json\",\n requireEncrypted = false,\n ): T {\n const lowered = contentType.toLowerCase();\n const isEncrypted = lowered.includes(\"application/octet-stream\");\n\n if (requireEncrypted && !isEncrypted) {\n throw new Error(\n \"Encrypted request required: Content-Type must be application/octet-stream\",\n );\n }\n\n if (isEncrypted) {\n if (body == null) {\n throw new Error(\"Encrypted request body is empty\");\n }\n if (body instanceof ArrayBuffer) {\n return this.decryptPacket<T>(body);\n }\n if (body instanceof Uint8Array) {\n const sliced = body.buffer.slice(\n body.byteOffset,\n body.byteOffset + body.byteLength,\n );\n return this.decryptPacket<T>(sliced as ArrayBuffer);\n }\n throw new Error(\n \"Encrypted request body must be ArrayBuffer or Uint8Array\",\n );\n }\n\n if (body == null || body === \"\") return {} as T;\n if (typeof body === \"string\") return JSON.parse(body) as T;\n return body as T;\n }\n\n /**\n * \uACF5\uD1B5 HTTP \uC694\uCCAD \uD568\uC218\uC785\uB2C8\uB2E4.\n *\n * - `encryptRequests`\uAC00 \uD65C\uC131\uD654\uB41C \uC778\uC99D \uC694\uCCAD\uC758 POST \uBC14\uB514\uB97C \uC790\uB3D9 \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * - \uC751\uB2F5\uC774 `application/octet-stream`\uC774\uBA74 \uC790\uB3D9 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n * - JSON \uC751\uB2F5\uC758 `ok`\uAC00 false\uC774\uBA74 \uC5D0\uB7EC\uB97C \uB358\uC9D1\uB2C8\uB2E4.\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n withAuth = true,\n extraHeaders: Record<string, string> = {},\n ): Promise<T> {\n const isHmacMode = withAuth && !!(this.apiKey && this.hmacSecret);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...extraHeaders,\n };\n if (!isHmacMode && withAuth && this.token) {\n headers.Authorization = `Bearer ${this.token}`;\n }\n\n // \uC694\uCCAD \uBC14\uB514 \uACB0\uC815: encryptRequests \uD65C\uC131\uD654 \uC2DC POST \uBC14\uB514\uB97C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n // - \uB85C\uADF8\uC778/\uD1A0\uD070 \uAC31\uC2E0(withAuth=false)\uC740 \uC554\uD638\uD654\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.\n // - GET \uC740 \uBC14\uB514\uAC00 \uC5C6\uC73C\uBBC0\uB85C \uAC74\uB108\uB701\uB2C8\uB2E4.\n // - HMAC \uBAA8\uB4DC\uB294 token \uC5C6\uC774\uB3C4 hmacSecret \uC774 \uC788\uC73C\uBA74 \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n let fetchBody: string | Uint8Array | null = null;\n if (body != null) {\n const shouldEncrypt =\n this.encryptRequests &&\n withAuth &&\n (this.token || isHmacMode) &&\n method !== \"GET\" &&\n method !== \"HEAD\";\n\n if (shouldEncrypt) {\n const plaintext = new TextEncoder().encode(\n JSON.stringify(body),\n );\n const encrypted = this.encryptPacket(plaintext);\n headers[\"Content-Type\"] = \"application/octet-stream\";\n fetchBody = encrypted;\n } else {\n fetchBody = JSON.stringify(body);\n }\n }\n\n // HMAC \uBAA8\uB4DC: X-API-Key / X-Timestamp / X-Nonce / X-Signature \uD5E4\uB354 \uCD94\uAC00\n if (isHmacMode) {\n const timestamp = String(Math.floor(Date.now() / 1000));\n const nonce = crypto.randomUUID();\n const bodyBytes =\n fetchBody instanceof Uint8Array\n ? fetchBody\n : typeof fetchBody === \"string\"\n ? new TextEncoder().encode(fetchBody)\n : new Uint8Array(0);\n const prefix = new TextEncoder().encode(\n `${method}|${path}|${timestamp}|${nonce}|`,\n );\n const payload = new Uint8Array(prefix.length + bodyBytes.length);\n payload.set(prefix, 0);\n payload.set(bodyBytes, prefix.length);\n const sig = hmac(\n sha256,\n new TextEncoder().encode(this.hmacSecret),\n payload,\n );\n const signature = [...sig]\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n headers[\"X-API-Key\"] = this.apiKey;\n headers[\"X-Timestamp\"] = timestamp;\n headers[\"X-Nonce\"] = nonce;\n headers[\"X-Signature\"] = signature;\n }\n\n const res = await fetch(this.baseUrl + path, {\n method,\n headers,\n ...(fetchBody != null ? { body: fetchBody as BodyInit } : {}),\n });\n\n const contentType = res.headers.get(\"Content-Type\") ?? \"\";\n\n if (contentType.includes(\"application/octet-stream\")) {\n const buffer = await res.arrayBuffer();\n return this.decryptPacket<T>(buffer);\n }\n\n const data = await res.json();\n if (!data.ok) {\n const err = new Error(\n data.message ?? `EntityServer error (HTTP ${res.status})`,\n );\n (err as { status?: number }).status = res.status;\n throw err;\n }\n return data as T;\n }\n\n /**\n * \uD328\uD0B7 \uC554\uD638\uD654 \uD0A4\uB97C \uC720\uB3C4\uD569\uB2C8\uB2E4.\n * - HMAC \uBAA8\uB4DC (`hmacSecret` \uC124\uC815 \uC2DC): HKDF-SHA256(hmac_secret, \"entity-server:packet-encryption\")\n * - JWT \uBAA8\uB4DC: sha256(jwt_token)\n */\n private derivePacketKey(): Uint8Array {\n if (this.hmacSecret) {\n const salt = new TextEncoder().encode(\"entity-server:hkdf:v1\");\n const info = new TextEncoder().encode(\n \"entity-server:packet-encryption\",\n );\n return hkdf(\n sha256,\n new TextEncoder().encode(this.hmacSecret),\n salt,\n info,\n 32,\n );\n }\n return sha256(new TextEncoder().encode(this.token));\n }\n\n /**\n * \uD3C9\uBB38 \uBC14\uC774\uD2B8\uB97C XChaCha20-Poly1305\uB85C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * \uD3EC\uB9F7: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]\n */\n private encryptPacket(plaintext: Uint8Array): Uint8Array {\n const key = this.derivePacketKey();\n const magic = new Uint8Array(this.packetMagicLen);\n const nonce = new Uint8Array(24);\n crypto.getRandomValues(magic);\n crypto.getRandomValues(nonce);\n const cipher = xchacha20poly1305(key, nonce);\n const ciphertext = cipher.encrypt(plaintext);\n const result = new Uint8Array(\n this.packetMagicLen + 24 + ciphertext.length,\n );\n result.set(magic, 0);\n result.set(nonce, this.packetMagicLen);\n result.set(ciphertext, this.packetMagicLen + 24);\n return result;\n }\n\n /** \uC11C\uBC84\uC758 \uC554\uD638\uD654 \uD328\uD0B7\uC744 \uBCF5\uD638\uD654\uD574 JSON \uAC1D\uCCB4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\n private decryptPacket<T>(buffer: ArrayBuffer): T {\n const key = this.derivePacketKey();\n const data = new Uint8Array(buffer);\n\n if (data.length < this.packetMagicLen + 24 + 16) {\n throw new Error(\"Encrypted packet too short\");\n }\n\n const nonce = data.slice(this.packetMagicLen, this.packetMagicLen + 24);\n const ciphertext = data.slice(this.packetMagicLen + 24);\n const cipher = xchacha20poly1305(key, nonce);\n const plaintext = cipher.decrypt(ciphertext);\n return JSON.parse(new TextDecoder().decode(plaintext)) as T;\n }\n}\n\n/** \uCFFC\uB9AC \uD30C\uB77C\uBBF8\uD130 \uAC1D\uCCB4\uB97C URL \uCFFC\uB9AC \uBB38\uC790\uC5F4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\nfunction buildQuery(params: Record<string, unknown>): string {\n return Object.entries(params)\n .filter(([, value]) => value != null)\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key === \"orderBy\" ? \"order_by\" : key)}=${encodeURIComponent(String(value))}`,\n )\n .join(\"&\");\n}\n\nexport const entityServer = new EntityServerClient();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["xchacha20poly1305", "sha256", "hkdf", "hmac", "readEnv", "name", "EntityServerClient", "options", "envBaseUrl", "envMagicLen", "token", "apiKey", "secret", "length", "refreshToken", "expiresIn", "delayMs", "result", "err", "data", "email", "password", "res", "transactionId", "txId", "entity", "seq", "opts", "q", "conditions", "params", "fields", "orderDir", "orderBy", "rest", "queryObj", "buildQuery", "req", "extraHeaders", "historySeq", "pushEntity", "payload", "accountSeq", "deviceId", "pushToken", "platform", "deviceType", "browser", "browserVersion", "pushEnabled", "deviceSeq", "body", "contentType", "requireEncrypted", "isEncrypted", "sliced", "method", "path", "withAuth", "isHmacMode", "headers", "fetchBody", "plaintext", "encrypted", "timestamp", "nonce", "bodyBytes", "prefix", "signature", "b", "buffer", "salt", "info", "key", "magic", "ciphertext", "value", "entityServer"]
|
|
7
7
|
}
|
package/dist/react.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useCallback as
|
|
1
|
+
import{useCallback as v,useEffect as q,useMemo as O,useRef as I,useState as L}from"react";import{xchacha20poly1305 as w}from"@noble/ciphers/chacha";import{sha256 as S}from"@noble/hashes/sha2";import{hkdf as U}from"@noble/hashes/hkdf";import{hmac as $}from"@noble/hashes/hmac";function P(k){return import.meta?.env?.[k]}var b=class{baseUrl;token;apiKey;hmacSecret;packetMagicLen;encryptRequests;activeTxId=null;keepSession;refreshBuffer;onTokenRefreshed;onSessionExpired;_sessionRefreshToken=null;_refreshTimer=null;constructor(e={}){let t=P("VITE_ENTITY_SERVER_URL"),r=P("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:o,orderBy:s,...i}=t,a={page:1,limit:20,...i};s&&(a.orderBy=o==="DESC"?`-${s}`:s),n?.length&&(a.fields=n.join(","));let h=_(a);return this.request("POST",`/v1/entity/${e}/list?${h}`,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,o=n?{"X-Transaction-ID":n}:void 0,s=r.skipHooks?"?skipHooks=true":"";return this.request("POST",`/v1/entity/${e}/submit${s}`,t,!0,o)}delete(e,t,r={}){let n=new URLSearchParams;r.hard&&n.set("hard","true"),r.skipHooks&&n.set("skipHooks","true");let o=n.size?`?${n}`:"",s=r.transactionId??this.activeTxId,i=s?{"X-Transaction-ID":s}:void 0;return this.request("POST",`/v1/entity/${e}/delete/${t}${o}`,void 0,!0,i)}history(e,t,r={}){let n=_({page:1,limit:50,...r});return this.request("GET",`/v1/entity/${e}/history/${t}?${n}`)}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:o,deviceType:s,browser:i,browserVersion:a,pushEnabled:h=!0,transactionId:m}=n;return this.submit("account_device",{id:t,account_seq:e,push_token:r,push_enabled:h,...o?{platform:o}:{},...s?{device_type:s}:{},...i?{browser:i}:{},...a?{browser_version:a}:{}},{transactionId:m})}updatePushDeviceToken(e,t,r={}){let{pushEnabled:n=!0,transactionId:o}=r;return this.submit("account_device",{seq:e,push_token:t,push_enabled:n},{transactionId:o})}disablePushDevice(e,t={}){return this.submit("account_device",{seq:e,push_enabled:!1},{transactionId:t.transactionId})}readRequestBody(e,t="application/json",r=!1){let o=t.toLowerCase().includes("application/octet-stream");if(r&&!o)throw new Error("Encrypted request required: Content-Type must be application/octet-stream");if(o){if(e==null)throw new Error("Encrypted request body is empty");if(e instanceof ArrayBuffer)return this.decryptPacket(e);if(e instanceof Uint8Array){let s=e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength);return this.decryptPacket(s)}throw new Error("Encrypted request body must be ArrayBuffer or Uint8Array")}return e==null||e===""?{}:typeof e=="string"?JSON.parse(e):e}async request(e,t,r,n=!0,o={}){let s=n&&!!(this.apiKey&&this.hmacSecret),i={"Content-Type":"application/json",...o};!s&&n&&this.token&&(i.Authorization=`Bearer ${this.token}`);let a=null;if(r!=null)if(this.encryptRequests&&n&&(this.token||s)&&e!=="GET"&&e!=="HEAD"){let u=new TextEncoder().encode(JSON.stringify(r)),d=this.encryptPacket(u);i["Content-Type"]="application/octet-stream",a=d}else a=JSON.stringify(r);if(s){let p=String(Math.floor(Date.now()/1e3)),u=crypto.randomUUID(),d=a instanceof Uint8Array?a:typeof a=="string"?new TextEncoder().encode(a):new Uint8Array(0),g=new TextEncoder().encode(`${e}|${t}|${p}|${u}|`),T=new Uint8Array(g.length+d.length);T.set(g,0),T.set(d,g.length);let E=[...$(S,new TextEncoder().encode(this.hmacSecret),T)].map(c=>c.toString(16).padStart(2,"0")).join("");i["X-API-Key"]=this.apiKey,i["X-Timestamp"]=p,i["X-Nonce"]=u,i["X-Signature"]=E}let h=await fetch(this.baseUrl+t,{method:e,headers:i,...a!=null?{body:a}:{}});if((h.headers.get("Content-Type")??"").includes("application/octet-stream")){let p=await h.arrayBuffer();return this.decryptPacket(p)}let f=await h.json();if(!f.ok){let p=new Error(f.message??`EntityServer error (HTTP ${h.status})`);throw p.status=h.status,p}return f}derivePacketKey(){if(this.hmacSecret){let e=new TextEncoder().encode("entity-server:hkdf:v1"),t=new TextEncoder().encode("entity-server:packet-encryption");return U(S,new TextEncoder().encode(this.hmacSecret),e,t,32)}return S(new TextEncoder().encode(this.token))}encryptPacket(e){let t=this.derivePacketKey(),r=new Uint8Array(this.packetMagicLen),n=new Uint8Array(24);crypto.getRandomValues(r),crypto.getRandomValues(n);let s=w(t,n).encrypt(e),i=new Uint8Array(this.packetMagicLen+24+s.length);return i.set(r,0),i.set(n,this.packetMagicLen),i.set(s,this.packetMagicLen+24),i}decryptPacket(e){let t=this.derivePacketKey(),r=new Uint8Array(e);if(r.length<this.packetMagicLen+24+16)throw new Error("Encrypted packet too short");let n=r.slice(this.packetMagicLen,this.packetMagicLen+24),o=r.slice(this.packetMagicLen+24),i=w(t,n).decrypt(o);return JSON.parse(new TextDecoder().decode(i))}};function _(k){return Object.entries(k).filter(([,e])=>e!=null).map(([e,t])=>`${encodeURIComponent(e==="orderBy"?"order_by":e)}=${encodeURIComponent(String(t))}`).join("&")}var x=new b;function N(k={}){let{singleton:e=!0,tokenResolver:t,baseUrl:r,packetMagicLen:n,token:o,resumeSession:s}=k,[i,a]=L(!1),[h,m]=L(null),f=I(!0);q(()=>(f.current=!0,()=>{f.current=!1}),[]);let p=I(s);q(()=>{let c=p.current;c&&u.refreshToken(c).catch(()=>{})},[]);let u=O(()=>{let c=e?x:new b({baseUrl:r,packetMagicLen:n,token:o});e&&c.configure({baseUrl:r,packetMagicLen:n,token:o});let l=t?.();return typeof l=="string"&&c.setToken(l),c},[e,t,r,n,o]),d=v(async c=>{f.current&&(a(!0),m(null));try{return await c()}catch(l){let y=l instanceof Error?l:new Error(String(l));throw f.current&&m(y),y}finally{f.current&&a(!1)}},[]),g=v((c,l,y)=>d(()=>u.submit(c,l,y)),[u,d]),T=v((c,l,y)=>d(()=>u.delete(c,l,y)),[u,d]),R=v((c,l)=>d(()=>u.query(c,l)),[u,d]),E=v(()=>{a(!1),m(null)},[]);return{client:u,isPending:i,error:h,reset:E,submit:g,del:T,query:R}}export{N as useEntityServer};
|
|
2
2
|
//# sourceMappingURL=react.js.map
|
package/dist/react.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/hooks/useEntityServer.ts", "../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n EntityServerClient,\n entityServer,\n type EntityListParams,\n type EntityQueryRequest,\n type EntityServerClientOptions,\n} from \"../index\";\n\nexport interface UseEntityServerOptions extends EntityServerClientOptions {\n singleton?: boolean;\n tokenResolver?: () => string | undefined | null;\n /**\n * \uD398\uC774\uC9C0 \uC0C8\uB85C\uACE0\uCE68 \uD6C4 \uB85C\uADF8\uC778 \uC0C1\uD0DC\uB97C \uBCF5\uC6D0\uD560 \uB54C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n * \uC774 \uAC12\uC774 \uC788\uC73C\uBA74 \uB9C8\uC6B4\uD2B8 \uC2DC `client.refreshToken()`\uC744 \uD638\uCD9C\uD574 \uC0C8 access_token\uC744 \uBC1C\uAE09\uBC1B\uC2B5\uB2C8\uB2E4.\n * `keepSession: true`\uC640 \uD568\uAED8 \uC0AC\uC6A9\uD558\uBA74 \uC138\uC158 \uC720\uC9C0 \uD0C0\uC774\uBA38\uB3C4 \uC7AC\uC2DC\uC791\uB429\uB2C8\uB2E4.\n * \uAC31\uC2E0 \uC131\uACF5 \uC2DC `onTokenRefreshed` \uCF5C\uBC31\uC774 \uD638\uCD9C\uB429\uB2C8\uB2E4.\n */\n resumeSession?: string;\n}\n\nexport interface UseEntityServerResult {\n /** EntityServerClient \uC778\uC2A4\uD134\uC2A4 (read \uC804\uC6A9 \uBA54\uC11C\uB4DC \uC9C1\uC811 \uD638\uCD9C \uC2DC \uC0AC\uC6A9) */\n client: EntityServerClient;\n /** submit \uB610\uB294 delete \uC9C4\uD589 \uC911 \uC5EC\uBD80 */\n isPending: boolean;\n /** \uB9C8\uC9C0\uB9C9 mutation \uC5D0\uB7EC (\uC5C6\uC73C\uBA74 null) */\n error: Error | null;\n /** \uC5D0\uB7EC\u00B7\uACB0\uACFC \uC0C1\uD0DC \uCD08\uAE30\uD654 */\n reset: () => void;\n /** entity \uB370\uC774\uD130 \uC0DD\uC131/\uC218\uC815 (seq \uC5C6\uC73C\uBA74 INSERT, \uC788\uC73C\uBA74 UPDATE) */\n submit: (\n entity: string,\n data: Record<string, unknown>,\n opts?: { transactionId?: string; skipHooks?: boolean },\n ) => Promise<{ ok: boolean; seq: number }>;\n /** entity \uB370\uC774\uD130 \uC0AD\uC81C */\n del: (\n entity: string,\n seq: number,\n opts?: { transactionId?: string; hard?: boolean; skipHooks?: boolean },\n ) => Promise<{ ok: boolean; deleted: number }>;\n /** \uCEE4\uC2A4\uD140 SQL \uC870\uD68C */\n query: <T = unknown>(\n entity: string,\n req: EntityQueryRequest,\n ) => Promise<{ ok: boolean; data: { items: T[]; count: number } }>;\n}\n\n/**\n * React \uD658\uACBD\uC5D0\uC11C EntityServerClient \uC778\uC2A4\uD134\uC2A4\uC640 mutation \uC0C1\uD0DC\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.\n *\n * - `singleton=true`(\uAE30\uBCF8): \uD328\uD0A4\uC9C0 \uC804\uC5ED `entityServer` \uC778\uC2A4\uD134\uC2A4\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n * - `singleton=false`: \uCEF4\uD3EC\uB10C\uD2B8 \uC2A4\uCF54\uD504\uC758 \uC0C8 \uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4.\n *\n * @example\n * ```tsx\n * const { submit, del, isPending, error, reset } = useEntityServer();\n *\n * const handleSave = async () => {\n * await submit(\"account\", { name: \"\uD64D\uAE38\uB3D9\" });\n * };\n * ```\n */\nexport function useEntityServer(\n options: UseEntityServerOptions = {},\n): UseEntityServerResult {\n const {\n singleton = true,\n tokenResolver,\n baseUrl,\n packetMagicLen,\n token,\n resumeSession,\n } = options;\n\n const [isPending, setIsPending] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // \uC5B8\uB9C8\uC6B4\uD2B8 \uD6C4 setState \uBC29\uC9C0\n const mountedRef = useRef(true);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n // \uC0C8\uB85C\uACE0\uCE68 \uD6C4 \uB85C\uADF8\uC778 \uC0C1\uD0DC \uBCF5\uC6D0: resumeSession\uC774 \uC788\uC73C\uBA74 \uB9C8\uC6B4\uD2B8 \uC2DC refreshToken() \uD638\uCD9C\n const resumeTokenRef = useRef(resumeSession);\n useEffect(() => {\n const storedRefreshToken = resumeTokenRef.current;\n if (!storedRefreshToken) return;\n client.refreshToken(storedRefreshToken).catch(() => {\n // refresh_token \uB9CC\uB8CC \uB4F1 \u2014 onSessionExpired \uCF5C\uBC31\uC774 \uC774\uBBF8 \uCC98\uB9AC\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const client = useMemo(() => {\n const c = singleton\n ? entityServer\n : new EntityServerClient({ baseUrl, packetMagicLen, token });\n\n if (singleton) {\n c.configure({ baseUrl, packetMagicLen, token });\n }\n\n const resolvedToken = tokenResolver?.();\n if (typeof resolvedToken === \"string\") {\n c.setToken(resolvedToken);\n }\n\n return c;\n }, [singleton, tokenResolver, baseUrl, packetMagicLen, token]);\n\n const run = useCallback(async <T>(fn: () => Promise<T>): Promise<T> => {\n if (mountedRef.current) {\n setIsPending(true);\n setError(null);\n }\n try {\n const result = await fn();\n return result;\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err));\n if (mountedRef.current) setError(e);\n throw e;\n } finally {\n if (mountedRef.current) setIsPending(false);\n }\n }, []);\n\n const submit = useCallback<UseEntityServerResult[\"submit\"]>(\n (entity, data, opts) => run(() => client.submit(entity, data, opts)),\n [client, run],\n );\n\n const del = useCallback<UseEntityServerResult[\"del\"]>(\n (entity, seq, opts) => run(() => client.delete(entity, seq, opts)),\n [client, run],\n );\n\n const query = useCallback<UseEntityServerResult[\"query\"]>(\n (entity, req) => run(() => client.query(entity, req)),\n [client, run],\n );\n\n const reset = useCallback(() => {\n setIsPending(false);\n setError(null);\n }, []);\n\n return { client, isPending, error, reset, submit, del, query };\n}\n", "// @ts-ignore\nimport { xchacha20poly1305 } from \"@noble/ciphers/chacha\";\n// @ts-ignore\nimport { sha256 } from \"@noble/hashes/sha2\";\n\n/**\n * \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D \uC870\uD68C \uD30C\uB77C\uBBF8\uD130\uC785\uB2C8\uB2E4.\n *\n * ```ts\n * client.list(\"post\", {\n * page: 1, limit: 10,\n * orderBy: \"created_time\", orderDir: \"DESC\",\n * fields: [\"seq\", \"title\", \"created_time\"],\n * conditions: { status: \"active\" },\n * });\n * ```\n */\nexport interface EntityListParams {\n /** \uC870\uD68C \uD398\uC774\uC9C0 \uBC88\uD638. \uAE30\uBCF8\uAC12: `1` */\n page?: number;\n /** \uD398\uC774\uC9C0\uB2F9 \uB808\uCF54\uB4DC \uC218. \uAE30\uBCF8\uAC12: `20` */\n limit?: number;\n /** \uC815\uB82C \uAE30\uC900 \uD544\uB4DC\uBA85 */\n orderBy?: string;\n /** \uC815\uB82C \uBC29\uD5A5. \uAE30\uBCF8\uAC12: `\"ASC\"` */\n orderDir?: \"ASC\" | \"DESC\";\n /**\n * \uBC18\uD658\uD560 \uD544\uB4DC \uBAA9\uB85D.\n *\n * - **\uBBF8\uC9C0\uC815 (\uAE30\uBCF8\uAC12)**: \uC5D4\uD2F0\uD2F0\uC758 \uC778\uB371\uC2A4 \uD544\uB4DC\uB9CC \uBC18\uD658\uD569\uB2C8\uB2E4.\n * \uBCF5\uD638\uD654\uB97C \uAC74\uB108\uB6F0\uAE30 \uB54C\uBB38\uC5D0 **\uAC00\uC7A5 \uBE60\uB985\uB2C8\uB2E4**.\n * - `[\"*\"]`: \uC804\uCCB4 \uD544\uB4DC \uBC18\uD658 (\uBCF5\uD638\uD654 \uC218\uD589).\n * - \uD544\uB4DC\uBA85 \uBAA9\uB85D: \uD574\uB2F9 \uD544\uB4DC\uB9CC \uBC18\uD658\uD569\uB2C8\uB2E4.\n * \uC5D4\uD2F0\uD2F0 \uC124\uC815\uC5D0 `index`\uB85C \uC120\uC5B8\uB41C \uD544\uB4DC\uB9CC \uC9C0\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * \uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 \uD544\uB4DC\uBA85\uC744 \uC9C0\uC815\uD558\uBA74 \uC11C\uBC84 \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD569\uB2C8\uB2E4.\n * - `seq`, `created_time`, `updated_time`, `license_seq`\uB294 \uD544\uB4DC\uC5D0 \uAD00\uACC4\uC5C6\uC774 \uD56D\uC0C1 \uD3EC\uD568\uB429\uB2C8\uB2E4.\n *\n * ```ts\n * // \uAE30\uBCF8\uAC12 (\uC778\uB371\uC2A4 \uD544\uB4DC\uB9CC, \uAC00\uC7A5 \uBE60\uB984)\n * client.list(\"account\")\n * // \uC804\uCCB4 \uD544\uB4DC\n * client.list(\"account\", { fields: [\"*\"] })\n * // seq, name, email\uB9CC\n * client.list(\"account\", { fields: [\"seq\", \"name\", \"email\"] })\n * ```\n */\n fields?: string[];\n /** \uD544\uD130 \uC870\uAC74. POST body\uB85C \uC804\uB2EC\uB429\uB2C8\uB2E4. (\uC608: `{ status: \"active\" }`) */\n conditions?: Record<string, unknown>;\n}\n\n/**\n * `query()` \uBA54\uC11C\uB4DC\uC5D0 \uC804\uB2EC\uD558\uB294 SQL \uCFFC\uB9AC \uC694\uCCAD\uC785\uB2C8\uB2E4.\n *\n * - `sql`: SELECT \uC804\uC6A9 SQL. \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD558\uBA70 JOIN \uC9C0\uC6D0.\n * - `params`: SQL \uBC14\uC778\uB529 \uD30C\uB77C\uBBF8\uD130 (`?` \uD50C\uB808\uC774\uC2A4\uD640\uB354 \uB300\uC751).\n * - `limit`: \uCD5C\uB300 \uBC18\uD658 \uAC74\uC218 (\uCD5C\uB300 1000. \uBBF8\uC9C0\uC815 \uC2DC \uC11C\uBC84 \uAE30\uBCF8\uAC12 \uC801\uC6A9).\n *\n * ```ts\n * client.query(\"order\", {\n * sql: `SELECT o.seq, o.status, u.name\n * FROM order o\n * JOIN account u ON u.data_seq = o.account_seq\n * WHERE o.status = ?`,\n * params: [\"pending\"],\n * limit: 100,\n * });\n * ```\n */\nexport interface EntityQueryRequest {\n sql: string;\n params?: unknown[];\n limit?: number;\n}\n\nexport interface RegisterPushDeviceOptions {\n platform?: string;\n deviceType?: string;\n browser?: string;\n browserVersion?: string;\n pushEnabled?: boolean;\n transactionId?: string;\n}\n\n/** EntityServerClient \uC0DD\uC131/\uC124\uC815 \uC635\uC158\uC785\uB2C8\uB2E4. */\nexport interface EntityServerClientOptions {\n baseUrl?: string;\n token?: string;\n packetMagicLen?: number;\n /**\n * `true`\uC774\uBA74 \uC778\uC99D\uB41C POST/PUT \uC694\uCCAD \uBC14\uB514\uB97C XChaCha20-Poly1305\uB85C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uC758 `EnablePacketEncryption`\uC774 \uD65C\uC131\uD654\uB41C \uACBD\uC6B0 \uD544\uC218\uB85C \uC124\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.\n * \uB85C\uADF8\uC778(`login()`)\u00B7\uD1A0\uD070 \uAC31\uC2E0(`refreshToken()`)\uC740 \uC778\uC99D \uC804 \uC694\uCCAD\uC774\uBBC0\uB85C \uC790\uB3D9\uC73C\uB85C \uAC74\uB108\uB701\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12: `false`\n */\n encryptRequests?: boolean;\n /**\n * `true`\uC774\uBA74 `login()` \uC131\uACF5 \uD6C4 Access Token \uB9CC\uB8CC \uC804\uC5D0 \uC790\uB3D9\uC73C\uB85C \uAC31\uC2E0(silent refresh)\uD569\uB2C8\uB2E4.\n * \uAC31\uC2E0 \uC2DC\uC810\uC740 `expires_in - refreshBuffer` \uCD08\uC785\uB2C8\uB2E4.\n *\n * \uAC31\uC2E0 \uC131\uACF5 \uC2DC `onTokenRefreshed`, \uC2E4\uD328 \uC2DC `onSessionExpired` \uCF5C\uBC31\uC774 \uD638\uCD9C\uB429\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12: `false`\n */\n keepSession?: boolean;\n /**\n * \uB9CC\uB8CC \uBA87 \uCD08 \uC804\uC5D0 \uC790\uB3D9 \uAC31\uC2E0\uC744 \uC2DC\uB3C4\uD560\uC9C0 \uC124\uC815\uD569\uB2C8\uB2E4.\n *\n * \uC608: `expires_in = 3600`, `refreshBuffer = 60` \u2192 3540\uCD08 \uD6C4 \uAC31\uC2E0\n *\n * \uAE30\uBCF8\uAC12: `60`\n */\n refreshBuffer?: number;\n /**\n * \uC790\uB3D9 \uAC31\uC2E0 \uC131\uACF5 \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\uC785\uB2C8\uB2E4.\n * \uC0C8 `access_token`\uACFC `expires_in`\uC774 \uC804\uB2EC\uB429\uB2C8\uB2E4.\n * \uC571\uC740 \uC774 \uCF5C\uBC31\uC5D0\uC11C localStorage \uB4F1\uC5D0 \uD1A0\uD070\uC744 \uC800\uC7A5\uD574\uC57C \uD569\uB2C8\uB2E4.\n */\n onTokenRefreshed?: (accessToken: string, expiresIn: number) => void;\n /**\n * \uC138\uC158 \uC720\uC9C0 \uAC31\uC2E0 \uC2E4\uD328 \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\uC785\uB2C8\uB2E4.\n * refresh_token \uB9CC\uB8CC \uB4F1\uC73C\uB85C \uC7AC\uBC1C\uAE09\uC774 \uBD88\uAC00\uB2A5\uD55C \uACBD\uC6B0\uC785\uB2C8\uB2E4.\n * \uC571\uC740 \uC774 \uCF5C\uBC31\uC5D0\uC11C \uB85C\uADF8\uC778 \uD398\uC774\uC9C0\uB85C \uC774\uB3D9\uD558\uB294 \uB4F1\uC758 \uCC98\uB9AC\uB97C \uD574\uC57C \uD569\uB2C8\uB2E4.\n */\n onSessionExpired?: (error: Error) => void;\n}\n\n/**\n * `list()`, `history()` \uC751\uB2F5\uC758 `data` \uD544\uB4DC \uAD6C\uC870\uC785\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uB294 \uD56D\uC0C1 \uC774 \uAD6C\uC870\uB85C \uBC18\uD658\uD569\uB2C8\uB2E4:\n * ```json\n * { \"ok\": true, \"data\": { \"items\": [...], \"total\": 100, \"page\": 1, \"limit\": 20 } }\n * ```\n */\nexport interface EntityListResult<T = unknown> {\n items: T[];\n /** \uC804\uCCB4 \uB808\uCF54\uB4DC \uC218 */\n total: number;\n /** \uD604\uC7AC \uD398\uC774\uC9C0 \uBC88\uD638 */\n page: number;\n /** \uD398\uC774\uC9C0\uB2F9 \uB808\uCF54\uB4DC \uC218 */\n limit: number;\n}\n\n/**\n * `history()` \uC751\uB2F5\uC758 \uAC1C\uBCC4 \uC774\uB825 \uB808\uCF54\uB4DC \uAD6C\uC870\uC785\uB2C8\uB2E4.\n *\n * - `action`: `\"INSERT\"` | `\"UPDATE\"` | `\"DELETE_SOFT\"` | `\"DELETE_HARD\"` | `\"ROLLBACK\"`\n * - `data_snapshot`: \uBCC0\uACBD \uB2F9\uC2DC \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130 \uC2A4\uB0C5\uC0F7\n */\nexport interface EntityHistoryRecord<T = unknown> {\n seq: number;\n action:\n | \"INSERT\"\n | \"UPDATE\"\n | \"DELETE_SOFT\"\n | \"DELETE_HARD\"\n | \"ROLLBACK\"\n | string;\n data_snapshot: T | null;\n changed_by: number | null;\n changed_time: string;\n}\n\n/** Vite \uD658\uACBD\uBCC0\uC218(`import.meta.env`)\uC5D0\uC11C \uAC12\uC744 \uC77D\uC2B5\uB2C8\uB2E4. */\nfunction readEnv(name: string): string | undefined {\n const meta = import.meta as unknown as {\n env?: Record<string, string | undefined>;\n };\n return meta?.env?.[name];\n}\n\nexport class EntityServerClient {\n private baseUrl: string;\n private token: string;\n private packetMagicLen: number;\n private encryptRequests: boolean;\n private activeTxId: string | null = null;\n\n // \uC138\uC158 \uC720\uC9C0 \uAD00\uB828\n private keepSession: boolean;\n private refreshBuffer: number;\n private onTokenRefreshed?: (accessToken: string, expiresIn: number) => void;\n private onSessionExpired?: (error: Error) => void;\n private _sessionRefreshToken: string | null = null;\n private _refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * EntityServerClient \uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12:\n * - `baseUrl`: `VITE_ENTITY_SERVER_URL` \uB610\uB294 `http://localhost:47200`\n * - `packetMagicLen`: `VITE_ENTITY_SERVER_PACKET_MAGIC_LEN` \uB610\uB294 `4`\n */\n constructor(options: EntityServerClientOptions = {}) {\n const envBaseUrl = readEnv(\"VITE_ENTITY_SERVER_URL\");\n const envMagicLen = readEnv(\"VITE_ENTITY_SERVER_PACKET_MAGIC_LEN\");\n\n this.baseUrl = (\n options.baseUrl ??\n envBaseUrl ??\n \"http://localhost:47200\"\n ).replace(/\\/$/, \"\");\n\n this.token = options.token ?? \"\";\n this.packetMagicLen =\n options.packetMagicLen ?? (envMagicLen ? Number(envMagicLen) : 4);\n this.encryptRequests = options.encryptRequests ?? false;\n this.keepSession = options.keepSession ?? false;\n this.refreshBuffer = options.refreshBuffer ?? 60;\n this.onTokenRefreshed = options.onTokenRefreshed;\n this.onSessionExpired = options.onSessionExpired;\n }\n\n /** baseUrl, token, packetMagicLen, encryptRequests \uAC12\uC744 \uB7F0\uD0C0\uC784\uC5D0 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n configure(options: Partial<EntityServerClientOptions>): void {\n if (options.baseUrl) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n }\n if (typeof options.token === \"string\") {\n this.token = options.token;\n }\n if (typeof options.packetMagicLen === \"number\") {\n this.packetMagicLen = options.packetMagicLen;\n }\n if (typeof options.encryptRequests === \"boolean\") {\n this.encryptRequests = options.encryptRequests;\n }\n if (typeof options.keepSession === \"boolean\") {\n this.keepSession = options.keepSession;\n }\n if (typeof options.refreshBuffer === \"number\") {\n this.refreshBuffer = options.refreshBuffer;\n }\n if (options.onTokenRefreshed) {\n this.onTokenRefreshed = options.onTokenRefreshed;\n }\n if (options.onSessionExpired) {\n this.onSessionExpired = options.onSessionExpired;\n }\n }\n\n /** \uC778\uC99D \uC694\uCCAD\uC5D0 \uC0AC\uC6A9\uD560 JWT Access Token\uC744 \uC124\uC815\uD569\uB2C8\uB2E4. */\n setToken(token: string): void {\n this.token = token;\n }\n\n /** \uC554\uD638\uD654 \uD328\uD0B7 magic \uAE38\uC774(`packet_magic_len`)\uB97C \uC124\uC815\uD569\uB2C8\uB2E4. */\n setPacketMagicLen(length: number): void {\n this.packetMagicLen = length;\n }\n\n /** \uD604\uC7AC \uC554\uD638\uD654 \uD328\uD0B7 magic \uAE38\uC774\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4. */\n getPacketMagicLen(): number {\n return this.packetMagicLen;\n }\n\n /**\n * \uC790\uB3D9 \uD1A0\uD070 \uAC31\uC2E0 \uD0C0\uC774\uBA38\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4.\n * @param refreshToken \uAC31\uC2E0\uC5D0 \uC0AC\uC6A9\uD560 Refresh Token\n * @param expiresIn Access Token\uC758 \uC720\uD6A8 \uAE30\uAC04 (\uCD08)\n */\n private _scheduleKeepSession(\n refreshToken: string,\n expiresIn: number,\n ): void {\n this._clearRefreshTimer();\n this._sessionRefreshToken = refreshToken;\n\n const delayMs = Math.max((expiresIn - this.refreshBuffer) * 1000, 0);\n this._refreshTimer = setTimeout(async () => {\n if (!this._sessionRefreshToken) return;\n try {\n const result = await this.refreshToken(this._sessionRefreshToken);\n this.onTokenRefreshed?.(result.access_token, result.expires_in);\n // \uAC31\uC2E0 \uC131\uACF5 \uC2DC \uB2E4\uC74C \uB9CC\uB8CC \uC804 \uD0C0\uC774\uBA38 \uC7AC\uC608\uC57D\n this._scheduleKeepSession(\n this._sessionRefreshToken,\n result.expires_in,\n );\n } catch (err) {\n this._clearRefreshTimer();\n this.onSessionExpired?.(\n err instanceof Error ? err : new Error(String(err)),\n );\n }\n }, delayMs);\n }\n\n /** \uC790\uB3D9 \uAC31\uC2E0 \uD0C0\uC774\uBA38\uB97C \uC815\uB9AC\uD569\uB2C8\uB2E4. */\n private _clearRefreshTimer(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n /**\n * \uC138\uC158 \uC720\uC9C0 \uD0C0\uC774\uBA38\uB97C \uC911\uC9C0\uD569\uB2C8\uB2E4.\n * `logout()` \uD638\uCD9C \uC2DC \uC790\uB3D9\uC73C\uB85C \uC911\uC9C0\uB418\uBBC0\uB85C \uC9C1\uC811 \uD638\uCD9C\uC774 \uD544\uC694\uD55C \uACBD\uC6B0\uB294 \uB4DC\uBB45\uB2C8\uB2E4.\n */\n stopKeepSession(): void {\n this._clearRefreshTimer();\n this._sessionRefreshToken = null;\n }\n\n /**\n * \uC11C\uBC84 \uD5EC\uC2A4 \uCCB4\uD06C\uB97C \uC218\uD589\uD558\uACE0 \uD328\uD0B7 \uC554\uD638\uD654 \uD65C\uC131 \uC5EC\uBD80\uB97C \uC790\uB3D9\uC73C\uB85C \uAC10\uC9C0\uD569\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uAC00 `packet_encryption: true`\uB97C \uC751\uB2F5\uD558\uBA74 \uC774\uD6C4 \uBAA8\uB4E0 \uC694\uCCAD\uC5D0 \uC554\uD638\uD654\uAC00 \uC790\uB3D9 \uC801\uC6A9\uB429\uB2C8\uB2E4.\n * \uCD08\uAE30\uD654 \uC9C1\uD6C4 \uB610\uB294 \uB85C\uADF8\uC778 \uC804\uC5D0 \uD638\uCD9C\uD558\uBA74 \uC554\uD638\uD654 \uC124\uC815\uC744 \uC790\uB3D9\uC73C\uB85C \uAD6C\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n *\n * ```ts\n * await client.checkHealth();\n * await client.login(email, password); // \uC774\uD6C4 \uC694\uCCAD\uC740 \uC554\uD638\uD654 \uC790\uB3D9 \uC801\uC6A9\n * ```\n *\n * @returns `{ ok: true }` \uB610\uB294 `{ ok: true, packet_encryption: true }`\n */\n async checkHealth(): Promise<{ ok: boolean; packet_encryption?: boolean }> {\n const res = await fetch(`${this.baseUrl}/v1/health`, {\n signal: AbortSignal.timeout(3000),\n });\n const data = (await res.json()) as {\n ok: boolean;\n packet_encryption?: boolean;\n };\n if (data.packet_encryption) {\n this.encryptRequests = true;\n }\n return data;\n }\n\n /** \uB85C\uADF8\uC778 \uD6C4 `access_token`\uC744 \uB0B4\uBD80 \uC0C1\uD0DC\uC5D0 \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async login(\n email: string,\n password: string,\n ): Promise<{\n access_token: string;\n refresh_token: string;\n expires_in: number;\n }> {\n const data = await this.request<{\n data: {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n };\n }>(\"POST\", \"/v1/auth/login\", { email, passwd: password }, false);\n this.token = data.data.access_token;\n if (this.keepSession) {\n this._scheduleKeepSession(\n data.data.refresh_token,\n data.data.expires_in,\n );\n }\n return data.data;\n }\n\n /** Refresh Token\uC73C\uB85C Access Token\uC744 \uC7AC\uBC1C\uAE09\uBC1B\uC544 \uB0B4\uBD80 \uD1A0\uD070\uC744 \uAD50\uCCB4\uD569\uB2C8\uB2E4. */\n async refreshToken(\n refreshToken: string,\n ): Promise<{ access_token: string; expires_in: number }> {\n const data = await this.request<{\n data: { access_token: string; expires_in: number };\n }>(\"POST\", \"/v1/auth/refresh\", { refresh_token: refreshToken }, false);\n this.token = data.data.access_token;\n if (this.keepSession) {\n this._scheduleKeepSession(refreshToken, data.data.expires_in);\n }\n return data.data;\n }\n\n /**\n * \uC11C\uBC84\uC5D0 \uB85C\uADF8\uC544\uC6C3\uC744 \uC694\uCCAD\uD558\uACE0 \uB0B4\uBD80 \uD1A0\uD070\uC744 \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4.\n * refresh_token\uC744 \uC11C\uBC84\uC5D0 \uC804\uB2EC\uD574 \uBB34\uD6A8\uD654\uD569\uB2C8\uB2E4.\n */\n async logout(refreshToken: string): Promise<{ ok: boolean }> {\n this.stopKeepSession();\n const data = await this.request<{ ok: boolean }>(\n \"POST\",\n \"/v1/auth/logout\",\n { refresh_token: refreshToken },\n false,\n );\n this.token = \"\";\n return data;\n }\n\n /** \uD2B8\uB79C\uC7AD\uC158\uC744 \uC2DC\uC791\uD558\uACE0 \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158 ID\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async transStart(): Promise<string> {\n const res = await this.request<{ ok: boolean; transaction_id: string }>(\n \"POST\",\n \"/v1/transaction/start\",\n undefined,\n false,\n );\n this.activeTxId = res.transaction_id;\n return this.activeTxId;\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uB864\uBC31\uD569\uB2C8\uB2E4. */\n transRollback(transactionId?: string): Promise<{ ok: boolean }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/rollback/${txId}`);\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uCEE4\uBC0B\uD569\uB2C8\uB2E4.\n *\n * @returns `results` \uBC30\uC5F4: commit\uB41C \uAC01 \uC791\uC5C5\uC758 `entity`, `action`, `seq`\n */\n transCommit(transactionId?: string): Promise<{\n ok: boolean;\n results: Array<{ entity: string; action: string; seq: number }>;\n }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/commit/${txId}`);\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n get<T = unknown>(\n entity: string,\n seq: number,\n opts: { skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; data: T }> {\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n return this.request(\"GET\", `/v1/entity/${entity}/${seq}${q}`);\n }\n\n /** \uD398\uC774\uC9C0\uB124\uC774\uC158/\uC815\uB82C/\uD544\uD130 \uC870\uAC74\uC73C\uB85C \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n list<T = unknown>(\n entity: string,\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n const { conditions, fields, orderDir, orderBy, ...rest } = params;\n\n const queryObj: Record<string, unknown> = {\n page: 1,\n limit: 20,\n ...rest,\n };\n if (orderBy) {\n queryObj.orderBy = orderDir === \"DESC\" ? `-${orderBy}` : orderBy;\n }\n if (fields?.length) {\n queryObj.fields = fields.join(\",\");\n }\n\n const q = buildQuery(queryObj);\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/list?${q}`,\n conditions ?? {},\n );\n }\n\n /**\n * \uC5D4\uD2F0\uD2F0 \uCD1D \uAC74\uC218\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * @param conditions \uD544\uD130 \uC870\uAC74 (\uC608: `{ status: \"active\" }`)\n */\n count(\n entity: string,\n conditions?: Record<string, unknown>,\n ): Promise<{ ok: boolean; count: number }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/count`,\n conditions ?? {},\n );\n }\n\n /**\n * \uCEE4\uC2A4\uD140 SQL\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * SELECT \uC804\uC6A9\uC774\uBA70 \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * JOIN\uC744 \uC0AC\uC6A9\uD574 \uC5EC\uB7EC \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD569\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n * `entity`\uB294 SQL\uC5D0 \uD3EC\uD568\uB41C \uAE30\uBCF8 \uC5D4\uD2F0\uD2F0\uBA85(\uB77C\uC6B0\uD2B8 \uACBD\uB85C\uC6A9)\uC785\uB2C8\uB2E4.\n */\n query<T = unknown>(\n entity: string,\n req: EntityQueryRequest,\n ): Promise<{ ok: boolean; data: { items: T[]; count: number } }> {\n return this.request(\"POST\", `/v1/entity/${entity}/query`, req);\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130\uB97C \uC0DD\uC131/\uC218\uC815(Submit)\uD569\uB2C8\uB2E4. `seq`\uAC00 \uC5C6\uC73C\uBA74 INSERT, \uC788\uC73C\uBA74 UPDATE\uC785\uB2C8\uB2E4. */\n submit(\n entity: string,\n data: Record<string, unknown>,\n opts: { transactionId?: string; skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/submit${q}`,\n data,\n true,\n extraHeaders,\n );\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4(`hard=true`\uBA74 \uD558\uB4DC \uC0AD\uC81C, \uAE30\uBCF8\uC740 \uC18C\uD504\uD2B8 \uC0AD\uC81C). */\n delete(\n entity: string,\n seq: number,\n opts: {\n transactionId?: string;\n hard?: boolean;\n skipHooks?: boolean;\n } = {},\n ): Promise<{ ok: boolean; deleted: number }> {\n const params = new URLSearchParams();\n if (opts.hard) params.set(\"hard\", \"true\");\n if (opts.skipHooks) params.set(\"skipHooks\", \"true\");\n const q = params.size ? `?${params}` : \"\";\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/delete/${seq}${q}`,\n undefined,\n true,\n extraHeaders,\n );\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC758 \uBCC0\uACBD \uC774\uB825\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uC774\uB825 \uD56D\uBAA9\uB2F9 `action`, `data_snapshot`, `changed_by`, `changed_time`\uC744 \uD3EC\uD568\uD569\uB2C8\uB2E4. */\n history<T = unknown>(\n entity: string,\n seq: number,\n params: Pick<EntityListParams, \"page\" | \"limit\"> = {},\n ): Promise<{\n ok: boolean;\n data: EntityListResult<EntityHistoryRecord<T>>;\n }> {\n const q = buildQuery({ page: 1, limit: 50, ...params });\n return this.request(\"GET\", `/v1/entity/${entity}/history/${seq}?${q}`);\n }\n\n /** \uD2B9\uC815 \uC774\uB825 \uC2DC\uC810\uC73C\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uB864\uBC31\uD569\uB2C8\uB2E4. */\n rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/rollback/${historySeq}`,\n );\n }\n\n /** \uD478\uC2DC \uAD00\uB828 \uC5D4\uD2F0\uD2F0\uB85C payload\uB97C \uC804\uC1A1(Submit)\uD569\uB2C8\uB2E4. */\n push(\n pushEntity: string,\n payload: Record<string, unknown>,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(pushEntity, payload, opts);\n }\n\n /** \uD478\uC2DC \uB85C\uADF8 \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n pushLogList<T = unknown>(\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n return this.list<T>(\"push_log\", params);\n }\n\n /** \uACC4\uC815\uC758 \uD478\uC2DC \uB514\uBC14\uC774\uC2A4\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4. */\n registerPushDevice(\n accountSeq: number,\n deviceId: string,\n pushToken: string,\n opts: RegisterPushDeviceOptions = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const {\n platform,\n deviceType,\n browser,\n browserVersion,\n pushEnabled = true,\n transactionId,\n } = opts;\n\n return this.submit(\n \"account_device\",\n {\n id: deviceId,\n account_seq: accountSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n ...(platform ? { platform } : {}),\n ...(deviceType ? { device_type: deviceType } : {}),\n ...(browser ? { browser } : {}),\n ...(browserVersion ? { browser_version: browserVersion } : {}),\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4 \uB808\uCF54\uB4DC\uC758 \uD478\uC2DC \uD1A0\uD070\uC744 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n updatePushDeviceToken(\n deviceSeq: number,\n pushToken: string,\n opts: { pushEnabled?: boolean; transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const { pushEnabled = true, transactionId } = opts;\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4\uC758 \uD478\uC2DC \uC218\uC2E0\uC744 \uBE44\uD65C\uC131\uD654\uD569\uB2C8\uB2E4. */\n disablePushDevice(\n deviceSeq: number,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_enabled: false,\n },\n { transactionId: opts.transactionId },\n );\n }\n\n /**\n * \uC694\uCCAD \uBC14\uB514\uB97C \uD30C\uC2F1\uD558\uACE0 `application/octet-stream`\uC778 \uACBD\uC6B0 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC6D0\uC2DC \uC554\uD638\uD654 payload\uB97C \uC9C1\uC811 \uB2E4\uB8E8\uB294 \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n */\n readRequestBody<T = Record<string, unknown>>(\n body: ArrayBuffer | Uint8Array | string | T | null | undefined,\n contentType = \"application/json\",\n requireEncrypted = false,\n ): T {\n const lowered = contentType.toLowerCase();\n const isEncrypted = lowered.includes(\"application/octet-stream\");\n\n if (requireEncrypted && !isEncrypted) {\n throw new Error(\n \"Encrypted request required: Content-Type must be application/octet-stream\",\n );\n }\n\n if (isEncrypted) {\n if (body == null) {\n throw new Error(\"Encrypted request body is empty\");\n }\n if (body instanceof ArrayBuffer) {\n return this.decryptPacket<T>(body);\n }\n if (body instanceof Uint8Array) {\n const sliced = body.buffer.slice(\n body.byteOffset,\n body.byteOffset + body.byteLength,\n );\n return this.decryptPacket<T>(sliced as ArrayBuffer);\n }\n throw new Error(\n \"Encrypted request body must be ArrayBuffer or Uint8Array\",\n );\n }\n\n if (body == null || body === \"\") return {} as T;\n if (typeof body === \"string\") return JSON.parse(body) as T;\n return body as T;\n }\n\n /**\n * \uACF5\uD1B5 HTTP \uC694\uCCAD \uD568\uC218\uC785\uB2C8\uB2E4.\n *\n * - `encryptRequests`\uAC00 \uD65C\uC131\uD654\uB41C \uC778\uC99D \uC694\uCCAD\uC758 POST \uBC14\uB514\uB97C \uC790\uB3D9 \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * - \uC751\uB2F5\uC774 `application/octet-stream`\uC774\uBA74 \uC790\uB3D9 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n * - JSON \uC751\uB2F5\uC758 `ok`\uAC00 false\uC774\uBA74 \uC5D0\uB7EC\uB97C \uB358\uC9D1\uB2C8\uB2E4.\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n withAuth = true,\n extraHeaders: Record<string, string> = {},\n ): Promise<T> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...extraHeaders,\n };\n if (withAuth && this.token) {\n headers.Authorization = `Bearer ${this.token}`;\n }\n\n // \uC694\uCCAD \uBC14\uB514 \uACB0\uC815: encryptRequests \uD65C\uC131\uD654 \uC2DC POST \uBC14\uB514\uB97C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n // - \uB85C\uADF8\uC778/\uD1A0\uD070 \uAC31\uC2E0(withAuth=false)\uC740 \uC554\uD638\uD654\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.\n // - GET \uC740 \uBC14\uB514\uAC00 \uC5C6\uC73C\uBBC0\uB85C \uAC74\uB108\uB701\uB2C8\uB2E4.\n let fetchBody: string | Uint8Array | null = null;\n if (body != null) {\n const shouldEncrypt =\n this.encryptRequests &&\n withAuth &&\n this.token &&\n method !== \"GET\" &&\n method !== \"HEAD\";\n\n if (shouldEncrypt) {\n const plaintext = new TextEncoder().encode(\n JSON.stringify(body),\n );\n const encrypted = this.encryptPacket(plaintext);\n headers[\"Content-Type\"] = \"application/octet-stream\";\n fetchBody = encrypted;\n } else {\n fetchBody = JSON.stringify(body);\n }\n }\n\n const res = await fetch(this.baseUrl + path, {\n method,\n headers,\n ...(fetchBody != null ? { body: fetchBody as BodyInit } : {}),\n });\n\n const contentType = res.headers.get(\"Content-Type\") ?? \"\";\n\n if (contentType.includes(\"application/octet-stream\")) {\n const buffer = await res.arrayBuffer();\n return this.decryptPacket<T>(buffer);\n }\n\n const data = await res.json();\n if (!data.ok) {\n const err = new Error(\n data.message ?? `EntityServer error (HTTP ${res.status})`,\n );\n (err as { status?: number }).status = res.status;\n throw err;\n }\n return data as T;\n }\n\n /**\n * \uD3C9\uBB38 \uBC14\uC774\uD2B8\uB97C XChaCha20-Poly1305\uB85C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * \uD3EC\uB9F7: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]\n */\n private encryptPacket(plaintext: Uint8Array): Uint8Array {\n const key = sha256(new TextEncoder().encode(this.token));\n const magic = new Uint8Array(this.packetMagicLen);\n const nonce = new Uint8Array(24);\n crypto.getRandomValues(magic);\n crypto.getRandomValues(nonce);\n const cipher = xchacha20poly1305(key, nonce);\n const ciphertext = cipher.encrypt(plaintext);\n const result = new Uint8Array(\n this.packetMagicLen + 24 + ciphertext.length,\n );\n result.set(magic, 0);\n result.set(nonce, this.packetMagicLen);\n result.set(ciphertext, this.packetMagicLen + 24);\n return result;\n }\n\n /** \uC11C\uBC84\uC758 \uC554\uD638\uD654 \uD328\uD0B7\uC744 \uBCF5\uD638\uD654\uD574 JSON \uAC1D\uCCB4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\n private decryptPacket<T>(buffer: ArrayBuffer): T {\n const key = sha256(new TextEncoder().encode(this.token));\n const data = new Uint8Array(buffer);\n\n if (data.length < this.packetMagicLen + 24 + 16) {\n throw new Error(\"Encrypted packet too short\");\n }\n\n const nonce = data.slice(this.packetMagicLen, this.packetMagicLen + 24);\n const ciphertext = data.slice(this.packetMagicLen + 24);\n const cipher = xchacha20poly1305(key, nonce);\n const plaintext = cipher.decrypt(ciphertext);\n return JSON.parse(new TextDecoder().decode(plaintext)) as T;\n }\n}\n\n/** \uCFFC\uB9AC \uD30C\uB77C\uBBF8\uD130 \uAC1D\uCCB4\uB97C URL \uCFFC\uB9AC \uBB38\uC790\uC5F4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\nfunction buildQuery(params: Record<string, unknown>): string {\n return Object.entries(params)\n .filter(([, value]) => value != null)\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key === \"orderBy\" ? \"order_by\" : key)}=${encodeURIComponent(String(value))}`,\n )\n .join(\"&\");\n}\n\nexport const entityServer = new EntityServerClient();\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["useCallback", "useEffect", "useMemo", "useRef", "useState", "xchacha20poly1305", "sha256", "readEnv", "name", "EntityServerClient", "options", "envBaseUrl", "envMagicLen", "token", "length", "refreshToken", "expiresIn", "delayMs", "result", "err", "data", "email", "password", "res", "transactionId", "txId", "entity", "seq", "opts", "q", "
|
|
4
|
+
"sourcesContent": ["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n EntityServerClient,\n entityServer,\n type EntityListParams,\n type EntityQueryRequest,\n type EntityServerClientOptions,\n} from \"../index\";\n\nexport interface UseEntityServerOptions extends EntityServerClientOptions {\n singleton?: boolean;\n tokenResolver?: () => string | undefined | null;\n /**\n * \uD398\uC774\uC9C0 \uC0C8\uB85C\uACE0\uCE68 \uD6C4 \uB85C\uADF8\uC778 \uC0C1\uD0DC\uB97C \uBCF5\uC6D0\uD560 \uB54C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n * \uC774 \uAC12\uC774 \uC788\uC73C\uBA74 \uB9C8\uC6B4\uD2B8 \uC2DC `client.refreshToken()`\uC744 \uD638\uCD9C\uD574 \uC0C8 access_token\uC744 \uBC1C\uAE09\uBC1B\uC2B5\uB2C8\uB2E4.\n * `keepSession: true`\uC640 \uD568\uAED8 \uC0AC\uC6A9\uD558\uBA74 \uC138\uC158 \uC720\uC9C0 \uD0C0\uC774\uBA38\uB3C4 \uC7AC\uC2DC\uC791\uB429\uB2C8\uB2E4.\n * \uAC31\uC2E0 \uC131\uACF5 \uC2DC `onTokenRefreshed` \uCF5C\uBC31\uC774 \uD638\uCD9C\uB429\uB2C8\uB2E4.\n */\n resumeSession?: string;\n}\n\nexport interface UseEntityServerResult {\n /** EntityServerClient \uC778\uC2A4\uD134\uC2A4 (read \uC804\uC6A9 \uBA54\uC11C\uB4DC \uC9C1\uC811 \uD638\uCD9C \uC2DC \uC0AC\uC6A9) */\n client: EntityServerClient;\n /** submit \uB610\uB294 delete \uC9C4\uD589 \uC911 \uC5EC\uBD80 */\n isPending: boolean;\n /** \uB9C8\uC9C0\uB9C9 mutation \uC5D0\uB7EC (\uC5C6\uC73C\uBA74 null) */\n error: Error | null;\n /** \uC5D0\uB7EC\u00B7\uACB0\uACFC \uC0C1\uD0DC \uCD08\uAE30\uD654 */\n reset: () => void;\n /** entity \uB370\uC774\uD130 \uC0DD\uC131/\uC218\uC815 (seq \uC5C6\uC73C\uBA74 INSERT, \uC788\uC73C\uBA74 UPDATE) */\n submit: (\n entity: string,\n data: Record<string, unknown>,\n opts?: { transactionId?: string; skipHooks?: boolean },\n ) => Promise<{ ok: boolean; seq: number }>;\n /** entity \uB370\uC774\uD130 \uC0AD\uC81C */\n del: (\n entity: string,\n seq: number,\n opts?: { transactionId?: string; hard?: boolean; skipHooks?: boolean },\n ) => Promise<{ ok: boolean; deleted: number }>;\n /** \uCEE4\uC2A4\uD140 SQL \uC870\uD68C */\n query: <T = unknown>(\n entity: string,\n req: EntityQueryRequest,\n ) => Promise<{ ok: boolean; data: { items: T[]; count: number } }>;\n}\n\n/**\n * React \uD658\uACBD\uC5D0\uC11C EntityServerClient \uC778\uC2A4\uD134\uC2A4\uC640 mutation \uC0C1\uD0DC\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.\n *\n * - `singleton=true`(\uAE30\uBCF8): \uD328\uD0A4\uC9C0 \uC804\uC5ED `entityServer` \uC778\uC2A4\uD134\uC2A4\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n * - `singleton=false`: \uCEF4\uD3EC\uB10C\uD2B8 \uC2A4\uCF54\uD504\uC758 \uC0C8 \uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4.\n *\n * @example\n * ```tsx\n * const { submit, del, isPending, error, reset } = useEntityServer();\n *\n * const handleSave = async () => {\n * await submit(\"account\", { name: \"\uD64D\uAE38\uB3D9\" });\n * };\n * ```\n */\nexport function useEntityServer(\n options: UseEntityServerOptions = {},\n): UseEntityServerResult {\n const {\n singleton = true,\n tokenResolver,\n baseUrl,\n packetMagicLen,\n token,\n resumeSession,\n } = options;\n\n const [isPending, setIsPending] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // \uC5B8\uB9C8\uC6B4\uD2B8 \uD6C4 setState \uBC29\uC9C0\n const mountedRef = useRef(true);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n // \uC0C8\uB85C\uACE0\uCE68 \uD6C4 \uB85C\uADF8\uC778 \uC0C1\uD0DC \uBCF5\uC6D0: resumeSession\uC774 \uC788\uC73C\uBA74 \uB9C8\uC6B4\uD2B8 \uC2DC refreshToken() \uD638\uCD9C\n const resumeTokenRef = useRef(resumeSession);\n useEffect(() => {\n const storedRefreshToken = resumeTokenRef.current;\n if (!storedRefreshToken) return;\n client.refreshToken(storedRefreshToken).catch(() => {\n // refresh_token \uB9CC\uB8CC \uB4F1 \u2014 onSessionExpired \uCF5C\uBC31\uC774 \uC774\uBBF8 \uCC98\uB9AC\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const client = useMemo(() => {\n const c = singleton\n ? entityServer\n : new EntityServerClient({ baseUrl, packetMagicLen, token });\n\n if (singleton) {\n c.configure({ baseUrl, packetMagicLen, token });\n }\n\n const resolvedToken = tokenResolver?.();\n if (typeof resolvedToken === \"string\") {\n c.setToken(resolvedToken);\n }\n\n return c;\n }, [singleton, tokenResolver, baseUrl, packetMagicLen, token]);\n\n const run = useCallback(async <T>(fn: () => Promise<T>): Promise<T> => {\n if (mountedRef.current) {\n setIsPending(true);\n setError(null);\n }\n try {\n const result = await fn();\n return result;\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err));\n if (mountedRef.current) setError(e);\n throw e;\n } finally {\n if (mountedRef.current) setIsPending(false);\n }\n }, []);\n\n const submit = useCallback<UseEntityServerResult[\"submit\"]>(\n (entity, data, opts) => run(() => client.submit(entity, data, opts)),\n [client, run],\n );\n\n const del = useCallback<UseEntityServerResult[\"del\"]>(\n (entity, seq, opts) => run(() => client.delete(entity, seq, opts)),\n [client, run],\n );\n\n const query = useCallback<UseEntityServerResult[\"query\"]>(\n (entity, req) => run(() => client.query(entity, req)),\n [client, run],\n );\n\n const reset = useCallback(() => {\n setIsPending(false);\n setError(null);\n }, []);\n\n return { client, isPending, error, reset, submit, del, query };\n}\n", "// @ts-ignore\nimport { xchacha20poly1305 } from \"@noble/ciphers/chacha\";\n// @ts-ignore\nimport { sha256 } from \"@noble/hashes/sha2\";\n// @ts-ignore\nimport { hkdf } from \"@noble/hashes/hkdf\";\n// @ts-ignore\nimport { hmac } from \"@noble/hashes/hmac\";\n\n/**\n * \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D \uC870\uD68C \uD30C\uB77C\uBBF8\uD130\uC785\uB2C8\uB2E4.\n *\n * ```ts\n * client.list(\"post\", {\n * page: 1, limit: 10,\n * orderBy: \"created_time\", orderDir: \"DESC\",\n * fields: [\"seq\", \"title\", \"created_time\"],\n * conditions: { status: \"active\" },\n * });\n * ```\n */\nexport interface EntityListParams {\n /** \uC870\uD68C \uD398\uC774\uC9C0 \uBC88\uD638. \uAE30\uBCF8\uAC12: `1` */\n page?: number;\n /** \uD398\uC774\uC9C0\uB2F9 \uB808\uCF54\uB4DC \uC218. \uAE30\uBCF8\uAC12: `20` */\n limit?: number;\n /** \uC815\uB82C \uAE30\uC900 \uD544\uB4DC\uBA85 */\n orderBy?: string;\n /** \uC815\uB82C \uBC29\uD5A5. \uAE30\uBCF8\uAC12: `\"ASC\"` */\n orderDir?: \"ASC\" | \"DESC\";\n /**\n * \uBC18\uD658\uD560 \uD544\uB4DC \uBAA9\uB85D.\n *\n * - **\uBBF8\uC9C0\uC815 (\uAE30\uBCF8\uAC12)**: \uC5D4\uD2F0\uD2F0\uC758 \uC778\uB371\uC2A4 \uD544\uB4DC\uB9CC \uBC18\uD658\uD569\uB2C8\uB2E4.\n * \uBCF5\uD638\uD654\uB97C \uAC74\uB108\uB6F0\uAE30 \uB54C\uBB38\uC5D0 **\uAC00\uC7A5 \uBE60\uB985\uB2C8\uB2E4**.\n * - `[\"*\"]`: \uC804\uCCB4 \uD544\uB4DC \uBC18\uD658 (\uBCF5\uD638\uD654 \uC218\uD589).\n * - \uD544\uB4DC\uBA85 \uBAA9\uB85D: \uD574\uB2F9 \uD544\uB4DC\uB9CC \uBC18\uD658\uD569\uB2C8\uB2E4.\n * \uC5D4\uD2F0\uD2F0 \uC124\uC815\uC5D0 `index`\uB85C \uC120\uC5B8\uB41C \uD544\uB4DC\uB9CC \uC9C0\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * \uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 \uD544\uB4DC\uBA85\uC744 \uC9C0\uC815\uD558\uBA74 \uC11C\uBC84 \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD569\uB2C8\uB2E4.\n * - `seq`, `created_time`, `updated_time`, `license_seq`\uB294 \uD544\uB4DC\uC5D0 \uAD00\uACC4\uC5C6\uC774 \uD56D\uC0C1 \uD3EC\uD568\uB429\uB2C8\uB2E4.\n *\n * ```ts\n * // \uAE30\uBCF8\uAC12 (\uC778\uB371\uC2A4 \uD544\uB4DC\uB9CC, \uAC00\uC7A5 \uBE60\uB984)\n * client.list(\"account\")\n * // \uC804\uCCB4 \uD544\uB4DC\n * client.list(\"account\", { fields: [\"*\"] })\n * // seq, name, email\uB9CC\n * client.list(\"account\", { fields: [\"seq\", \"name\", \"email\"] })\n * ```\n */\n fields?: string[];\n /** \uD544\uD130 \uC870\uAC74. POST body\uB85C \uC804\uB2EC\uB429\uB2C8\uB2E4. (\uC608: `{ status: \"active\" }`) */\n conditions?: Record<string, unknown>;\n}\n\n/**\n * `query()` \uBA54\uC11C\uB4DC\uC5D0 \uC804\uB2EC\uD558\uB294 SQL \uCFFC\uB9AC \uC694\uCCAD\uC785\uB2C8\uB2E4.\n *\n * - `sql`: SELECT \uC804\uC6A9 SQL. \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD558\uBA70 JOIN \uC9C0\uC6D0.\n * - `params`: SQL \uBC14\uC778\uB529 \uD30C\uB77C\uBBF8\uD130 (`?` \uD50C\uB808\uC774\uC2A4\uD640\uB354 \uB300\uC751).\n * - `limit`: \uCD5C\uB300 \uBC18\uD658 \uAC74\uC218 (\uCD5C\uB300 1000. \uBBF8\uC9C0\uC815 \uC2DC \uC11C\uBC84 \uAE30\uBCF8\uAC12 \uC801\uC6A9).\n *\n * ```ts\n * client.query(\"order\", {\n * sql: `SELECT o.seq, o.status, u.name\n * FROM order o\n * JOIN account u ON u.data_seq = o.account_seq\n * WHERE o.status = ?`,\n * params: [\"pending\"],\n * limit: 100,\n * });\n * ```\n */\nexport interface EntityQueryRequest {\n sql: string;\n params?: unknown[];\n limit?: number;\n}\n\nexport interface RegisterPushDeviceOptions {\n platform?: string;\n deviceType?: string;\n browser?: string;\n browserVersion?: string;\n pushEnabled?: boolean;\n transactionId?: string;\n}\n\n/** EntityServerClient \uC0DD\uC131/\uC124\uC815 \uC635\uC158\uC785\uB2C8\uB2E4. */\nexport interface EntityServerClientOptions {\n baseUrl?: string;\n token?: string;\n packetMagicLen?: number;\n /**\n * `true`\uC774\uBA74 \uC778\uC99D\uB41C POST/PUT \uC694\uCCAD \uBC14\uB514\uB97C XChaCha20-Poly1305\uB85C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uC758 `EnablePacketEncryption`\uC774 \uD65C\uC131\uD654\uB41C \uACBD\uC6B0 \uD544\uC218\uB85C \uC124\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.\n * \uB85C\uADF8\uC778(`login()`)\u00B7\uD1A0\uD070 \uAC31\uC2E0(`refreshToken()`)\uC740 \uC778\uC99D \uC804 \uC694\uCCAD\uC774\uBBC0\uB85C \uC790\uB3D9\uC73C\uB85C \uAC74\uB108\uB701\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12: `false`\n */\n encryptRequests?: boolean;\n /**\n * `true`\uC774\uBA74 `login()` \uC131\uACF5 \uD6C4 Access Token \uB9CC\uB8CC \uC804\uC5D0 \uC790\uB3D9\uC73C\uB85C \uAC31\uC2E0(silent refresh)\uD569\uB2C8\uB2E4.\n * \uAC31\uC2E0 \uC2DC\uC810\uC740 `expires_in - refreshBuffer` \uCD08\uC785\uB2C8\uB2E4.\n *\n * \uAC31\uC2E0 \uC131\uACF5 \uC2DC `onTokenRefreshed`, \uC2E4\uD328 \uC2DC `onSessionExpired` \uCF5C\uBC31\uC774 \uD638\uCD9C\uB429\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12: `false`\n */\n keepSession?: boolean;\n /**\n * \uB9CC\uB8CC \uBA87 \uCD08 \uC804\uC5D0 \uC790\uB3D9 \uAC31\uC2E0\uC744 \uC2DC\uB3C4\uD560\uC9C0 \uC124\uC815\uD569\uB2C8\uB2E4.\n *\n * \uC608: `expires_in = 3600`, `refreshBuffer = 60` \u2192 3540\uCD08 \uD6C4 \uAC31\uC2E0\n *\n * \uAE30\uBCF8\uAC12: `60`\n */\n refreshBuffer?: number;\n /**\n * \uC790\uB3D9 \uAC31\uC2E0 \uC131\uACF5 \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\uC785\uB2C8\uB2E4.\n * \uC0C8 `access_token`\uACFC `expires_in`\uC774 \uC804\uB2EC\uB429\uB2C8\uB2E4.\n * \uC571\uC740 \uC774 \uCF5C\uBC31\uC5D0\uC11C localStorage \uB4F1\uC5D0 \uD1A0\uD070\uC744 \uC800\uC7A5\uD574\uC57C \uD569\uB2C8\uB2E4.\n */\n onTokenRefreshed?: (accessToken: string, expiresIn: number) => void;\n /**\n * \uC138\uC158 \uC720\uC9C0 \uAC31\uC2E0 \uC2E4\uD328 \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\uC785\uB2C8\uB2E4.\n * refresh_token \uB9CC\uB8CC \uB4F1\uC73C\uB85C \uC7AC\uBC1C\uAE09\uC774 \uBD88\uAC00\uB2A5\uD55C \uACBD\uC6B0\uC785\uB2C8\uB2E4.\n * \uC571\uC740 \uC774 \uCF5C\uBC31\uC5D0\uC11C \uB85C\uADF8\uC778 \uD398\uC774\uC9C0\uB85C \uC774\uB3D9\uD558\uB294 \uB4F1\uC758 \uCC98\uB9AC\uB97C \uD574\uC57C \uD569\uB2C8\uB2E4.\n */\n onSessionExpired?: (error: Error) => void;\n /**\n * HMAC \uC778\uC99D\uC6A9 API Key (`X-API-Key` \uD5E4\uB354).\n * `hmacSecret`\uACFC \uD568\uAED8 \uC124\uC815\uD558\uBA74 HMAC \uC778\uC99D \uBAA8\uB4DC\uB85C \uB3D9\uC791\uD569\uB2C8\uB2E4.\n * **\uC11C\uBC84 \uC0AC\uC774\uB4DC(Node.js \uB4F1) \uC804\uC6A9. \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C\uB294 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.**\n */\n apiKey?: string;\n /**\n * HMAC \uC778\uC99D \uC2DC\uD06C\uB9BF. `apiKey`\uC640 \uD568\uAED8 \uC124\uC815\uD558\uBA74 HMAC \uC778\uC99D \uBAA8\uB4DC\uB85C \uB3D9\uC791\uD569\uB2C8\uB2E4.\n *\n * \uD328\uD0B7 \uC554\uD638\uD654 \uD0A4\uB3C4 \uC774 \uAC12\uC5D0\uC11C HKDF-SHA256\uC73C\uB85C \uC720\uB3C4\uD569\uB2C8\uB2E4:\n * `key = HKDF-SHA256(hmac_secret, info=\"entity-server:packet-encryption\", salt=\"entity-server:hkdf:v1\")`\n *\n * **\uC11C\uBC84 \uC0AC\uC774\uB4DC(Node.js \uB4F1) \uC804\uC6A9. \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C\uB294 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.**\n */\n hmacSecret?: string;\n}\n\n/**\n * `list()`, `history()` \uC751\uB2F5\uC758 `data` \uD544\uB4DC \uAD6C\uC870\uC785\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uB294 \uD56D\uC0C1 \uC774 \uAD6C\uC870\uB85C \uBC18\uD658\uD569\uB2C8\uB2E4:\n * ```json\n * { \"ok\": true, \"data\": { \"items\": [...], \"total\": 100, \"page\": 1, \"limit\": 20 } }\n * ```\n */\nexport interface EntityListResult<T = unknown> {\n items: T[];\n /** \uC804\uCCB4 \uB808\uCF54\uB4DC \uC218 */\n total: number;\n /** \uD604\uC7AC \uD398\uC774\uC9C0 \uBC88\uD638 */\n page: number;\n /** \uD398\uC774\uC9C0\uB2F9 \uB808\uCF54\uB4DC \uC218 */\n limit: number;\n}\n\n/**\n * `history()` \uC751\uB2F5\uC758 \uAC1C\uBCC4 \uC774\uB825 \uB808\uCF54\uB4DC \uAD6C\uC870\uC785\uB2C8\uB2E4.\n *\n * - `action`: `\"INSERT\"` | `\"UPDATE\"` | `\"DELETE_SOFT\"` | `\"DELETE_HARD\"` | `\"ROLLBACK\"`\n * - `data_snapshot`: \uBCC0\uACBD \uB2F9\uC2DC \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130 \uC2A4\uB0C5\uC0F7\n */\nexport interface EntityHistoryRecord<T = unknown> {\n seq: number;\n action:\n | \"INSERT\"\n | \"UPDATE\"\n | \"DELETE_SOFT\"\n | \"DELETE_HARD\"\n | \"ROLLBACK\"\n | string;\n data_snapshot: T | null;\n changed_by: number | null;\n changed_time: string;\n}\n\n/** Vite \uD658\uACBD\uBCC0\uC218(`import.meta.env`)\uC5D0\uC11C \uAC12\uC744 \uC77D\uC2B5\uB2C8\uB2E4. */\nfunction readEnv(name: string): string | undefined {\n const meta = import.meta as unknown as {\n env?: Record<string, string | undefined>;\n };\n return meta?.env?.[name];\n}\n\nexport class EntityServerClient {\n private baseUrl: string;\n private token: string;\n private apiKey: string;\n private hmacSecret: string;\n private packetMagicLen: number;\n private encryptRequests: boolean;\n private activeTxId: string | null = null;\n\n // \uC138\uC158 \uC720\uC9C0 \uAD00\uB828\n private keepSession: boolean;\n private refreshBuffer: number;\n private onTokenRefreshed?: (accessToken: string, expiresIn: number) => void;\n private onSessionExpired?: (error: Error) => void;\n private _sessionRefreshToken: string | null = null;\n private _refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * EntityServerClient \uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4.\n *\n * \uAE30\uBCF8\uAC12:\n * - `baseUrl`: `VITE_ENTITY_SERVER_URL` \uB610\uB294 `http://localhost:47200`\n * - `packetMagicLen`: `VITE_ENTITY_SERVER_PACKET_MAGIC_LEN` \uB610\uB294 `4`\n */\n constructor(options: EntityServerClientOptions = {}) {\n const envBaseUrl = readEnv(\"VITE_ENTITY_SERVER_URL\");\n const envMagicLen = readEnv(\"VITE_ENTITY_SERVER_PACKET_MAGIC_LEN\");\n\n this.baseUrl = (\n options.baseUrl ??\n envBaseUrl ??\n \"http://localhost:47200\"\n ).replace(/\\/$/, \"\");\n\n this.token = options.token ?? \"\";\n this.apiKey = options.apiKey ?? \"\";\n this.hmacSecret = options.hmacSecret ?? \"\";\n this.packetMagicLen =\n options.packetMagicLen ?? (envMagicLen ? Number(envMagicLen) : 4);\n this.encryptRequests = options.encryptRequests ?? false;\n this.keepSession = options.keepSession ?? false;\n this.refreshBuffer = options.refreshBuffer ?? 60;\n this.onTokenRefreshed = options.onTokenRefreshed;\n this.onSessionExpired = options.onSessionExpired;\n }\n\n /** baseUrl, token, packetMagicLen, encryptRequests \uAC12\uC744 \uB7F0\uD0C0\uC784\uC5D0 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n configure(options: Partial<EntityServerClientOptions>): void {\n if (options.baseUrl) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n }\n if (typeof options.token === \"string\") {\n this.token = options.token;\n }\n if (typeof options.packetMagicLen === \"number\") {\n this.packetMagicLen = options.packetMagicLen;\n }\n if (typeof options.encryptRequests === \"boolean\") {\n this.encryptRequests = options.encryptRequests;\n }\n if (typeof options.apiKey === \"string\") {\n this.apiKey = options.apiKey;\n }\n if (typeof options.hmacSecret === \"string\") {\n this.hmacSecret = options.hmacSecret;\n }\n if (typeof options.keepSession === \"boolean\") {\n this.keepSession = options.keepSession;\n }\n if (typeof options.refreshBuffer === \"number\") {\n this.refreshBuffer = options.refreshBuffer;\n }\n if (options.onTokenRefreshed) {\n this.onTokenRefreshed = options.onTokenRefreshed;\n }\n if (options.onSessionExpired) {\n this.onSessionExpired = options.onSessionExpired;\n }\n }\n\n /** \uC778\uC99D \uC694\uCCAD\uC5D0 \uC0AC\uC6A9\uD560 JWT Access Token\uC744 \uC124\uC815\uD569\uB2C8\uB2E4. */\n setToken(token: string): void {\n this.token = token;\n }\n\n /** HMAC \uC778\uC99D\uC6A9 API Key\uB97C \uC124\uC815\uD569\uB2C8\uB2E4. */\n setApiKey(apiKey: string): void {\n this.apiKey = apiKey;\n }\n\n /** HMAC \uC778\uC99D\uC6A9 \uC2DC\uD06C\uB9BF\uC744 \uC124\uC815\uD569\uB2C8\uB2E4. */\n setHmacSecret(secret: string): void {\n this.hmacSecret = secret;\n }\n\n /** \uC554\uD638\uD654 \uD328\uD0B7 magic \uAE38\uC774(`packet_magic_len`)\uB97C \uC124\uC815\uD569\uB2C8\uB2E4. */\n setPacketMagicLen(length: number): void {\n this.packetMagicLen = length;\n }\n\n /** \uD604\uC7AC \uC554\uD638\uD654 \uD328\uD0B7 magic \uAE38\uC774\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4. */\n getPacketMagicLen(): number {\n return this.packetMagicLen;\n }\n\n /**\n * \uC790\uB3D9 \uD1A0\uD070 \uAC31\uC2E0 \uD0C0\uC774\uBA38\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4.\n * @param refreshToken \uAC31\uC2E0\uC5D0 \uC0AC\uC6A9\uD560 Refresh Token\n * @param expiresIn Access Token\uC758 \uC720\uD6A8 \uAE30\uAC04 (\uCD08)\n */\n private _scheduleKeepSession(\n refreshToken: string,\n expiresIn: number,\n ): void {\n this._clearRefreshTimer();\n this._sessionRefreshToken = refreshToken;\n\n const delayMs = Math.max((expiresIn - this.refreshBuffer) * 1000, 0);\n this._refreshTimer = setTimeout(async () => {\n if (!this._sessionRefreshToken) return;\n try {\n const result = await this.refreshToken(\n this._sessionRefreshToken,\n );\n this.onTokenRefreshed?.(result.access_token, result.expires_in);\n // \uAC31\uC2E0 \uC131\uACF5 \uC2DC \uB2E4\uC74C \uB9CC\uB8CC \uC804 \uD0C0\uC774\uBA38 \uC7AC\uC608\uC57D\n this._scheduleKeepSession(\n this._sessionRefreshToken,\n result.expires_in,\n );\n } catch (err) {\n this._clearRefreshTimer();\n this.onSessionExpired?.(\n err instanceof Error ? err : new Error(String(err)),\n );\n }\n }, delayMs);\n }\n\n /** \uC790\uB3D9 \uAC31\uC2E0 \uD0C0\uC774\uBA38\uB97C \uC815\uB9AC\uD569\uB2C8\uB2E4. */\n private _clearRefreshTimer(): void {\n if (this._refreshTimer !== null) {\n clearTimeout(this._refreshTimer);\n this._refreshTimer = null;\n }\n }\n\n /**\n * \uC138\uC158 \uC720\uC9C0 \uD0C0\uC774\uBA38\uB97C \uC911\uC9C0\uD569\uB2C8\uB2E4.\n * `logout()` \uD638\uCD9C \uC2DC \uC790\uB3D9\uC73C\uB85C \uC911\uC9C0\uB418\uBBC0\uB85C \uC9C1\uC811 \uD638\uCD9C\uC774 \uD544\uC694\uD55C \uACBD\uC6B0\uB294 \uB4DC\uBB45\uB2C8\uB2E4.\n */\n stopKeepSession(): void {\n this._clearRefreshTimer();\n this._sessionRefreshToken = null;\n }\n\n /**\n * \uC11C\uBC84 \uD5EC\uC2A4 \uCCB4\uD06C\uB97C \uC218\uD589\uD558\uACE0 \uD328\uD0B7 \uC554\uD638\uD654 \uD65C\uC131 \uC5EC\uBD80\uB97C \uC790\uB3D9\uC73C\uB85C \uAC10\uC9C0\uD569\uB2C8\uB2E4.\n *\n * \uC11C\uBC84\uAC00 `packet_encryption: true`\uB97C \uC751\uB2F5\uD558\uBA74 \uC774\uD6C4 \uBAA8\uB4E0 \uC694\uCCAD\uC5D0 \uC554\uD638\uD654\uAC00 \uC790\uB3D9 \uC801\uC6A9\uB429\uB2C8\uB2E4.\n * \uCD08\uAE30\uD654 \uC9C1\uD6C4 \uB610\uB294 \uB85C\uADF8\uC778 \uC804\uC5D0 \uD638\uCD9C\uD558\uBA74 \uC554\uD638\uD654 \uC124\uC815\uC744 \uC790\uB3D9\uC73C\uB85C \uAD6C\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n *\n * ```ts\n * await client.checkHealth();\n * await client.login(email, password); // \uC774\uD6C4 \uC694\uCCAD\uC740 \uC554\uD638\uD654 \uC790\uB3D9 \uC801\uC6A9\n * ```\n *\n * @returns `{ ok: true }` \uB610\uB294 `{ ok: true, packet_encryption: true }`\n */\n async checkHealth(): Promise<{ ok: boolean; packet_encryption?: boolean }> {\n const res = await fetch(`${this.baseUrl}/v1/health`, {\n signal: AbortSignal.timeout(3000),\n });\n const data = (await res.json()) as {\n ok: boolean;\n packet_encryption?: boolean;\n };\n if (data.packet_encryption) {\n this.encryptRequests = true;\n }\n return data;\n }\n\n /** \uB85C\uADF8\uC778 \uD6C4 `access_token`\uC744 \uB0B4\uBD80 \uC0C1\uD0DC\uC5D0 \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async login(\n email: string,\n password: string,\n ): Promise<{\n access_token: string;\n refresh_token: string;\n expires_in: number;\n }> {\n const data = await this.request<{\n data: {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n };\n }>(\"POST\", \"/v1/auth/login\", { email, passwd: password }, false);\n this.token = data.data.access_token;\n if (this.keepSession) {\n this._scheduleKeepSession(\n data.data.refresh_token,\n data.data.expires_in,\n );\n }\n return data.data;\n }\n\n /** Refresh Token\uC73C\uB85C Access Token\uC744 \uC7AC\uBC1C\uAE09\uBC1B\uC544 \uB0B4\uBD80 \uD1A0\uD070\uC744 \uAD50\uCCB4\uD569\uB2C8\uB2E4. */\n async refreshToken(\n refreshToken: string,\n ): Promise<{ access_token: string; expires_in: number }> {\n const data = await this.request<{\n data: { access_token: string; expires_in: number };\n }>(\"POST\", \"/v1/auth/refresh\", { refresh_token: refreshToken }, false);\n this.token = data.data.access_token;\n if (this.keepSession) {\n this._scheduleKeepSession(refreshToken, data.data.expires_in);\n }\n return data.data;\n }\n\n /**\n * \uC11C\uBC84\uC5D0 \uB85C\uADF8\uC544\uC6C3\uC744 \uC694\uCCAD\uD558\uACE0 \uB0B4\uBD80 \uD1A0\uD070\uC744 \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4.\n * refresh_token\uC744 \uC11C\uBC84\uC5D0 \uC804\uB2EC\uD574 \uBB34\uD6A8\uD654\uD569\uB2C8\uB2E4.\n */\n async logout(refreshToken: string): Promise<{ ok: boolean }> {\n this.stopKeepSession();\n const data = await this.request<{ ok: boolean }>(\n \"POST\",\n \"/v1/auth/logout\",\n { refresh_token: refreshToken },\n false,\n );\n this.token = \"\";\n return data;\n }\n\n /** \uD2B8\uB79C\uC7AD\uC158\uC744 \uC2DC\uC791\uD558\uACE0 \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158 ID\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async transStart(): Promise<string> {\n const res = await this.request<{ ok: boolean; transaction_id: string }>(\n \"POST\",\n \"/v1/transaction/start\",\n undefined,\n false,\n );\n this.activeTxId = res.transaction_id;\n return this.activeTxId;\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uB864\uBC31\uD569\uB2C8\uB2E4. */\n transRollback(transactionId?: string): Promise<{ ok: boolean }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/rollback/${txId}`);\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uCEE4\uBC0B\uD569\uB2C8\uB2E4.\n *\n * @returns `results` \uBC30\uC5F4: commit\uB41C \uAC01 \uC791\uC5C5\uC758 `entity`, `action`, `seq`\n */\n transCommit(transactionId?: string): Promise<{\n ok: boolean;\n results: Array<{ entity: string; action: string; seq: number }>;\n }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/commit/${txId}`);\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n get<T = unknown>(\n entity: string,\n seq: number,\n opts: { skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; data: T }> {\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n return this.request(\"GET\", `/v1/entity/${entity}/${seq}${q}`);\n }\n\n /** \uC870\uAC74\uC73C\uB85C \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. data \uCEEC\uB7FC\uC744 \uC644\uC804\uD788 \uBCF5\uD638\uD654\uD558\uC5EC \uBC18\uD658\uD569\uB2C8\uB2E4. */\n find<T = unknown>(\n entity: string,\n conditions?: Record<string, unknown>,\n opts: { skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; data: T }> {\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/find${q}`,\n conditions ?? {},\n );\n }\n\n /** \uD398\uC774\uC9C0\uB124\uC774\uC158/\uC815\uB82C/\uD544\uD130 \uC870\uAC74\uC73C\uB85C \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n list<T = unknown>(\n entity: string,\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n const { conditions, fields, orderDir, orderBy, ...rest } = params;\n\n const queryObj: Record<string, unknown> = {\n page: 1,\n limit: 20,\n ...rest,\n };\n if (orderBy) {\n queryObj.orderBy = orderDir === \"DESC\" ? `-${orderBy}` : orderBy;\n }\n if (fields?.length) {\n queryObj.fields = fields.join(\",\");\n }\n\n const q = buildQuery(queryObj);\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/list?${q}`,\n conditions ?? {},\n );\n }\n\n /**\n * \uC5D4\uD2F0\uD2F0 \uCD1D \uAC74\uC218\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * @param conditions \uD544\uD130 \uC870\uAC74 (\uC608: `{ status: \"active\" }`)\n */\n count(\n entity: string,\n conditions?: Record<string, unknown>,\n ): Promise<{ ok: boolean; count: number }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/count`,\n conditions ?? {},\n );\n }\n\n /**\n * \uCEE4\uC2A4\uD140 SQL\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * SELECT \uC804\uC6A9\uC774\uBA70 \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * JOIN\uC744 \uC0AC\uC6A9\uD574 \uC5EC\uB7EC \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD569\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n * `entity`\uB294 SQL\uC5D0 \uD3EC\uD568\uB41C \uAE30\uBCF8 \uC5D4\uD2F0\uD2F0\uBA85(\uB77C\uC6B0\uD2B8 \uACBD\uB85C\uC6A9)\uC785\uB2C8\uB2E4.\n */\n query<T = unknown>(\n entity: string,\n req: EntityQueryRequest,\n ): Promise<{ ok: boolean; data: { items: T[]; count: number } }> {\n return this.request(\"POST\", `/v1/entity/${entity}/query`, req);\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130\uB97C \uC0DD\uC131/\uC218\uC815(Submit)\uD569\uB2C8\uB2E4. `seq`\uAC00 \uC5C6\uC73C\uBA74 INSERT, \uC788\uC73C\uBA74 UPDATE\uC785\uB2C8\uB2E4. */\n submit(\n entity: string,\n data: Record<string, unknown>,\n opts: { transactionId?: string; skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/submit${q}`,\n data,\n true,\n extraHeaders,\n );\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4(`hard=true`\uBA74 \uD558\uB4DC \uC0AD\uC81C, \uAE30\uBCF8\uC740 \uC18C\uD504\uD2B8 \uC0AD\uC81C). */\n delete(\n entity: string,\n seq: number,\n opts: {\n transactionId?: string;\n hard?: boolean;\n skipHooks?: boolean;\n } = {},\n ): Promise<{ ok: boolean; deleted: number }> {\n const params = new URLSearchParams();\n if (opts.hard) params.set(\"hard\", \"true\");\n if (opts.skipHooks) params.set(\"skipHooks\", \"true\");\n const q = params.size ? `?${params}` : \"\";\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/delete/${seq}${q}`,\n undefined,\n true,\n extraHeaders,\n );\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC758 \uBCC0\uACBD \uC774\uB825\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uC774\uB825 \uD56D\uBAA9\uB2F9 `action`, `data_snapshot`, `changed_by`, `changed_time`\uC744 \uD3EC\uD568\uD569\uB2C8\uB2E4. */\n history<T = unknown>(\n entity: string,\n seq: number,\n params: Pick<EntityListParams, \"page\" | \"limit\"> = {},\n ): Promise<{\n ok: boolean;\n data: EntityListResult<EntityHistoryRecord<T>>;\n }> {\n const q = buildQuery({ page: 1, limit: 50, ...params });\n return this.request(\"GET\", `/v1/entity/${entity}/history/${seq}?${q}`);\n }\n\n /** \uD2B9\uC815 \uC774\uB825 \uC2DC\uC810\uC73C\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uB864\uBC31\uD569\uB2C8\uB2E4. */\n rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/rollback/${historySeq}`,\n );\n }\n\n /** \uD478\uC2DC \uAD00\uB828 \uC5D4\uD2F0\uD2F0\uB85C payload\uB97C \uC804\uC1A1(Submit)\uD569\uB2C8\uB2E4. */\n push(\n pushEntity: string,\n payload: Record<string, unknown>,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(pushEntity, payload, opts);\n }\n\n /** \uD478\uC2DC \uB85C\uADF8 \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n pushLogList<T = unknown>(\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n return this.list<T>(\"push_log\", params);\n }\n\n /** \uACC4\uC815\uC758 \uD478\uC2DC \uB514\uBC14\uC774\uC2A4\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4. */\n registerPushDevice(\n accountSeq: number,\n deviceId: string,\n pushToken: string,\n opts: RegisterPushDeviceOptions = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const {\n platform,\n deviceType,\n browser,\n browserVersion,\n pushEnabled = true,\n transactionId,\n } = opts;\n\n return this.submit(\n \"account_device\",\n {\n id: deviceId,\n account_seq: accountSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n ...(platform ? { platform } : {}),\n ...(deviceType ? { device_type: deviceType } : {}),\n ...(browser ? { browser } : {}),\n ...(browserVersion ? { browser_version: browserVersion } : {}),\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4 \uB808\uCF54\uB4DC\uC758 \uD478\uC2DC \uD1A0\uD070\uC744 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n updatePushDeviceToken(\n deviceSeq: number,\n pushToken: string,\n opts: { pushEnabled?: boolean; transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const { pushEnabled = true, transactionId } = opts;\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4\uC758 \uD478\uC2DC \uC218\uC2E0\uC744 \uBE44\uD65C\uC131\uD654\uD569\uB2C8\uB2E4. */\n disablePushDevice(\n deviceSeq: number,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_enabled: false,\n },\n { transactionId: opts.transactionId },\n );\n }\n\n /**\n * \uC694\uCCAD \uBC14\uB514\uB97C \uD30C\uC2F1\uD558\uACE0 `application/octet-stream`\uC778 \uACBD\uC6B0 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC6D0\uC2DC \uC554\uD638\uD654 payload\uB97C \uC9C1\uC811 \uB2E4\uB8E8\uB294 \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n */\n readRequestBody<T = Record<string, unknown>>(\n body: ArrayBuffer | Uint8Array | string | T | null | undefined,\n contentType = \"application/json\",\n requireEncrypted = false,\n ): T {\n const lowered = contentType.toLowerCase();\n const isEncrypted = lowered.includes(\"application/octet-stream\");\n\n if (requireEncrypted && !isEncrypted) {\n throw new Error(\n \"Encrypted request required: Content-Type must be application/octet-stream\",\n );\n }\n\n if (isEncrypted) {\n if (body == null) {\n throw new Error(\"Encrypted request body is empty\");\n }\n if (body instanceof ArrayBuffer) {\n return this.decryptPacket<T>(body);\n }\n if (body instanceof Uint8Array) {\n const sliced = body.buffer.slice(\n body.byteOffset,\n body.byteOffset + body.byteLength,\n );\n return this.decryptPacket<T>(sliced as ArrayBuffer);\n }\n throw new Error(\n \"Encrypted request body must be ArrayBuffer or Uint8Array\",\n );\n }\n\n if (body == null || body === \"\") return {} as T;\n if (typeof body === \"string\") return JSON.parse(body) as T;\n return body as T;\n }\n\n /**\n * \uACF5\uD1B5 HTTP \uC694\uCCAD \uD568\uC218\uC785\uB2C8\uB2E4.\n *\n * - `encryptRequests`\uAC00 \uD65C\uC131\uD654\uB41C \uC778\uC99D \uC694\uCCAD\uC758 POST \uBC14\uB514\uB97C \uC790\uB3D9 \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * - \uC751\uB2F5\uC774 `application/octet-stream`\uC774\uBA74 \uC790\uB3D9 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n * - JSON \uC751\uB2F5\uC758 `ok`\uAC00 false\uC774\uBA74 \uC5D0\uB7EC\uB97C \uB358\uC9D1\uB2C8\uB2E4.\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n withAuth = true,\n extraHeaders: Record<string, string> = {},\n ): Promise<T> {\n const isHmacMode = withAuth && !!(this.apiKey && this.hmacSecret);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...extraHeaders,\n };\n if (!isHmacMode && withAuth && this.token) {\n headers.Authorization = `Bearer ${this.token}`;\n }\n\n // \uC694\uCCAD \uBC14\uB514 \uACB0\uC815: encryptRequests \uD65C\uC131\uD654 \uC2DC POST \uBC14\uB514\uB97C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n // - \uB85C\uADF8\uC778/\uD1A0\uD070 \uAC31\uC2E0(withAuth=false)\uC740 \uC554\uD638\uD654\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.\n // - GET \uC740 \uBC14\uB514\uAC00 \uC5C6\uC73C\uBBC0\uB85C \uAC74\uB108\uB701\uB2C8\uB2E4.\n // - HMAC \uBAA8\uB4DC\uB294 token \uC5C6\uC774\uB3C4 hmacSecret \uC774 \uC788\uC73C\uBA74 \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n let fetchBody: string | Uint8Array | null = null;\n if (body != null) {\n const shouldEncrypt =\n this.encryptRequests &&\n withAuth &&\n (this.token || isHmacMode) &&\n method !== \"GET\" &&\n method !== \"HEAD\";\n\n if (shouldEncrypt) {\n const plaintext = new TextEncoder().encode(\n JSON.stringify(body),\n );\n const encrypted = this.encryptPacket(plaintext);\n headers[\"Content-Type\"] = \"application/octet-stream\";\n fetchBody = encrypted;\n } else {\n fetchBody = JSON.stringify(body);\n }\n }\n\n // HMAC \uBAA8\uB4DC: X-API-Key / X-Timestamp / X-Nonce / X-Signature \uD5E4\uB354 \uCD94\uAC00\n if (isHmacMode) {\n const timestamp = String(Math.floor(Date.now() / 1000));\n const nonce = crypto.randomUUID();\n const bodyBytes =\n fetchBody instanceof Uint8Array\n ? fetchBody\n : typeof fetchBody === \"string\"\n ? new TextEncoder().encode(fetchBody)\n : new Uint8Array(0);\n const prefix = new TextEncoder().encode(\n `${method}|${path}|${timestamp}|${nonce}|`,\n );\n const payload = new Uint8Array(prefix.length + bodyBytes.length);\n payload.set(prefix, 0);\n payload.set(bodyBytes, prefix.length);\n const sig = hmac(\n sha256,\n new TextEncoder().encode(this.hmacSecret),\n payload,\n );\n const signature = [...sig]\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n headers[\"X-API-Key\"] = this.apiKey;\n headers[\"X-Timestamp\"] = timestamp;\n headers[\"X-Nonce\"] = nonce;\n headers[\"X-Signature\"] = signature;\n }\n\n const res = await fetch(this.baseUrl + path, {\n method,\n headers,\n ...(fetchBody != null ? { body: fetchBody as BodyInit } : {}),\n });\n\n const contentType = res.headers.get(\"Content-Type\") ?? \"\";\n\n if (contentType.includes(\"application/octet-stream\")) {\n const buffer = await res.arrayBuffer();\n return this.decryptPacket<T>(buffer);\n }\n\n const data = await res.json();\n if (!data.ok) {\n const err = new Error(\n data.message ?? `EntityServer error (HTTP ${res.status})`,\n );\n (err as { status?: number }).status = res.status;\n throw err;\n }\n return data as T;\n }\n\n /**\n * \uD328\uD0B7 \uC554\uD638\uD654 \uD0A4\uB97C \uC720\uB3C4\uD569\uB2C8\uB2E4.\n * - HMAC \uBAA8\uB4DC (`hmacSecret` \uC124\uC815 \uC2DC): HKDF-SHA256(hmac_secret, \"entity-server:packet-encryption\")\n * - JWT \uBAA8\uB4DC: sha256(jwt_token)\n */\n private derivePacketKey(): Uint8Array {\n if (this.hmacSecret) {\n const salt = new TextEncoder().encode(\"entity-server:hkdf:v1\");\n const info = new TextEncoder().encode(\n \"entity-server:packet-encryption\",\n );\n return hkdf(\n sha256,\n new TextEncoder().encode(this.hmacSecret),\n salt,\n info,\n 32,\n );\n }\n return sha256(new TextEncoder().encode(this.token));\n }\n\n /**\n * \uD3C9\uBB38 \uBC14\uC774\uD2B8\uB97C XChaCha20-Poly1305\uB85C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * \uD3EC\uB9F7: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]\n */\n private encryptPacket(plaintext: Uint8Array): Uint8Array {\n const key = this.derivePacketKey();\n const magic = new Uint8Array(this.packetMagicLen);\n const nonce = new Uint8Array(24);\n crypto.getRandomValues(magic);\n crypto.getRandomValues(nonce);\n const cipher = xchacha20poly1305(key, nonce);\n const ciphertext = cipher.encrypt(plaintext);\n const result = new Uint8Array(\n this.packetMagicLen + 24 + ciphertext.length,\n );\n result.set(magic, 0);\n result.set(nonce, this.packetMagicLen);\n result.set(ciphertext, this.packetMagicLen + 24);\n return result;\n }\n\n /** \uC11C\uBC84\uC758 \uC554\uD638\uD654 \uD328\uD0B7\uC744 \uBCF5\uD638\uD654\uD574 JSON \uAC1D\uCCB4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\n private decryptPacket<T>(buffer: ArrayBuffer): T {\n const key = this.derivePacketKey();\n const data = new Uint8Array(buffer);\n\n if (data.length < this.packetMagicLen + 24 + 16) {\n throw new Error(\"Encrypted packet too short\");\n }\n\n const nonce = data.slice(this.packetMagicLen, this.packetMagicLen + 24);\n const ciphertext = data.slice(this.packetMagicLen + 24);\n const cipher = xchacha20poly1305(key, nonce);\n const plaintext = cipher.decrypt(ciphertext);\n return JSON.parse(new TextDecoder().decode(plaintext)) as T;\n }\n}\n\n/** \uCFFC\uB9AC \uD30C\uB77C\uBBF8\uD130 \uAC1D\uCCB4\uB97C URL \uCFFC\uB9AC \uBB38\uC790\uC5F4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\nfunction buildQuery(params: Record<string, unknown>): string {\n return Object.entries(params)\n .filter(([, value]) => value != null)\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key === \"orderBy\" ? \"order_by\" : key)}=${encodeURIComponent(String(value))}`,\n )\n .join(\"&\");\n}\n\nexport const entityServer = new EntityServerClient();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["useCallback", "useEffect", "useMemo", "useRef", "useState", "xchacha20poly1305", "sha256", "hkdf", "hmac", "readEnv", "name", "EntityServerClient", "options", "envBaseUrl", "envMagicLen", "token", "apiKey", "secret", "length", "refreshToken", "expiresIn", "delayMs", "result", "err", "data", "email", "password", "res", "transactionId", "txId", "entity", "seq", "opts", "q", "conditions", "params", "fields", "orderDir", "orderBy", "rest", "queryObj", "buildQuery", "req", "extraHeaders", "historySeq", "pushEntity", "payload", "accountSeq", "deviceId", "pushToken", "platform", "deviceType", "browser", "browserVersion", "pushEnabled", "deviceSeq", "body", "contentType", "requireEncrypted", "isEncrypted", "sliced", "method", "path", "withAuth", "isHmacMode", "headers", "fetchBody", "plaintext", "encrypted", "timestamp", "nonce", "bodyBytes", "prefix", "signature", "b", "buffer", "salt", "info", "key", "magic", "ciphertext", "value", "entityServer", "useEntityServer", "options", "singleton", "tokenResolver", "baseUrl", "packetMagicLen", "token", "resumeSession", "isPending", "setIsPending", "useState", "error", "setError", "mountedRef", "useRef", "useEffect", "resumeTokenRef", "storedRefreshToken", "client", "useMemo", "entityServer", "EntityServerClient", "resolvedToken", "run", "useCallback", "fn", "err", "e", "submit", "entity", "data", "opts", "del", "seq", "query", "req", "reset"]
|
|
7
7
|
}
|
package/docs/apis.md
CHANGED
|
@@ -88,16 +88,16 @@ if (packet_encryption) {
|
|
|
88
88
|
|
|
89
89
|
### `new EntityServerClient(options?)`
|
|
90
90
|
|
|
91
|
-
| 옵션
|
|
92
|
-
|
|
|
93
|
-
| `baseUrl`
|
|
94
|
-
| `token`
|
|
95
|
-
| `packetMagicLen`
|
|
96
|
-
| `encryptRequests`
|
|
97
|
-
| `keepSession`
|
|
98
|
-
| `refreshBuffer`
|
|
99
|
-
| `onTokenRefreshed`
|
|
100
|
-
| `onSessionExpired`
|
|
91
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
92
|
+
| ------------------ | ---------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
93
|
+
| `baseUrl` | `string` | `VITE_ENTITY_SERVER_URL` 또는 `http://localhost:47200` | 서버 주소 |
|
|
94
|
+
| `token` | `string` | `""` | JWT Access Token |
|
|
95
|
+
| `packetMagicLen` | `number` | `VITE_PACKET_MAGIC_LEN` 또는 `4` | 암호화 패킷 magic 바이트 길이 |
|
|
96
|
+
| `encryptRequests` | `boolean` | `false` | `true` 로 설정하면 인증된 POST/PUT/PATCH 요청 바디를 XChaCha20-Poly1305 로 암호화하여 전송합니다. 서버에서 `requirePacketEncryption = true` 로 설정된 경우 반드시 활성화해야 합니다. |
|
|
97
|
+
| `keepSession` | `boolean` | `false` | `true`이면 `login()` 성공 후 Access Token 만료 전에 자동으로 갱신합니다. |
|
|
98
|
+
| `refreshBuffer` | `number` | `60` | 만료 몇 초 전에 갱신을 시도할지 설정합니다. 예: `expires_in=3600`, `refreshBuffer=60` → 3540초 후 갱신 |
|
|
99
|
+
| `onTokenRefreshed` | `(accessToken, expiresIn) => void` | — | 자동 갱신 성공 시 호출됩니다. 새 `access_token`을 받아 앱 레벨 저장소에 업데이트하세요. |
|
|
100
|
+
| `onSessionExpired` | `(error: Error) => void` | — | 세션 유지 갱신 실패 시 호출됩니다 (refresh_token 만료 등). 로그인 페이지로 이동하는 등의 처리를 여기서 합니다. |
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
103
|
// 직접 생성
|
|
@@ -307,6 +307,38 @@ result.data; // Account 타입 객체
|
|
|
307
307
|
|
|
308
308
|
---
|
|
309
309
|
|
|
310
|
+
### `find(entity, conditions?, opts?)`
|
|
311
|
+
|
|
312
|
+
조건(conditions)으로 첫 번째 일치 레코드를 조회합니다.
|
|
313
|
+
`data` 컬럼을 **항상 완전히 복호화**하여 반환합니다. 레코드가 없으면 `404` 에러가 됩니다.
|
|
314
|
+
|
|
315
|
+
| 파라미터 | 타입 | 설명 |
|
|
316
|
+
| ------------ | ------------------------- | ----------------------------------- |
|
|
317
|
+
| `conditions` | `Record<string, unknown>` | 검색 조건 (인덱스 필드만 조건 가능) |
|
|
318
|
+
| `skipHooks` | `boolean` | `true`이면 훅 실행 건너뛰기 |
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// 이메일로 계정 단건 조회
|
|
322
|
+
const result = await client.find<Account>("account", {
|
|
323
|
+
email: "hong@example.com",
|
|
324
|
+
});
|
|
325
|
+
result.data; // Account 전체 필드 (passwd 포함)
|
|
326
|
+
|
|
327
|
+
// skipHooks 옵션
|
|
328
|
+
const result = await client.find<Account>(
|
|
329
|
+
"account",
|
|
330
|
+
{ code: "A001" },
|
|
331
|
+
{ skipHooks: true },
|
|
332
|
+
);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
> **`get` vs `find`**
|
|
336
|
+
>
|
|
337
|
+
> - `get(entity, seq)`: seq(일련번호)를 정확히 알고 있을 때 빠르게 조회
|
|
338
|
+
> - `find(entity, conditions)`: 조건으로 검색, 항상 data 전체 복호화 반환
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
310
342
|
### `list(entity, params?)`
|
|
311
343
|
|
|
312
344
|
페이지네이션/정렬/필터 조건으로 엔티티 목록을 조회합니다.
|
|
@@ -622,17 +654,17 @@ import { useEntityServer } from "entity-server-client/react";
|
|
|
622
654
|
|
|
623
655
|
#### 옵션
|
|
624
656
|
|
|
625
|
-
| 옵션
|
|
626
|
-
|
|
|
627
|
-
| `singleton`
|
|
628
|
-
| `baseUrl`
|
|
629
|
-
| `packetMagicLen`
|
|
630
|
-
| `token`
|
|
631
|
-
| `tokenResolver`
|
|
632
|
-
| `keepSession`
|
|
633
|
-
| `onTokenRefreshed` | `(accessToken, expiresIn) => void`
|
|
634
|
-
| `onSessionExpired` | `(error: Error) => void`
|
|
635
|
-
| `resumeSession`
|
|
657
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
658
|
+
| ------------------ | ----------------------------------- | ------ | ----------------------------------------------------------------------------------------------- |
|
|
659
|
+
| `singleton` | `boolean` | `true` | `true`이면 전역 `entityServer` 인스턴스 사용 |
|
|
660
|
+
| `baseUrl` | `string` | — | 서버 주소 (singleton일 때 `configure()` 호출) |
|
|
661
|
+
| `packetMagicLen` | `number` | — | 암호화 패킷 magic 바이트 길이 |
|
|
662
|
+
| `token` | `string` | — | JWT Access Token |
|
|
663
|
+
| `tokenResolver` | `() => string \| undefined \| null` | — | 렌더 시점에 토큰을 동적으로 주입하는 함수 |
|
|
664
|
+
| `keepSession` | `boolean` | — | 세션 유지 활성화 (인덱스 참고) |
|
|
665
|
+
| `onTokenRefreshed` | `(accessToken, expiresIn) => void` | — | 세션 유지 성공 콜백 |
|
|
666
|
+
| `onSessionExpired` | `(error: Error) => void` | — | 세션 만료 콜백 (refresh_token 만료 등) |
|
|
667
|
+
| `resumeSession` | `string` | — | 저장된 refresh_token. 마운트 시 `refreshToken()` 호출해 토큰 복원 + `keepSession` 타이머 재시작 |
|
|
636
668
|
|
|
637
669
|
#### 반환값
|
|
638
670
|
|
|
@@ -694,7 +726,8 @@ const { submit } = useEntityServer({
|
|
|
694
726
|
```tsx
|
|
695
727
|
// App.tsx 또는 로그인 복원을 담당하는 컴포넌트
|
|
696
728
|
export function AppShell() {
|
|
697
|
-
const storedRefreshToken =
|
|
729
|
+
const storedRefreshToken =
|
|
730
|
+
localStorage.getItem("auth_refresh_token") ?? undefined;
|
|
698
731
|
|
|
699
732
|
useEntityServer({
|
|
700
733
|
resumeSession: storedRefreshToken,
|
|
@@ -712,6 +745,7 @@ export function AppShell() {
|
|
|
712
745
|
```
|
|
713
746
|
|
|
714
747
|
> 마운트 시 `resumeSession`이 있으면:
|
|
748
|
+
>
|
|
715
749
|
> 1. `refreshToken(storedRefreshToken)` 호출 → 서버에서 새 access_token 발급
|
|
716
750
|
> 2. 패키지 내부 `this.token` 자동 교체
|
|
717
751
|
> 3. `onTokenRefreshed` 콜백 호출
|
package/package.json
CHANGED
|
@@ -94,7 +94,7 @@ export function useEntityServer(
|
|
|
94
94
|
client.refreshToken(storedRefreshToken).catch(() => {
|
|
95
95
|
// refresh_token 만료 등 — onSessionExpired 콜백이 이미 처리
|
|
96
96
|
});
|
|
97
|
-
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
98
|
}, []);
|
|
99
99
|
|
|
100
100
|
const client = useMemo(() => {
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
3
3
|
// @ts-ignore
|
|
4
4
|
import { sha256 } from "@noble/hashes/sha2";
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
import { hkdf } from "@noble/hashes/hkdf";
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import { hmac } from "@noble/hashes/hmac";
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* 엔티티 목록 조회 파라미터입니다.
|
|
@@ -125,6 +129,21 @@ export interface EntityServerClientOptions {
|
|
|
125
129
|
* 앱은 이 콜백에서 로그인 페이지로 이동하는 등의 처리를 해야 합니다.
|
|
126
130
|
*/
|
|
127
131
|
onSessionExpired?: (error: Error) => void;
|
|
132
|
+
/**
|
|
133
|
+
* HMAC 인증용 API Key (`X-API-Key` 헤더).
|
|
134
|
+
* `hmacSecret`과 함께 설정하면 HMAC 인증 모드로 동작합니다.
|
|
135
|
+
* **서버 사이드(Node.js 등) 전용. 브라우저에서는 사용하지 마세요.**
|
|
136
|
+
*/
|
|
137
|
+
apiKey?: string;
|
|
138
|
+
/**
|
|
139
|
+
* HMAC 인증 시크릿. `apiKey`와 함께 설정하면 HMAC 인증 모드로 동작합니다.
|
|
140
|
+
*
|
|
141
|
+
* 패킷 암호화 키도 이 값에서 HKDF-SHA256으로 유도합니다:
|
|
142
|
+
* `key = HKDF-SHA256(hmac_secret, info="entity-server:packet-encryption", salt="entity-server:hkdf:v1")`
|
|
143
|
+
*
|
|
144
|
+
* **서버 사이드(Node.js 등) 전용. 브라우저에서는 사용하지 마세요.**
|
|
145
|
+
*/
|
|
146
|
+
hmacSecret?: string;
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
/**
|
|
@@ -176,6 +195,8 @@ function readEnv(name: string): string | undefined {
|
|
|
176
195
|
export class EntityServerClient {
|
|
177
196
|
private baseUrl: string;
|
|
178
197
|
private token: string;
|
|
198
|
+
private apiKey: string;
|
|
199
|
+
private hmacSecret: string;
|
|
179
200
|
private packetMagicLen: number;
|
|
180
201
|
private encryptRequests: boolean;
|
|
181
202
|
private activeTxId: string | null = null;
|
|
@@ -206,6 +227,8 @@ export class EntityServerClient {
|
|
|
206
227
|
).replace(/\/$/, "");
|
|
207
228
|
|
|
208
229
|
this.token = options.token ?? "";
|
|
230
|
+
this.apiKey = options.apiKey ?? "";
|
|
231
|
+
this.hmacSecret = options.hmacSecret ?? "";
|
|
209
232
|
this.packetMagicLen =
|
|
210
233
|
options.packetMagicLen ?? (envMagicLen ? Number(envMagicLen) : 4);
|
|
211
234
|
this.encryptRequests = options.encryptRequests ?? false;
|
|
@@ -229,6 +252,12 @@ export class EntityServerClient {
|
|
|
229
252
|
if (typeof options.encryptRequests === "boolean") {
|
|
230
253
|
this.encryptRequests = options.encryptRequests;
|
|
231
254
|
}
|
|
255
|
+
if (typeof options.apiKey === "string") {
|
|
256
|
+
this.apiKey = options.apiKey;
|
|
257
|
+
}
|
|
258
|
+
if (typeof options.hmacSecret === "string") {
|
|
259
|
+
this.hmacSecret = options.hmacSecret;
|
|
260
|
+
}
|
|
232
261
|
if (typeof options.keepSession === "boolean") {
|
|
233
262
|
this.keepSession = options.keepSession;
|
|
234
263
|
}
|
|
@@ -248,6 +277,16 @@ export class EntityServerClient {
|
|
|
248
277
|
this.token = token;
|
|
249
278
|
}
|
|
250
279
|
|
|
280
|
+
/** HMAC 인증용 API Key를 설정합니다. */
|
|
281
|
+
setApiKey(apiKey: string): void {
|
|
282
|
+
this.apiKey = apiKey;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** HMAC 인증용 시크릿을 설정합니다. */
|
|
286
|
+
setHmacSecret(secret: string): void {
|
|
287
|
+
this.hmacSecret = secret;
|
|
288
|
+
}
|
|
289
|
+
|
|
251
290
|
/** 암호화 패킷 magic 길이(`packet_magic_len`)를 설정합니다. */
|
|
252
291
|
setPacketMagicLen(length: number): void {
|
|
253
292
|
this.packetMagicLen = length;
|
|
@@ -274,7 +313,9 @@ export class EntityServerClient {
|
|
|
274
313
|
this._refreshTimer = setTimeout(async () => {
|
|
275
314
|
if (!this._sessionRefreshToken) return;
|
|
276
315
|
try {
|
|
277
|
-
const result = await this.refreshToken(
|
|
316
|
+
const result = await this.refreshToken(
|
|
317
|
+
this._sessionRefreshToken,
|
|
318
|
+
);
|
|
278
319
|
this.onTokenRefreshed?.(result.access_token, result.expires_in);
|
|
279
320
|
// 갱신 성공 시 다음 만료 전 타이머 재예약
|
|
280
321
|
this._scheduleKeepSession(
|
|
@@ -442,6 +483,20 @@ export class EntityServerClient {
|
|
|
442
483
|
return this.request("GET", `/v1/entity/${entity}/${seq}${q}`);
|
|
443
484
|
}
|
|
444
485
|
|
|
486
|
+
/** 조건으로 엔티티 단건을 조회합니다. data 컬럼을 완전히 복호화하여 반환합니다. */
|
|
487
|
+
find<T = unknown>(
|
|
488
|
+
entity: string,
|
|
489
|
+
conditions?: Record<string, unknown>,
|
|
490
|
+
opts: { skipHooks?: boolean } = {},
|
|
491
|
+
): Promise<{ ok: boolean; data: T }> {
|
|
492
|
+
const q = opts.skipHooks ? "?skipHooks=true" : "";
|
|
493
|
+
return this.request(
|
|
494
|
+
"POST",
|
|
495
|
+
`/v1/entity/${entity}/find${q}`,
|
|
496
|
+
conditions ?? {},
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
445
500
|
/** 페이지네이션/정렬/필터 조건으로 엔티티 목록을 조회합니다. */
|
|
446
501
|
list<T = unknown>(
|
|
447
502
|
entity: string,
|
|
@@ -703,23 +758,25 @@ export class EntityServerClient {
|
|
|
703
758
|
withAuth = true,
|
|
704
759
|
extraHeaders: Record<string, string> = {},
|
|
705
760
|
): Promise<T> {
|
|
761
|
+
const isHmacMode = withAuth && !!(this.apiKey && this.hmacSecret);
|
|
706
762
|
const headers: Record<string, string> = {
|
|
707
763
|
"Content-Type": "application/json",
|
|
708
764
|
...extraHeaders,
|
|
709
765
|
};
|
|
710
|
-
if (withAuth && this.token) {
|
|
766
|
+
if (!isHmacMode && withAuth && this.token) {
|
|
711
767
|
headers.Authorization = `Bearer ${this.token}`;
|
|
712
768
|
}
|
|
713
769
|
|
|
714
770
|
// 요청 바디 결정: encryptRequests 활성화 시 POST 바디를 암호화합니다.
|
|
715
771
|
// - 로그인/토큰 갱신(withAuth=false)은 암호화하지 않습니다.
|
|
716
772
|
// - GET 은 바디가 없으므로 건너뜁니다.
|
|
773
|
+
// - HMAC 모드는 token 없이도 hmacSecret 이 있으면 암호화합니다.
|
|
717
774
|
let fetchBody: string | Uint8Array | null = null;
|
|
718
775
|
if (body != null) {
|
|
719
776
|
const shouldEncrypt =
|
|
720
777
|
this.encryptRequests &&
|
|
721
778
|
withAuth &&
|
|
722
|
-
this.token &&
|
|
779
|
+
(this.token || isHmacMode) &&
|
|
723
780
|
method !== "GET" &&
|
|
724
781
|
method !== "HEAD";
|
|
725
782
|
|
|
@@ -735,6 +792,36 @@ export class EntityServerClient {
|
|
|
735
792
|
}
|
|
736
793
|
}
|
|
737
794
|
|
|
795
|
+
// HMAC 모드: X-API-Key / X-Timestamp / X-Nonce / X-Signature 헤더 추가
|
|
796
|
+
if (isHmacMode) {
|
|
797
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
798
|
+
const nonce = crypto.randomUUID();
|
|
799
|
+
const bodyBytes =
|
|
800
|
+
fetchBody instanceof Uint8Array
|
|
801
|
+
? fetchBody
|
|
802
|
+
: typeof fetchBody === "string"
|
|
803
|
+
? new TextEncoder().encode(fetchBody)
|
|
804
|
+
: new Uint8Array(0);
|
|
805
|
+
const prefix = new TextEncoder().encode(
|
|
806
|
+
`${method}|${path}|${timestamp}|${nonce}|`,
|
|
807
|
+
);
|
|
808
|
+
const payload = new Uint8Array(prefix.length + bodyBytes.length);
|
|
809
|
+
payload.set(prefix, 0);
|
|
810
|
+
payload.set(bodyBytes, prefix.length);
|
|
811
|
+
const sig = hmac(
|
|
812
|
+
sha256,
|
|
813
|
+
new TextEncoder().encode(this.hmacSecret),
|
|
814
|
+
payload,
|
|
815
|
+
);
|
|
816
|
+
const signature = [...sig]
|
|
817
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
818
|
+
.join("");
|
|
819
|
+
headers["X-API-Key"] = this.apiKey;
|
|
820
|
+
headers["X-Timestamp"] = timestamp;
|
|
821
|
+
headers["X-Nonce"] = nonce;
|
|
822
|
+
headers["X-Signature"] = signature;
|
|
823
|
+
}
|
|
824
|
+
|
|
738
825
|
const res = await fetch(this.baseUrl + path, {
|
|
739
826
|
method,
|
|
740
827
|
headers,
|
|
@@ -759,12 +846,34 @@ export class EntityServerClient {
|
|
|
759
846
|
return data as T;
|
|
760
847
|
}
|
|
761
848
|
|
|
849
|
+
/**
|
|
850
|
+
* 패킷 암호화 키를 유도합니다.
|
|
851
|
+
* - HMAC 모드 (`hmacSecret` 설정 시): HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
|
|
852
|
+
* - JWT 모드: sha256(jwt_token)
|
|
853
|
+
*/
|
|
854
|
+
private derivePacketKey(): Uint8Array {
|
|
855
|
+
if (this.hmacSecret) {
|
|
856
|
+
const salt = new TextEncoder().encode("entity-server:hkdf:v1");
|
|
857
|
+
const info = new TextEncoder().encode(
|
|
858
|
+
"entity-server:packet-encryption",
|
|
859
|
+
);
|
|
860
|
+
return hkdf(
|
|
861
|
+
sha256,
|
|
862
|
+
new TextEncoder().encode(this.hmacSecret),
|
|
863
|
+
salt,
|
|
864
|
+
info,
|
|
865
|
+
32,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
return sha256(new TextEncoder().encode(this.token));
|
|
869
|
+
}
|
|
870
|
+
|
|
762
871
|
/**
|
|
763
872
|
* 평문 바이트를 XChaCha20-Poly1305로 암호화합니다.
|
|
764
873
|
* 포맷: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]
|
|
765
874
|
*/
|
|
766
875
|
private encryptPacket(plaintext: Uint8Array): Uint8Array {
|
|
767
|
-
const key =
|
|
876
|
+
const key = this.derivePacketKey();
|
|
768
877
|
const magic = new Uint8Array(this.packetMagicLen);
|
|
769
878
|
const nonce = new Uint8Array(24);
|
|
770
879
|
crypto.getRandomValues(magic);
|
|
@@ -782,7 +891,7 @@ export class EntityServerClient {
|
|
|
782
891
|
|
|
783
892
|
/** 서버의 암호화 패킷을 복호화해 JSON 객체로 변환합니다. */
|
|
784
893
|
private decryptPacket<T>(buffer: ArrayBuffer): T {
|
|
785
|
-
const key =
|
|
894
|
+
const key = this.derivePacketKey();
|
|
786
895
|
const data = new Uint8Array(buffer);
|
|
787
896
|
|
|
788
897
|
if (data.length < this.packetMagicLen + 24 + 16) {
|