entity-server-client 0.2.3 → 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/dist/index.d.ts +27 -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 +13 -6
- package/package.json +1 -1
- package/src/index.ts +97 -4
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 길이를 반환합니다. */
|
|
@@ -356,6 +377,12 @@ export declare class EntityServerClient {
|
|
|
356
377
|
* - JSON 응답의 `ok`가 false이면 에러를 던집니다.
|
|
357
378
|
*/
|
|
358
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;
|
|
359
386
|
/**
|
|
360
387
|
* 평문 바이트를 XChaCha20-Poly1305로 암호화합니다.
|
|
361
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(\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 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", "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", "headers", "fetchBody", "plaintext", "encrypted", "buffer", "
|
|
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(\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 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", "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", "headers", "fetchBody", "plaintext", "encrypted", "buffer", "
|
|
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
|
@@ -312,21 +312,28 @@ result.data; // Account 타입 객체
|
|
|
312
312
|
조건(conditions)으로 첫 번째 일치 레코드를 조회합니다.
|
|
313
313
|
`data` 컬럼을 **항상 완전히 복호화**하여 반환합니다. 레코드가 없으면 `404` 에러가 됩니다.
|
|
314
314
|
|
|
315
|
-
| 파라미터 | 타입
|
|
316
|
-
| ------------ |
|
|
317
|
-
| `conditions` | `Record<string, unknown>`
|
|
318
|
-
| `skipHooks` | `boolean`
|
|
315
|
+
| 파라미터 | 타입 | 설명 |
|
|
316
|
+
| ------------ | ------------------------- | ----------------------------------- |
|
|
317
|
+
| `conditions` | `Record<string, unknown>` | 검색 조건 (인덱스 필드만 조건 가능) |
|
|
318
|
+
| `skipHooks` | `boolean` | `true`이면 훅 실행 건너뛰기 |
|
|
319
319
|
|
|
320
320
|
```ts
|
|
321
321
|
// 이메일로 계정 단건 조회
|
|
322
|
-
const result = await client.find<Account>("account", {
|
|
322
|
+
const result = await client.find<Account>("account", {
|
|
323
|
+
email: "hong@example.com",
|
|
324
|
+
});
|
|
323
325
|
result.data; // Account 전체 필드 (passwd 포함)
|
|
324
326
|
|
|
325
327
|
// skipHooks 옵션
|
|
326
|
-
const result = await client.find<Account>(
|
|
328
|
+
const result = await client.find<Account>(
|
|
329
|
+
"account",
|
|
330
|
+
{ code: "A001" },
|
|
331
|
+
{ skipHooks: true },
|
|
332
|
+
);
|
|
327
333
|
```
|
|
328
334
|
|
|
329
335
|
> **`get` vs `find`**
|
|
336
|
+
>
|
|
330
337
|
> - `get(entity, seq)`: seq(일련번호)를 정확히 알고 있을 때 빠르게 조회
|
|
331
338
|
> - `find(entity, conditions)`: 조건으로 검색, 항상 data 전체 복호화 반환
|
|
332
339
|
|
package/package.json
CHANGED
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;
|
|
@@ -719,23 +758,25 @@ export class EntityServerClient {
|
|
|
719
758
|
withAuth = true,
|
|
720
759
|
extraHeaders: Record<string, string> = {},
|
|
721
760
|
): Promise<T> {
|
|
761
|
+
const isHmacMode = withAuth && !!(this.apiKey && this.hmacSecret);
|
|
722
762
|
const headers: Record<string, string> = {
|
|
723
763
|
"Content-Type": "application/json",
|
|
724
764
|
...extraHeaders,
|
|
725
765
|
};
|
|
726
|
-
if (withAuth && this.token) {
|
|
766
|
+
if (!isHmacMode && withAuth && this.token) {
|
|
727
767
|
headers.Authorization = `Bearer ${this.token}`;
|
|
728
768
|
}
|
|
729
769
|
|
|
730
770
|
// 요청 바디 결정: encryptRequests 활성화 시 POST 바디를 암호화합니다.
|
|
731
771
|
// - 로그인/토큰 갱신(withAuth=false)은 암호화하지 않습니다.
|
|
732
772
|
// - GET 은 바디가 없으므로 건너뜁니다.
|
|
773
|
+
// - HMAC 모드는 token 없이도 hmacSecret 이 있으면 암호화합니다.
|
|
733
774
|
let fetchBody: string | Uint8Array | null = null;
|
|
734
775
|
if (body != null) {
|
|
735
776
|
const shouldEncrypt =
|
|
736
777
|
this.encryptRequests &&
|
|
737
778
|
withAuth &&
|
|
738
|
-
this.token &&
|
|
779
|
+
(this.token || isHmacMode) &&
|
|
739
780
|
method !== "GET" &&
|
|
740
781
|
method !== "HEAD";
|
|
741
782
|
|
|
@@ -751,6 +792,36 @@ export class EntityServerClient {
|
|
|
751
792
|
}
|
|
752
793
|
}
|
|
753
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
|
+
|
|
754
825
|
const res = await fetch(this.baseUrl + path, {
|
|
755
826
|
method,
|
|
756
827
|
headers,
|
|
@@ -775,12 +846,34 @@ export class EntityServerClient {
|
|
|
775
846
|
return data as T;
|
|
776
847
|
}
|
|
777
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
|
+
|
|
778
871
|
/**
|
|
779
872
|
* 평문 바이트를 XChaCha20-Poly1305로 암호화합니다.
|
|
780
873
|
* 포맷: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]
|
|
781
874
|
*/
|
|
782
875
|
private encryptPacket(plaintext: Uint8Array): Uint8Array {
|
|
783
|
-
const key =
|
|
876
|
+
const key = this.derivePacketKey();
|
|
784
877
|
const magic = new Uint8Array(this.packetMagicLen);
|
|
785
878
|
const nonce = new Uint8Array(24);
|
|
786
879
|
crypto.getRandomValues(magic);
|
|
@@ -798,7 +891,7 @@ export class EntityServerClient {
|
|
|
798
891
|
|
|
799
892
|
/** 서버의 암호화 패킷을 복호화해 JSON 객체로 변환합니다. */
|
|
800
893
|
private decryptPacket<T>(buffer: ArrayBuffer): T {
|
|
801
|
-
const key =
|
|
894
|
+
const key = this.derivePacketKey();
|
|
802
895
|
const data = new Uint8Array(buffer);
|
|
803
896
|
|
|
804
897
|
if (data.length < this.packetMagicLen + 24 + 16) {
|