entity-server-client 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/index.d.ts +37 -4
- 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 +54 -5
- package/package.json +1 -1
- package/src/index.ts +93 -6
package/README.md
CHANGED
|
@@ -45,6 +45,37 @@ const res = await entityServer.list("account", { page: 1, limit: 20 });
|
|
|
45
45
|
console.log(res.data);
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
## React 훅
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { useEntityServer } from "entity-server-client/react";
|
|
52
|
+
|
|
53
|
+
export function AccountPage() {
|
|
54
|
+
const client = useEntityServer({
|
|
55
|
+
tokenResolver: () => localStorage.getItem("auth_access_token"),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const [items, setItems] = useState([]);
|
|
59
|
+
|
|
60
|
+
const load = async () => {
|
|
61
|
+
const res = await client.list("account", { page: 1, limit: 20 });
|
|
62
|
+
setItems(res.data.items);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const save = async () => {
|
|
66
|
+
await client.submit("account", {
|
|
67
|
+
name: "홍길동",
|
|
68
|
+
email: "hong@example.com",
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return <button onClick={load}>불러오기</button>;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> `useEntityServer`는 기본적으로 전역 `entityServer` 인스턴스를 사용합니다.
|
|
77
|
+
> 컴포넌트마다 독립 인스턴스가 필요하면 `singleton: false` 옵션을 사용하세요.
|
|
78
|
+
|
|
48
79
|
## 문서
|
|
49
80
|
|
|
50
81
|
- [함수별 사용법](docs/apis.md)
|
package/dist/index.d.ts
CHANGED
|
@@ -79,6 +79,15 @@ export interface EntityServerClientOptions {
|
|
|
79
79
|
baseUrl?: string;
|
|
80
80
|
token?: string;
|
|
81
81
|
packetMagicLen?: number;
|
|
82
|
+
/**
|
|
83
|
+
* `true`이면 인증된 POST/PUT 요청 바디를 XChaCha20-Poly1305로 암호화합니다.
|
|
84
|
+
*
|
|
85
|
+
* 서버의 `EnablePacketEncryption`이 활성화된 경우 필수로 설정해야 합니다.
|
|
86
|
+
* 로그인(`login()`)·토큰 갱신(`refreshToken()`)은 인증 전 요청이므로 자동으로 건너뜁니다.
|
|
87
|
+
*
|
|
88
|
+
* 기본값: `false`
|
|
89
|
+
*/
|
|
90
|
+
encryptRequests?: boolean;
|
|
82
91
|
}
|
|
83
92
|
/**
|
|
84
93
|
* `list()`, `history()` 응답의 `data` 필드 구조입니다.
|
|
@@ -114,16 +123,17 @@ export declare class EntityServerClient {
|
|
|
114
123
|
private baseUrl;
|
|
115
124
|
private token;
|
|
116
125
|
private packetMagicLen;
|
|
126
|
+
private encryptRequests;
|
|
117
127
|
private activeTxId;
|
|
118
128
|
/**
|
|
119
129
|
* EntityServerClient 인스턴스를 생성합니다.
|
|
120
130
|
*
|
|
121
131
|
* 기본값:
|
|
122
132
|
* - `baseUrl`: `VITE_ENTITY_SERVER_URL` 또는 `http://localhost:47200`
|
|
123
|
-
* - `packetMagicLen`: `
|
|
133
|
+
* - `packetMagicLen`: `VITE_ENTITY_SERVER_PACKET_MAGIC_LEN` 또는 `4`
|
|
124
134
|
*/
|
|
125
135
|
constructor(options?: EntityServerClientOptions);
|
|
126
|
-
/** baseUrl, token, packetMagicLen 값을 런타임에 갱신합니다. */
|
|
136
|
+
/** baseUrl, token, packetMagicLen, encryptRequests 값을 런타임에 갱신합니다. */
|
|
127
137
|
configure(options: Partial<EntityServerClientOptions>): void;
|
|
128
138
|
/** 인증 요청에 사용할 JWT Access Token을 설정합니다. */
|
|
129
139
|
setToken(token: string): void;
|
|
@@ -131,6 +141,23 @@ export declare class EntityServerClient {
|
|
|
131
141
|
setPacketMagicLen(length: number): void;
|
|
132
142
|
/** 현재 암호화 패킷 magic 길이를 반환합니다. */
|
|
133
143
|
getPacketMagicLen(): number;
|
|
144
|
+
/**
|
|
145
|
+
* 서버 헬스 체크를 수행하고 패킷 암호화 활성 여부를 자동으로 감지합니다.
|
|
146
|
+
*
|
|
147
|
+
* 서버가 `packet_encryption: true`를 응답하면 이후 모든 요청에 암호화가 자동 적용됩니다.
|
|
148
|
+
* 초기화 직후 또는 로그인 전에 호출하면 암호화 설정을 자동으로 구성할 수 있습니다.
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* await client.checkHealth();
|
|
152
|
+
* await client.login(email, password); // 이후 요청은 암호화 자동 적용
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* @returns `{ ok: true }` 또는 `{ ok: true, packet_encryption: true }`
|
|
156
|
+
*/
|
|
157
|
+
checkHealth(): Promise<{
|
|
158
|
+
ok: boolean;
|
|
159
|
+
packet_encryption?: boolean;
|
|
160
|
+
}>;
|
|
134
161
|
/** 로그인 후 `access_token`을 내부 상태에 저장합니다. */
|
|
135
162
|
login(email: string, password: string): Promise<{
|
|
136
163
|
access_token: string;
|
|
@@ -262,10 +289,16 @@ export declare class EntityServerClient {
|
|
|
262
289
|
/**
|
|
263
290
|
* 공통 HTTP 요청 함수입니다.
|
|
264
291
|
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
292
|
+
* - `encryptRequests`가 활성화된 인증 요청의 POST 바디를 자동 암호화합니다.
|
|
293
|
+
* - 응답이 `application/octet-stream`이면 자동 복호화합니다.
|
|
294
|
+
* - JSON 응답의 `ok`가 false이면 에러를 던집니다.
|
|
267
295
|
*/
|
|
268
296
|
private request;
|
|
297
|
+
/**
|
|
298
|
+
* 평문 바이트를 XChaCha20-Poly1305로 암호화합니다.
|
|
299
|
+
* 포맷: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]
|
|
300
|
+
*/
|
|
301
|
+
private encryptPacket;
|
|
269
302
|
/** 서버의 암호화 패킷을 복호화해 JSON 객체로 변환합니다. */
|
|
270
303
|
private decryptPacket;
|
|
271
304
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{xchacha20poly1305 as
|
|
1
|
+
import{xchacha20poly1305 as h}from"@noble/ciphers/chacha";import{sha256 as g}from"@noble/hashes/sha2";function k(l){return import.meta?.env?.[l]}var d=class{baseUrl;token;packetMagicLen;encryptRequests;activeTxId=null;constructor(e={}){let t=k("VITE_ENTITY_SERVER_URL"),n=k("VITE_ENTITY_SERVER_PACKET_MAGIC_LEN");this.baseUrl=(e.baseUrl??t??"http://localhost:47200").replace(/\/$/,""),this.token=e.token??"",this.packetMagicLen=e.packetMagicLen??(n?Number(n):4),this.encryptRequests=e.encryptRequests??!1}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)}setToken(e){this.token=e}setPacketMagicLen(e){this.packetMagicLen=e}getPacketMagicLen(){return this.packetMagicLen}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,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,t.data}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}`)}list(e,t={}){let{conditions:n,fields:r,orderDir:i,orderBy:s,...a}=t,o={page:1,limit:20,...a};s&&(o.orderBy=i==="DESC"?`-${s}`:s),r?.length&&(o.fields=r.join(","));let p=m(o);return this.request("POST",`/v1/entity/${e}/list?${p}`,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,i=r?{"X-Transaction-ID":r}:void 0,s=n.skipHooks?"?skipHooks=true":"";return this.request("POST",`/v1/entity/${e}/submit${s}`,t,!0,i)}delete(e,t,n={}){let r=new URLSearchParams;n.hard&&r.set("hard","true"),n.skipHooks&&r.set("skipHooks","true");let i=r.size?`?${r}`:"",s=n.transactionId??this.activeTxId,a=s?{"X-Transaction-ID":s}:void 0;return this.request("POST",`/v1/entity/${e}/delete/${t}${i}`,void 0,!0,a)}history(e,t,n={}){let r=m({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:i,deviceType:s,browser:a,browserVersion:o,pushEnabled:p=!0,transactionId:c}=r;return this.submit("account_device",{id:t,account_seq:e,push_token:n,push_enabled:p,...i?{platform:i}:{},...s?{device_type:s}:{},...a?{browser:a}:{},...o?{browser_version:o}:{}},{transactionId:c})}updatePushDeviceToken(e,t,n={}){let{pushEnabled:r=!0,transactionId:i}=n;return this.submit("account_device",{seq:e,push_token:t,push_enabled:r},{transactionId:i})}disablePushDevice(e,t={}){return this.submit("account_device",{seq:e,push_enabled:!1},{transactionId:t.transactionId})}readRequestBody(e,t="application/json",n=!1){let i=t.toLowerCase().includes("application/octet-stream");if(n&&!i)throw new Error("Encrypted request required: Content-Type must be application/octet-stream");if(i){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,i={}){let s={"Content-Type":"application/json",...i};r&&this.token&&(s.Authorization=`Bearer ${this.token}`);let a=null;if(n!=null)if(this.encryptRequests&&r&&this.token&&e!=="GET"&&e!=="HEAD"){let y=new TextEncoder().encode(JSON.stringify(n)),b=this.encryptPacket(y);s["Content-Type"]="application/octet-stream",a=b}else a=JSON.stringify(n);let o=await fetch(this.baseUrl+t,{method:e,headers:s,...a!=null?{body:a}:{}});if((o.headers.get("Content-Type")??"").includes("application/octet-stream")){let u=await o.arrayBuffer();return this.decryptPacket(u)}let c=await o.json();if(!c.ok){let u=new Error(c.message??`EntityServer error (HTTP ${o.status})`);throw u.status=o.status,u}return c}encryptPacket(e){let t=g(new TextEncoder().encode(this.token)),n=new Uint8Array(this.packetMagicLen),r=new Uint8Array(24);crypto.getRandomValues(n),crypto.getRandomValues(r);let s=h(t,r).encrypt(e),a=new Uint8Array(this.packetMagicLen+24+s.length);return a.set(n,0),a.set(r,this.packetMagicLen),a.set(s,this.packetMagicLen+24),a}decryptPacket(e){let t=g(new TextEncoder().encode(this.token)),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),i=n.slice(this.packetMagicLen+24),a=h(t,r).decrypt(i);return JSON.parse(new TextDecoder().decode(a))}};function m(l){return Object.entries(l).filter(([,e])=>e!=null).map(([e,t])=>`${encodeURIComponent(e==="orderBy"?"order_by":e)}=${encodeURIComponent(String(t))}`).join("&")}var E=new d;export{d as EntityServerClient,E 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\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 activeTxId: string | 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_PACKET_MAGIC_LEN` \uB610\uB294 `4`\n */\n constructor(options: EntityServerClientOptions = {}) {\n const envBaseUrl = readEnv(\"VITE_ENTITY_SERVER_URL\");\n const envMagicLen = readEnv(\"VITE_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 }\n\n /** baseUrl, token, packetMagicLen \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 }\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 /** \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 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 return data.data;\n }\n\n /** \uD2B8\uB79C\uC7AD\uC158\uC744 \uC2DC\uC791\uD558\uACE0 \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158 ID\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async transStart(): Promise<string> {\n const res = await this.request<{ ok: boolean; transaction_id: string }>(\n \"POST\",\n \"/v1/transaction/start\",\n undefined,\n false,\n );\n this.activeTxId = res.transaction_id;\n return this.activeTxId;\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uB864\uBC31\uD569\uB2C8\uB2E4. */\n transRollback(transactionId?: string): Promise<{ ok: boolean }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/rollback/${txId}`);\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uCEE4\uBC0B\uD569\uB2C8\uB2E4.\n *\n * @returns `results` \uBC30\uC5F4: commit\uB41C \uAC01 \uC791\uC5C5\uC758 `entity`, `action`, `seq`\n */\n transCommit(transactionId?: string): Promise<{\n ok: boolean;\n results: Array<{ entity: string; action: string; seq: number }>;\n }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/commit/${txId}`);\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n get<T = unknown>(\n entity: string,\n seq: number,\n opts: { skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; data: T }> {\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n return this.request(\"GET\", `/v1/entity/${entity}/${seq}${q}`);\n }\n\n /** \uD398\uC774\uC9C0\uB124\uC774\uC158/\uC815\uB82C/\uD544\uD130 \uC870\uAC74\uC73C\uB85C \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n list<T = unknown>(\n entity: string,\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n const { conditions, fields, orderDir, orderBy, ...rest } = params;\n\n const queryObj: Record<string, unknown> = {\n page: 1,\n limit: 20,\n ...rest,\n };\n if (orderBy) {\n queryObj.orderBy = orderDir === \"DESC\" ? `-${orderBy}` : orderBy;\n }\n if (fields?.length) {\n queryObj.fields = fields.join(\",\");\n }\n\n const q = buildQuery(queryObj);\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/list?${q}`,\n conditions ?? {},\n );\n }\n\n /**\n * \uC5D4\uD2F0\uD2F0 \uCD1D \uAC74\uC218\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * @param conditions \uD544\uD130 \uC870\uAC74 (\uC608: `{ status: \"active\" }`)\n */\n count(\n entity: string,\n conditions?: Record<string, unknown>,\n ): Promise<{ ok: boolean; count: number }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/count`,\n conditions ?? {},\n );\n }\n\n /**\n * \uCEE4\uC2A4\uD140 SQL\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * SELECT \uC804\uC6A9\uC774\uBA70 \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * JOIN\uC744 \uC0AC\uC6A9\uD574 \uC5EC\uB7EC \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD569\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n * `entity`\uB294 SQL\uC5D0 \uD3EC\uD568\uB41C \uAE30\uBCF8 \uC5D4\uD2F0\uD2F0\uBA85(\uB77C\uC6B0\uD2B8 \uACBD\uB85C\uC6A9)\uC785\uB2C8\uB2E4.\n */\n query<T = unknown>(\n entity: string,\n req: EntityQueryRequest,\n ): Promise<{ ok: boolean; data: { items: T[]; count: number } }> {\n return this.request(\"POST\", `/v1/entity/${entity}/query`, req);\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130\uB97C \uC0DD\uC131/\uC218\uC815(Submit)\uD569\uB2C8\uB2E4. `seq`\uAC00 \uC5C6\uC73C\uBA74 INSERT, \uC788\uC73C\uBA74 UPDATE\uC785\uB2C8\uB2E4. */\n submit(\n entity: string,\n data: Record<string, unknown>,\n opts: { transactionId?: string; skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/submit${q}`,\n data,\n true,\n extraHeaders,\n );\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4(`hard=true`\uBA74 \uD558\uB4DC \uC0AD\uC81C, \uAE30\uBCF8\uC740 \uC18C\uD504\uD2B8 \uC0AD\uC81C). */\n delete(\n entity: string,\n seq: number,\n opts: {\n transactionId?: string;\n hard?: boolean;\n skipHooks?: boolean;\n } = {},\n ): Promise<{ ok: boolean; deleted: number }> {\n const params = new URLSearchParams();\n if (opts.hard) params.set(\"hard\", \"true\");\n if (opts.skipHooks) params.set(\"skipHooks\", \"true\");\n const q = params.size ? `?${params}` : \"\";\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/delete/${seq}${q}`,\n undefined,\n true,\n extraHeaders,\n );\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC758 \uBCC0\uACBD \uC774\uB825\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uC774\uB825 \uD56D\uBAA9\uB2F9 `action`, `data_snapshot`, `changed_by`, `changed_time`\uC744 \uD3EC\uD568\uD569\uB2C8\uB2E4. */\n history<T = unknown>(\n entity: string,\n seq: number,\n params: Pick<EntityListParams, \"page\" | \"limit\"> = {},\n ): Promise<{\n ok: boolean;\n data: EntityListResult<EntityHistoryRecord<T>>;\n }> {\n const q = buildQuery({ page: 1, limit: 50, ...params });\n return this.request(\"GET\", `/v1/entity/${entity}/history/${seq}?${q}`);\n }\n\n /** \uD2B9\uC815 \uC774\uB825 \uC2DC\uC810\uC73C\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uB864\uBC31\uD569\uB2C8\uB2E4. */\n rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/rollback/${historySeq}`,\n );\n }\n\n /** \uD478\uC2DC \uAD00\uB828 \uC5D4\uD2F0\uD2F0\uB85C payload\uB97C \uC804\uC1A1(Submit)\uD569\uB2C8\uB2E4. */\n push(\n pushEntity: string,\n payload: Record<string, unknown>,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(pushEntity, payload, opts);\n }\n\n /** \uD478\uC2DC \uB85C\uADF8 \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n pushLogList<T = unknown>(\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n return this.list<T>(\"push_log\", params);\n }\n\n /** \uACC4\uC815\uC758 \uD478\uC2DC \uB514\uBC14\uC774\uC2A4\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4. */\n registerPushDevice(\n accountSeq: number,\n deviceId: string,\n pushToken: string,\n opts: RegisterPushDeviceOptions = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const {\n platform,\n deviceType,\n browser,\n browserVersion,\n pushEnabled = true,\n transactionId,\n } = opts;\n\n return this.submit(\n \"account_device\",\n {\n id: deviceId,\n account_seq: accountSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n ...(platform ? { platform } : {}),\n ...(deviceType ? { device_type: deviceType } : {}),\n ...(browser ? { browser } : {}),\n ...(browserVersion ? { browser_version: browserVersion } : {}),\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4 \uB808\uCF54\uB4DC\uC758 \uD478\uC2DC \uD1A0\uD070\uC744 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n updatePushDeviceToken(\n deviceSeq: number,\n pushToken: string,\n opts: { pushEnabled?: boolean; transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const { pushEnabled = true, transactionId } = opts;\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4\uC758 \uD478\uC2DC \uC218\uC2E0\uC744 \uBE44\uD65C\uC131\uD654\uD569\uB2C8\uB2E4. */\n disablePushDevice(\n deviceSeq: number,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_enabled: false,\n },\n { transactionId: opts.transactionId },\n );\n }\n\n /**\n * \uC694\uCCAD \uBC14\uB514\uB97C \uD30C\uC2F1\uD558\uACE0 `application/octet-stream`\uC778 \uACBD\uC6B0 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC6D0\uC2DC \uC554\uD638\uD654 payload\uB97C \uC9C1\uC811 \uB2E4\uB8E8\uB294 \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n */\n readRequestBody<T = Record<string, unknown>>(\n body: ArrayBuffer | Uint8Array | string | T | null | undefined,\n contentType = \"application/json\",\n requireEncrypted = false,\n ): T {\n const lowered = contentType.toLowerCase();\n const isEncrypted = lowered.includes(\"application/octet-stream\");\n\n if (requireEncrypted && !isEncrypted) {\n throw new Error(\n \"Encrypted request required: Content-Type must be application/octet-stream\",\n );\n }\n\n if (isEncrypted) {\n if (body == null) {\n throw new Error(\"Encrypted request body is empty\");\n }\n if (body instanceof ArrayBuffer) {\n return this.decryptPacket<T>(body);\n }\n if (body instanceof Uint8Array) {\n const sliced = body.buffer.slice(\n body.byteOffset,\n body.byteOffset + body.byteLength,\n );\n return this.decryptPacket<T>(sliced as ArrayBuffer);\n }\n throw new Error(\n \"Encrypted request body must be ArrayBuffer or Uint8Array\",\n );\n }\n\n if (body == null || body === \"\") return {} as T;\n if (typeof body === \"string\") return JSON.parse(body) as T;\n return body as T;\n }\n\n /**\n * \uACF5\uD1B5 HTTP \uC694\uCCAD \uD568\uC218\uC785\uB2C8\uB2E4.\n *\n * \uC751\uB2F5\uC774 `application/octet-stream`\uC774\uBA74 \uC790\uB3D9 \uBCF5\uD638\uD654\uD558\uACE0,\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 const res = await fetch(this.baseUrl + path, {\n method,\n headers,\n ...(body != null ? { body: JSON.stringify(body) } : {}),\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 /** \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", "
|
|
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\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 /**\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 }\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 }\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 * \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 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 return data.data;\n }\n\n /** \uD2B8\uB79C\uC7AD\uC158\uC744 \uC2DC\uC791\uD558\uACE0 \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158 ID\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async transStart(): Promise<string> {\n const res = await this.request<{ ok: boolean; transaction_id: string }>(\n \"POST\",\n \"/v1/transaction/start\",\n undefined,\n false,\n );\n this.activeTxId = res.transaction_id;\n return this.activeTxId;\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uB864\uBC31\uD569\uB2C8\uB2E4. */\n transRollback(transactionId?: string): Promise<{ ok: boolean }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/rollback/${txId}`);\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uCEE4\uBC0B\uD569\uB2C8\uB2E4.\n *\n * @returns `results` \uBC30\uC5F4: commit\uB41C \uAC01 \uC791\uC5C5\uC758 `entity`, `action`, `seq`\n */\n transCommit(transactionId?: string): Promise<{\n ok: boolean;\n results: Array<{ entity: string; action: string; seq: number }>;\n }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/commit/${txId}`);\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n get<T = unknown>(\n entity: string,\n seq: number,\n opts: { skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; data: T }> {\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n return this.request(\"GET\", `/v1/entity/${entity}/${seq}${q}`);\n }\n\n /** \uD398\uC774\uC9C0\uB124\uC774\uC158/\uC815\uB82C/\uD544\uD130 \uC870\uAC74\uC73C\uB85C \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n list<T = unknown>(\n entity: string,\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n const { conditions, fields, orderDir, orderBy, ...rest } = params;\n\n const queryObj: Record<string, unknown> = {\n page: 1,\n limit: 20,\n ...rest,\n };\n if (orderBy) {\n queryObj.orderBy = orderDir === \"DESC\" ? `-${orderBy}` : orderBy;\n }\n if (fields?.length) {\n queryObj.fields = fields.join(\",\");\n }\n\n const q = buildQuery(queryObj);\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/list?${q}`,\n conditions ?? {},\n );\n }\n\n /**\n * \uC5D4\uD2F0\uD2F0 \uCD1D \uAC74\uC218\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * @param conditions \uD544\uD130 \uC870\uAC74 (\uC608: `{ status: \"active\" }`)\n */\n count(\n entity: string,\n conditions?: Record<string, unknown>,\n ): Promise<{ ok: boolean; count: number }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/count`,\n conditions ?? {},\n );\n }\n\n /**\n * \uCEE4\uC2A4\uD140 SQL\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * SELECT \uC804\uC6A9\uC774\uBA70 \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * JOIN\uC744 \uC0AC\uC6A9\uD574 \uC5EC\uB7EC \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD569\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n * `entity`\uB294 SQL\uC5D0 \uD3EC\uD568\uB41C \uAE30\uBCF8 \uC5D4\uD2F0\uD2F0\uBA85(\uB77C\uC6B0\uD2B8 \uACBD\uB85C\uC6A9)\uC785\uB2C8\uB2E4.\n */\n query<T = unknown>(\n entity: string,\n req: EntityQueryRequest,\n ): Promise<{ ok: boolean; data: { items: T[]; count: number } }> {\n return this.request(\"POST\", `/v1/entity/${entity}/query`, req);\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130\uB97C \uC0DD\uC131/\uC218\uC815(Submit)\uD569\uB2C8\uB2E4. `seq`\uAC00 \uC5C6\uC73C\uBA74 INSERT, \uC788\uC73C\uBA74 UPDATE\uC785\uB2C8\uB2E4. */\n submit(\n entity: string,\n data: Record<string, unknown>,\n opts: { transactionId?: string; skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/submit${q}`,\n data,\n true,\n extraHeaders,\n );\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4(`hard=true`\uBA74 \uD558\uB4DC \uC0AD\uC81C, \uAE30\uBCF8\uC740 \uC18C\uD504\uD2B8 \uC0AD\uC81C). */\n delete(\n entity: string,\n seq: number,\n opts: {\n transactionId?: string;\n hard?: boolean;\n skipHooks?: boolean;\n } = {},\n ): Promise<{ ok: boolean; deleted: number }> {\n const params = new URLSearchParams();\n if (opts.hard) params.set(\"hard\", \"true\");\n if (opts.skipHooks) params.set(\"skipHooks\", \"true\");\n const q = params.size ? `?${params}` : \"\";\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/delete/${seq}${q}`,\n undefined,\n true,\n extraHeaders,\n );\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC758 \uBCC0\uACBD \uC774\uB825\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uC774\uB825 \uD56D\uBAA9\uB2F9 `action`, `data_snapshot`, `changed_by`, `changed_time`\uC744 \uD3EC\uD568\uD569\uB2C8\uB2E4. */\n history<T = unknown>(\n entity: string,\n seq: number,\n params: Pick<EntityListParams, \"page\" | \"limit\"> = {},\n ): Promise<{\n ok: boolean;\n data: EntityListResult<EntityHistoryRecord<T>>;\n }> {\n const q = buildQuery({ page: 1, limit: 50, ...params });\n return this.request(\"GET\", `/v1/entity/${entity}/history/${seq}?${q}`);\n }\n\n /** \uD2B9\uC815 \uC774\uB825 \uC2DC\uC810\uC73C\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uB864\uBC31\uD569\uB2C8\uB2E4. */\n rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/rollback/${historySeq}`,\n );\n }\n\n /** \uD478\uC2DC \uAD00\uB828 \uC5D4\uD2F0\uD2F0\uB85C payload\uB97C \uC804\uC1A1(Submit)\uD569\uB2C8\uB2E4. */\n push(\n pushEntity: string,\n payload: Record<string, unknown>,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(pushEntity, payload, opts);\n }\n\n /** \uD478\uC2DC \uB85C\uADF8 \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n pushLogList<T = unknown>(\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n return this.list<T>(\"push_log\", params);\n }\n\n /** \uACC4\uC815\uC758 \uD478\uC2DC \uB514\uBC14\uC774\uC2A4\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4. */\n registerPushDevice(\n accountSeq: number,\n deviceId: string,\n pushToken: string,\n opts: RegisterPushDeviceOptions = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const {\n platform,\n deviceType,\n browser,\n browserVersion,\n pushEnabled = true,\n transactionId,\n } = opts;\n\n return this.submit(\n \"account_device\",\n {\n id: deviceId,\n account_seq: accountSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n ...(platform ? { platform } : {}),\n ...(deviceType ? { device_type: deviceType } : {}),\n ...(browser ? { browser } : {}),\n ...(browserVersion ? { browser_version: browserVersion } : {}),\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4 \uB808\uCF54\uB4DC\uC758 \uD478\uC2DC \uD1A0\uD070\uC744 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n updatePushDeviceToken(\n deviceSeq: number,\n pushToken: string,\n opts: { pushEnabled?: boolean; transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const { pushEnabled = true, transactionId } = opts;\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4\uC758 \uD478\uC2DC \uC218\uC2E0\uC744 \uBE44\uD65C\uC131\uD654\uD569\uB2C8\uB2E4. */\n disablePushDevice(\n deviceSeq: number,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_enabled: false,\n },\n { transactionId: opts.transactionId },\n );\n }\n\n /**\n * \uC694\uCCAD \uBC14\uB514\uB97C \uD30C\uC2F1\uD558\uACE0 `application/octet-stream`\uC778 \uACBD\uC6B0 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC6D0\uC2DC \uC554\uD638\uD654 payload\uB97C \uC9C1\uC811 \uB2E4\uB8E8\uB294 \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n */\n readRequestBody<T = Record<string, unknown>>(\n body: ArrayBuffer | Uint8Array | string | T | null | undefined,\n contentType = \"application/json\",\n requireEncrypted = false,\n ): T {\n const lowered = contentType.toLowerCase();\n const isEncrypted = lowered.includes(\"application/octet-stream\");\n\n if (requireEncrypted && !isEncrypted) {\n throw new Error(\n \"Encrypted request required: Content-Type must be application/octet-stream\",\n );\n }\n\n if (isEncrypted) {\n if (body == null) {\n throw new Error(\"Encrypted request body is empty\");\n }\n if (body instanceof ArrayBuffer) {\n return this.decryptPacket<T>(body);\n }\n if (body instanceof Uint8Array) {\n const sliced = body.buffer.slice(\n body.byteOffset,\n body.byteOffset + body.byteLength,\n );\n return this.decryptPacket<T>(sliced as ArrayBuffer);\n }\n throw new Error(\n \"Encrypted request body must be ArrayBuffer or Uint8Array\",\n );\n }\n\n if (body == null || body === \"\") return {} as T;\n if (typeof body === \"string\") return JSON.parse(body) as T;\n return body as T;\n }\n\n /**\n * \uACF5\uD1B5 HTTP \uC694\uCCAD \uD568\uC218\uC785\uB2C8\uB2E4.\n *\n * - `encryptRequests`\uAC00 \uD65C\uC131\uD654\uB41C \uC778\uC99D \uC694\uCCAD\uC758 POST \uBC14\uB514\uB97C \uC790\uB3D9 \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * - \uC751\uB2F5\uC774 `application/octet-stream`\uC774\uBA74 \uC790\uB3D9 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n * - JSON \uC751\uB2F5\uC758 `ok`\uAC00 false\uC774\uBA74 \uC5D0\uB7EC\uB97C \uB358\uC9D1\uB2C8\uB2E4.\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n withAuth = true,\n extraHeaders: Record<string, string> = {},\n ): Promise<T> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...extraHeaders,\n };\n if (withAuth && this.token) {\n headers.Authorization = `Bearer ${this.token}`;\n }\n\n // \uC694\uCCAD \uBC14\uB514 \uACB0\uC815: encryptRequests \uD65C\uC131\uD654 \uC2DC POST \uBC14\uB514\uB97C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n // - \uB85C\uADF8\uC778/\uD1A0\uD070 \uAC31\uC2E0(withAuth=false)\uC740 \uC554\uD638\uD654\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.\n // - GET \uC740 \uBC14\uB514\uAC00 \uC5C6\uC73C\uBBC0\uB85C \uAC74\uB108\uB701\uB2C8\uB2E4.\n let fetchBody: string | Uint8Array | null = null;\n if (body != null) {\n const shouldEncrypt =\n this.encryptRequests &&\n withAuth &&\n this.token &&\n method !== \"GET\" &&\n method !== \"HEAD\";\n\n if (shouldEncrypt) {\n const plaintext = new TextEncoder().encode(\n JSON.stringify(body),\n );\n const encrypted = this.encryptPacket(plaintext);\n headers[\"Content-Type\"] = \"application/octet-stream\";\n fetchBody = encrypted;\n } else {\n fetchBody = JSON.stringify(body);\n }\n }\n\n const res = await fetch(this.baseUrl + path, {\n method,\n headers,\n ...(fetchBody != null ? { body: fetchBody as BodyInit } : {}),\n });\n\n const contentType = res.headers.get(\"Content-Type\") ?? \"\";\n\n if (contentType.includes(\"application/octet-stream\")) {\n const buffer = await res.arrayBuffer();\n return this.decryptPacket<T>(buffer);\n }\n\n const data = await res.json();\n if (!data.ok) {\n const err = new Error(\n data.message ?? `EntityServer error (HTTP ${res.status})`,\n );\n (err as { status?: number }).status = res.status;\n throw err;\n }\n return data as T;\n }\n\n /**\n * \uD3C9\uBB38 \uBC14\uC774\uD2B8\uB97C XChaCha20-Poly1305\uB85C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * \uD3EC\uB9F7: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]\n */\n private encryptPacket(plaintext: Uint8Array): Uint8Array {\n const key = sha256(new TextEncoder().encode(this.token));\n const magic = new Uint8Array(this.packetMagicLen);\n const nonce = new Uint8Array(24);\n crypto.getRandomValues(magic);\n crypto.getRandomValues(nonce);\n const cipher = xchacha20poly1305(key, nonce);\n const ciphertext = cipher.encrypt(plaintext);\n const result = new Uint8Array(\n this.packetMagicLen + 24 + ciphertext.length,\n );\n result.set(magic, 0);\n result.set(nonce, this.packetMagicLen);\n result.set(ciphertext, this.packetMagicLen + 24);\n return result;\n }\n\n /** \uC11C\uBC84\uC758 \uC554\uD638\uD654 \uD328\uD0B7\uC744 \uBCF5\uD638\uD654\uD574 JSON \uAC1D\uCCB4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\n private decryptPacket<T>(buffer: ArrayBuffer): T {\n const key = sha256(new TextEncoder().encode(this.token));\n const data = new Uint8Array(buffer);\n\n if (data.length < this.packetMagicLen + 24 + 16) {\n throw new Error(\"Encrypted packet too short\");\n }\n\n const nonce = data.slice(this.packetMagicLen, this.packetMagicLen + 24);\n const ciphertext = data.slice(this.packetMagicLen + 24);\n const cipher = xchacha20poly1305(key, nonce);\n const plaintext = cipher.decrypt(ciphertext);\n return JSON.parse(new TextDecoder().decode(plaintext)) as T;\n }\n}\n\n/** \uCFFC\uB9AC \uD30C\uB77C\uBBF8\uD130 \uAC1D\uCCB4\uB97C URL \uCFFC\uB9AC \uBB38\uC790\uC5F4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\nfunction buildQuery(params: Record<string, unknown>): string {\n return Object.entries(params)\n .filter(([, value]) => value != null)\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key === \"orderBy\" ? \"order_by\" : key)}=${encodeURIComponent(String(value))}`,\n )\n .join(\"&\");\n}\n\nexport const entityServer = new EntityServerClient();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["xchacha20poly1305", "sha256", "readEnv", "name", "EntityServerClient", "options", "envBaseUrl", "envMagicLen", "token", "length", "data", "email", "password", "refreshToken", "res", "transactionId", "txId", "entity", "seq", "opts", "q", "params", "conditions", "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", "err", "key", "magic", "nonce", "ciphertext", "result", "value", "entityServer"]
|
|
7
7
|
}
|
package/dist/react.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useMemo as
|
|
1
|
+
import{useMemo as T}from"react";import{xchacha20poly1305 as g}from"@noble/ciphers/chacha";import{sha256 as k}from"@noble/hashes/sha2";function h(c){return import.meta?.env?.[c]}var p=class{baseUrl;token;packetMagicLen;encryptRequests;activeTxId=null;constructor(e={}){let t=h("VITE_ENTITY_SERVER_URL"),n=h("VITE_ENTITY_SERVER_PACKET_MAGIC_LEN");this.baseUrl=(e.baseUrl??t??"http://localhost:47200").replace(/\/$/,""),this.token=e.token??"",this.packetMagicLen=e.packetMagicLen??(n?Number(n):4),this.encryptRequests=e.encryptRequests??!1}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)}setToken(e){this.token=e}setPacketMagicLen(e){this.packetMagicLen=e}getPacketMagicLen(){return this.packetMagicLen}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,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,t.data}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}`)}list(e,t={}){let{conditions:n,fields:r,orderDir:i,orderBy:s,...o}=t,a={page:1,limit:20,...o};s&&(a.orderBy=i==="DESC"?`-${s}`:s),r?.length&&(a.fields=r.join(","));let d=y(a);return this.request("POST",`/v1/entity/${e}/list?${d}`,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,i=r?{"X-Transaction-ID":r}:void 0,s=n.skipHooks?"?skipHooks=true":"";return this.request("POST",`/v1/entity/${e}/submit${s}`,t,!0,i)}delete(e,t,n={}){let r=new URLSearchParams;n.hard&&r.set("hard","true"),n.skipHooks&&r.set("skipHooks","true");let i=r.size?`?${r}`:"",s=n.transactionId??this.activeTxId,o=s?{"X-Transaction-ID":s}:void 0;return this.request("POST",`/v1/entity/${e}/delete/${t}${i}`,void 0,!0,o)}history(e,t,n={}){let r=y({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:i,deviceType:s,browser:o,browserVersion:a,pushEnabled:d=!0,transactionId:u}=r;return this.submit("account_device",{id:t,account_seq:e,push_token:n,push_enabled:d,...i?{platform:i}:{},...s?{device_type:s}:{},...o?{browser:o}:{},...a?{browser_version:a}:{}},{transactionId:u})}updatePushDeviceToken(e,t,n={}){let{pushEnabled:r=!0,transactionId:i}=n;return this.submit("account_device",{seq:e,push_token:t,push_enabled:r},{transactionId:i})}disablePushDevice(e,t={}){return this.submit("account_device",{seq:e,push_enabled:!1},{transactionId:t.transactionId})}readRequestBody(e,t="application/json",n=!1){let i=t.toLowerCase().includes("application/octet-stream");if(n&&!i)throw new Error("Encrypted request required: Content-Type must be application/octet-stream");if(i){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,i={}){let s={"Content-Type":"application/json",...i};r&&this.token&&(s.Authorization=`Bearer ${this.token}`);let o=null;if(n!=null)if(this.encryptRequests&&r&&this.token&&e!=="GET"&&e!=="HEAD"){let f=new TextEncoder().encode(JSON.stringify(n)),b=this.encryptPacket(f);s["Content-Type"]="application/octet-stream",o=b}else o=JSON.stringify(n);let a=await fetch(this.baseUrl+t,{method:e,headers:s,...o!=null?{body:o}:{}});if((a.headers.get("Content-Type")??"").includes("application/octet-stream")){let l=await a.arrayBuffer();return this.decryptPacket(l)}let u=await a.json();if(!u.ok){let l=new Error(u.message??`EntityServer error (HTTP ${a.status})`);throw l.status=a.status,l}return u}encryptPacket(e){let t=k(new TextEncoder().encode(this.token)),n=new Uint8Array(this.packetMagicLen),r=new Uint8Array(24);crypto.getRandomValues(n),crypto.getRandomValues(r);let s=g(t,r).encrypt(e),o=new Uint8Array(this.packetMagicLen+24+s.length);return o.set(n,0),o.set(r,this.packetMagicLen),o.set(s,this.packetMagicLen+24),o}decryptPacket(e){let t=k(new TextEncoder().encode(this.token)),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),i=n.slice(this.packetMagicLen+24),o=g(t,r).decrypt(i);return JSON.parse(new TextDecoder().decode(o))}};function y(c){return Object.entries(c).filter(([,e])=>e!=null).map(([e,t])=>`${encodeURIComponent(e==="orderBy"?"order_by":e)}=${encodeURIComponent(String(t))}`).join("&")}var m=new p;function R(c={}){let{singleton:e=!0,tokenResolver:t,baseUrl:n,packetMagicLen:r,token:i}=c;return T(()=>{let s=e?m:new p({baseUrl:n,packetMagicLen:r,token:i});e&&s.configure({baseUrl:n,packetMagicLen:r,token:i});let o=t?.();return typeof o=="string"&&s.setToken(o),s},[e,t,n,r,i])}export{R 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 { useMemo } from \"react\";\nimport {\n EntityServerClient,\n entityServer,\n type EntityServerClientOptions,\n} from \"../index\";\n\nexport interface UseEntityServerOptions extends EntityServerClientOptions {\n singleton?: boolean;\n tokenResolver?: () => string | undefined | null;\n}\n\n/**\n * React \uD658\uACBD\uC5D0\uC11C EntityServerClient \uC778\uC2A4\uD134\uC2A4\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.\n *\n * - `singleton=true`(\uAE30\uBCF8): \uD328\uD0A4\uC9C0 \uC804\uC5ED `entityServer` \uC778\uC2A4\uD134\uC2A4\uB97C \uBC18\uD658\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 */\nexport function useEntityServer(\n options: UseEntityServerOptions = {},\n): EntityServerClient {\n const {\n singleton = true,\n tokenResolver,\n baseUrl,\n packetMagicLen,\n token,\n } = options;\n\n return useMemo(() => {\n const client = singleton\n ? entityServer\n : new EntityServerClient({\n baseUrl,\n packetMagicLen,\n token,\n });\n\n if (singleton) {\n client.configure({ baseUrl, packetMagicLen, token });\n }\n\n const resolvedToken = tokenResolver?.();\n if (typeof resolvedToken === \"string\") {\n client.setToken(resolvedToken);\n }\n\n return client;\n }, [singleton, tokenResolver, baseUrl, packetMagicLen, token]);\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\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 activeTxId: string | 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_PACKET_MAGIC_LEN` \uB610\uB294 `4`\n */\n constructor(options: EntityServerClientOptions = {}) {\n const envBaseUrl = readEnv(\"VITE_ENTITY_SERVER_URL\");\n const envMagicLen = readEnv(\"VITE_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 }\n\n /** baseUrl, token, packetMagicLen \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 }\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 /** \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 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 return data.data;\n }\n\n /** \uD2B8\uB79C\uC7AD\uC158\uC744 \uC2DC\uC791\uD558\uACE0 \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158 ID\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async transStart(): Promise<string> {\n const res = await this.request<{ ok: boolean; transaction_id: string }>(\n \"POST\",\n \"/v1/transaction/start\",\n undefined,\n false,\n );\n this.activeTxId = res.transaction_id;\n return this.activeTxId;\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uB864\uBC31\uD569\uB2C8\uB2E4. */\n transRollback(transactionId?: string): Promise<{ ok: boolean }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/rollback/${txId}`);\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uCEE4\uBC0B\uD569\uB2C8\uB2E4.\n *\n * @returns `results` \uBC30\uC5F4: commit\uB41C \uAC01 \uC791\uC5C5\uC758 `entity`, `action`, `seq`\n */\n transCommit(transactionId?: string): Promise<{\n ok: boolean;\n results: Array<{ entity: string; action: string; seq: number }>;\n }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/commit/${txId}`);\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n get<T = unknown>(\n entity: string,\n seq: number,\n opts: { skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; data: T }> {\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n return this.request(\"GET\", `/v1/entity/${entity}/${seq}${q}`);\n }\n\n /** \uD398\uC774\uC9C0\uB124\uC774\uC158/\uC815\uB82C/\uD544\uD130 \uC870\uAC74\uC73C\uB85C \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n list<T = unknown>(\n entity: string,\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n const { conditions, fields, orderDir, orderBy, ...rest } = params;\n\n const queryObj: Record<string, unknown> = {\n page: 1,\n limit: 20,\n ...rest,\n };\n if (orderBy) {\n queryObj.orderBy = orderDir === \"DESC\" ? `-${orderBy}` : orderBy;\n }\n if (fields?.length) {\n queryObj.fields = fields.join(\",\");\n }\n\n const q = buildQuery(queryObj);\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/list?${q}`,\n conditions ?? {},\n );\n }\n\n /**\n * \uC5D4\uD2F0\uD2F0 \uCD1D \uAC74\uC218\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * @param conditions \uD544\uD130 \uC870\uAC74 (\uC608: `{ status: \"active\" }`)\n */\n count(\n entity: string,\n conditions?: Record<string, unknown>,\n ): Promise<{ ok: boolean; count: number }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/count`,\n conditions ?? {},\n );\n }\n\n /**\n * \uCEE4\uC2A4\uD140 SQL\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * SELECT \uC804\uC6A9\uC774\uBA70 \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * JOIN\uC744 \uC0AC\uC6A9\uD574 \uC5EC\uB7EC \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD569\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n * `entity`\uB294 SQL\uC5D0 \uD3EC\uD568\uB41C \uAE30\uBCF8 \uC5D4\uD2F0\uD2F0\uBA85(\uB77C\uC6B0\uD2B8 \uACBD\uB85C\uC6A9)\uC785\uB2C8\uB2E4.\n */\n query<T = unknown>(\n entity: string,\n req: EntityQueryRequest,\n ): Promise<{ ok: boolean; data: { items: T[]; count: number } }> {\n return this.request(\"POST\", `/v1/entity/${entity}/query`, req);\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130\uB97C \uC0DD\uC131/\uC218\uC815(Submit)\uD569\uB2C8\uB2E4. `seq`\uAC00 \uC5C6\uC73C\uBA74 INSERT, \uC788\uC73C\uBA74 UPDATE\uC785\uB2C8\uB2E4. */\n submit(\n entity: string,\n data: Record<string, unknown>,\n opts: { transactionId?: string; skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/submit${q}`,\n data,\n true,\n extraHeaders,\n );\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4(`hard=true`\uBA74 \uD558\uB4DC \uC0AD\uC81C, \uAE30\uBCF8\uC740 \uC18C\uD504\uD2B8 \uC0AD\uC81C). */\n delete(\n entity: string,\n seq: number,\n opts: {\n transactionId?: string;\n hard?: boolean;\n skipHooks?: boolean;\n } = {},\n ): Promise<{ ok: boolean; deleted: number }> {\n const params = new URLSearchParams();\n if (opts.hard) params.set(\"hard\", \"true\");\n if (opts.skipHooks) params.set(\"skipHooks\", \"true\");\n const q = params.size ? `?${params}` : \"\";\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/delete/${seq}${q}`,\n undefined,\n true,\n extraHeaders,\n );\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC758 \uBCC0\uACBD \uC774\uB825\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uC774\uB825 \uD56D\uBAA9\uB2F9 `action`, `data_snapshot`, `changed_by`, `changed_time`\uC744 \uD3EC\uD568\uD569\uB2C8\uB2E4. */\n history<T = unknown>(\n entity: string,\n seq: number,\n params: Pick<EntityListParams, \"page\" | \"limit\"> = {},\n ): Promise<{\n ok: boolean;\n data: EntityListResult<EntityHistoryRecord<T>>;\n }> {\n const q = buildQuery({ page: 1, limit: 50, ...params });\n return this.request(\"GET\", `/v1/entity/${entity}/history/${seq}?${q}`);\n }\n\n /** \uD2B9\uC815 \uC774\uB825 \uC2DC\uC810\uC73C\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uB864\uBC31\uD569\uB2C8\uB2E4. */\n rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/rollback/${historySeq}`,\n );\n }\n\n /** \uD478\uC2DC \uAD00\uB828 \uC5D4\uD2F0\uD2F0\uB85C payload\uB97C \uC804\uC1A1(Submit)\uD569\uB2C8\uB2E4. */\n push(\n pushEntity: string,\n payload: Record<string, unknown>,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(pushEntity, payload, opts);\n }\n\n /** \uD478\uC2DC \uB85C\uADF8 \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n pushLogList<T = unknown>(\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n return this.list<T>(\"push_log\", params);\n }\n\n /** \uACC4\uC815\uC758 \uD478\uC2DC \uB514\uBC14\uC774\uC2A4\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4. */\n registerPushDevice(\n accountSeq: number,\n deviceId: string,\n pushToken: string,\n opts: RegisterPushDeviceOptions = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const {\n platform,\n deviceType,\n browser,\n browserVersion,\n pushEnabled = true,\n transactionId,\n } = opts;\n\n return this.submit(\n \"account_device\",\n {\n id: deviceId,\n account_seq: accountSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n ...(platform ? { platform } : {}),\n ...(deviceType ? { device_type: deviceType } : {}),\n ...(browser ? { browser } : {}),\n ...(browserVersion ? { browser_version: browserVersion } : {}),\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4 \uB808\uCF54\uB4DC\uC758 \uD478\uC2DC \uD1A0\uD070\uC744 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n updatePushDeviceToken(\n deviceSeq: number,\n pushToken: string,\n opts: { pushEnabled?: boolean; transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const { pushEnabled = true, transactionId } = opts;\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4\uC758 \uD478\uC2DC \uC218\uC2E0\uC744 \uBE44\uD65C\uC131\uD654\uD569\uB2C8\uB2E4. */\n disablePushDevice(\n deviceSeq: number,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_enabled: false,\n },\n { transactionId: opts.transactionId },\n );\n }\n\n /**\n * \uC694\uCCAD \uBC14\uB514\uB97C \uD30C\uC2F1\uD558\uACE0 `application/octet-stream`\uC778 \uACBD\uC6B0 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC6D0\uC2DC \uC554\uD638\uD654 payload\uB97C \uC9C1\uC811 \uB2E4\uB8E8\uB294 \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n */\n readRequestBody<T = Record<string, unknown>>(\n body: ArrayBuffer | Uint8Array | string | T | null | undefined,\n contentType = \"application/json\",\n requireEncrypted = false,\n ): T {\n const lowered = contentType.toLowerCase();\n const isEncrypted = lowered.includes(\"application/octet-stream\");\n\n if (requireEncrypted && !isEncrypted) {\n throw new Error(\n \"Encrypted request required: Content-Type must be application/octet-stream\",\n );\n }\n\n if (isEncrypted) {\n if (body == null) {\n throw new Error(\"Encrypted request body is empty\");\n }\n if (body instanceof ArrayBuffer) {\n return this.decryptPacket<T>(body);\n }\n if (body instanceof Uint8Array) {\n const sliced = body.buffer.slice(\n body.byteOffset,\n body.byteOffset + body.byteLength,\n );\n return this.decryptPacket<T>(sliced as ArrayBuffer);\n }\n throw new Error(\n \"Encrypted request body must be ArrayBuffer or Uint8Array\",\n );\n }\n\n if (body == null || body === \"\") return {} as T;\n if (typeof body === \"string\") return JSON.parse(body) as T;\n return body as T;\n }\n\n /**\n * \uACF5\uD1B5 HTTP \uC694\uCCAD \uD568\uC218\uC785\uB2C8\uB2E4.\n *\n * \uC751\uB2F5\uC774 `application/octet-stream`\uC774\uBA74 \uC790\uB3D9 \uBCF5\uD638\uD654\uD558\uACE0,\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 const res = await fetch(this.baseUrl + path, {\n method,\n headers,\n ...(body != null ? { body: JSON.stringify(body) } : {}),\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 /** \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": "AAAA,OAAS,WAAAA,MAAe,QCCxB,OAAS,qBAAAC,MAAyB,wBAElC,OAAS,UAAAC,MAAc,
|
|
6
|
-
"names": ["useMemo", "xchacha20poly1305", "sha256", "readEnv", "name", "EntityServerClient", "options", "envBaseUrl", "envMagicLen", "token", "length", "
|
|
4
|
+
"sourcesContent": ["import { useMemo } from \"react\";\nimport {\n EntityServerClient,\n entityServer,\n type EntityServerClientOptions,\n} from \"../index\";\n\nexport interface UseEntityServerOptions extends EntityServerClientOptions {\n singleton?: boolean;\n tokenResolver?: () => string | undefined | null;\n}\n\n/**\n * React \uD658\uACBD\uC5D0\uC11C EntityServerClient \uC778\uC2A4\uD134\uC2A4\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.\n *\n * - `singleton=true`(\uAE30\uBCF8): \uD328\uD0A4\uC9C0 \uC804\uC5ED `entityServer` \uC778\uC2A4\uD134\uC2A4\uB97C \uBC18\uD658\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 */\nexport function useEntityServer(\n options: UseEntityServerOptions = {},\n): EntityServerClient {\n const {\n singleton = true,\n tokenResolver,\n baseUrl,\n packetMagicLen,\n token,\n } = options;\n\n return useMemo(() => {\n const client = singleton\n ? entityServer\n : new EntityServerClient({\n baseUrl,\n packetMagicLen,\n token,\n });\n\n if (singleton) {\n client.configure({ baseUrl, packetMagicLen, token });\n }\n\n const resolvedToken = tokenResolver?.();\n if (typeof resolvedToken === \"string\") {\n client.setToken(resolvedToken);\n }\n\n return client;\n }, [singleton, tokenResolver, baseUrl, packetMagicLen, token]);\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\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 /**\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 }\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 }\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 * \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 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 return data.data;\n }\n\n /** \uD2B8\uB79C\uC7AD\uC158\uC744 \uC2DC\uC791\uD558\uACE0 \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158 ID\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. */\n async transStart(): Promise<string> {\n const res = await this.request<{ ok: boolean; transaction_id: string }>(\n \"POST\",\n \"/v1/transaction/start\",\n undefined,\n false,\n );\n this.activeTxId = res.transaction_id;\n return this.activeTxId;\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uB864\uBC31\uD569\uB2C8\uB2E4. */\n transRollback(transactionId?: string): Promise<{ ok: boolean }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/rollback/${txId}`);\n }\n\n /** \uD65C\uC131 \uD2B8\uB79C\uC7AD\uC158(\uB610\uB294 \uC804\uB2EC\uB41C transactionId)\uC744 \uCEE4\uBC0B\uD569\uB2C8\uB2E4.\n *\n * @returns `results` \uBC30\uC5F4: commit\uB41C \uAC01 \uC791\uC5C5\uC758 `entity`, `action`, `seq`\n */\n transCommit(transactionId?: string): Promise<{\n ok: boolean;\n results: Array<{ entity: string; action: string; seq: number }>;\n }> {\n const txId = transactionId ?? this.activeTxId;\n if (!txId) {\n return Promise.reject(\n new Error(\"No active transaction. Call transStart() first.\"),\n );\n }\n this.activeTxId = null;\n return this.request(\"POST\", `/v1/transaction/commit/${txId}`);\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n get<T = unknown>(\n entity: string,\n seq: number,\n opts: { skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; data: T }> {\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n return this.request(\"GET\", `/v1/entity/${entity}/${seq}${q}`);\n }\n\n /** \uD398\uC774\uC9C0\uB124\uC774\uC158/\uC815\uB82C/\uD544\uD130 \uC870\uAC74\uC73C\uB85C \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n list<T = unknown>(\n entity: string,\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n const { conditions, fields, orderDir, orderBy, ...rest } = params;\n\n const queryObj: Record<string, unknown> = {\n page: 1,\n limit: 20,\n ...rest,\n };\n if (orderBy) {\n queryObj.orderBy = orderDir === \"DESC\" ? `-${orderBy}` : orderBy;\n }\n if (fields?.length) {\n queryObj.fields = fields.join(\",\");\n }\n\n const q = buildQuery(queryObj);\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/list?${q}`,\n conditions ?? {},\n );\n }\n\n /**\n * \uC5D4\uD2F0\uD2F0 \uCD1D \uAC74\uC218\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * @param conditions \uD544\uD130 \uC870\uAC74 (\uC608: `{ status: \"active\" }`)\n */\n count(\n entity: string,\n conditions?: Record<string, unknown>,\n ): Promise<{ ok: boolean; count: number }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/count`,\n conditions ?? {},\n );\n }\n\n /**\n * \uCEE4\uC2A4\uD140 SQL\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.\n *\n * SELECT \uC804\uC6A9\uC774\uBA70 \uC778\uB371\uC2A4 \uD14C\uC774\uBE14\uB9CC \uC870\uD68C \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n * JOIN\uC744 \uC0AC\uC6A9\uD574 \uC5EC\uB7EC \uC5D4\uD2F0\uD2F0\uB97C \uC870\uD569\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n * `entity`\uB294 SQL\uC5D0 \uD3EC\uD568\uB41C \uAE30\uBCF8 \uC5D4\uD2F0\uD2F0\uBA85(\uB77C\uC6B0\uD2B8 \uACBD\uB85C\uC6A9)\uC785\uB2C8\uB2E4.\n */\n query<T = unknown>(\n entity: string,\n req: EntityQueryRequest,\n ): Promise<{ ok: boolean; data: { items: T[]; count: number } }> {\n return this.request(\"POST\", `/v1/entity/${entity}/query`, req);\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB370\uC774\uD130\uB97C \uC0DD\uC131/\uC218\uC815(Submit)\uD569\uB2C8\uB2E4. `seq`\uAC00 \uC5C6\uC73C\uBA74 INSERT, \uC788\uC73C\uBA74 UPDATE\uC785\uB2C8\uB2E4. */\n submit(\n entity: string,\n data: Record<string, unknown>,\n opts: { transactionId?: string; skipHooks?: boolean } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n const q = opts.skipHooks ? \"?skipHooks=true\" : \"\";\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/submit${q}`,\n data,\n true,\n extraHeaders,\n );\n }\n\n /** \uC2DC\uD000\uC2A4 ID\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4(`hard=true`\uBA74 \uD558\uB4DC \uC0AD\uC81C, \uAE30\uBCF8\uC740 \uC18C\uD504\uD2B8 \uC0AD\uC81C). */\n delete(\n entity: string,\n seq: number,\n opts: {\n transactionId?: string;\n hard?: boolean;\n skipHooks?: boolean;\n } = {},\n ): Promise<{ ok: boolean; deleted: number }> {\n const params = new URLSearchParams();\n if (opts.hard) params.set(\"hard\", \"true\");\n if (opts.skipHooks) params.set(\"skipHooks\", \"true\");\n const q = params.size ? `?${params}` : \"\";\n const txId = opts.transactionId ?? this.activeTxId;\n const extraHeaders = txId ? { \"X-Transaction-ID\": txId } : undefined;\n\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/delete/${seq}${q}`,\n undefined,\n true,\n extraHeaders,\n );\n }\n\n /** \uC5D4\uD2F0\uD2F0 \uB2E8\uAC74\uC758 \uBCC0\uACBD \uC774\uB825\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uC774\uB825 \uD56D\uBAA9\uB2F9 `action`, `data_snapshot`, `changed_by`, `changed_time`\uC744 \uD3EC\uD568\uD569\uB2C8\uB2E4. */\n history<T = unknown>(\n entity: string,\n seq: number,\n params: Pick<EntityListParams, \"page\" | \"limit\"> = {},\n ): Promise<{\n ok: boolean;\n data: EntityListResult<EntityHistoryRecord<T>>;\n }> {\n const q = buildQuery({ page: 1, limit: 50, ...params });\n return this.request(\"GET\", `/v1/entity/${entity}/history/${seq}?${q}`);\n }\n\n /** \uD2B9\uC815 \uC774\uB825 \uC2DC\uC810\uC73C\uB85C \uC5D4\uD2F0\uD2F0\uB97C \uB864\uBC31\uD569\uB2C8\uB2E4. */\n rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {\n return this.request(\n \"POST\",\n `/v1/entity/${entity}/rollback/${historySeq}`,\n );\n }\n\n /** \uD478\uC2DC \uAD00\uB828 \uC5D4\uD2F0\uD2F0\uB85C payload\uB97C \uC804\uC1A1(Submit)\uD569\uB2C8\uB2E4. */\n push(\n pushEntity: string,\n payload: Record<string, unknown>,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(pushEntity, payload, opts);\n }\n\n /** \uD478\uC2DC \uB85C\uADF8 \uC5D4\uD2F0\uD2F0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. */\n pushLogList<T = unknown>(\n params: EntityListParams = {},\n ): Promise<{ ok: boolean; data: EntityListResult<T> }> {\n return this.list<T>(\"push_log\", params);\n }\n\n /** \uACC4\uC815\uC758 \uD478\uC2DC \uB514\uBC14\uC774\uC2A4\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4. */\n registerPushDevice(\n accountSeq: number,\n deviceId: string,\n pushToken: string,\n opts: RegisterPushDeviceOptions = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const {\n platform,\n deviceType,\n browser,\n browserVersion,\n pushEnabled = true,\n transactionId,\n } = opts;\n\n return this.submit(\n \"account_device\",\n {\n id: deviceId,\n account_seq: accountSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n ...(platform ? { platform } : {}),\n ...(deviceType ? { device_type: deviceType } : {}),\n ...(browser ? { browser } : {}),\n ...(browserVersion ? { browser_version: browserVersion } : {}),\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4 \uB808\uCF54\uB4DC\uC758 \uD478\uC2DC \uD1A0\uD070\uC744 \uAC31\uC2E0\uD569\uB2C8\uB2E4. */\n updatePushDeviceToken(\n deviceSeq: number,\n pushToken: string,\n opts: { pushEnabled?: boolean; transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n const { pushEnabled = true, transactionId } = opts;\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_token: pushToken,\n push_enabled: pushEnabled,\n },\n { transactionId },\n );\n }\n\n /** \uB514\uBC14\uC774\uC2A4\uC758 \uD478\uC2DC \uC218\uC2E0\uC744 \uBE44\uD65C\uC131\uD654\uD569\uB2C8\uB2E4. */\n disablePushDevice(\n deviceSeq: number,\n opts: { transactionId?: string } = {},\n ): Promise<{ ok: boolean; seq: number }> {\n return this.submit(\n \"account_device\",\n {\n seq: deviceSeq,\n push_enabled: false,\n },\n { transactionId: opts.transactionId },\n );\n }\n\n /**\n * \uC694\uCCAD \uBC14\uB514\uB97C \uD30C\uC2F1\uD558\uACE0 `application/octet-stream`\uC778 \uACBD\uC6B0 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n *\n * \uC6D0\uC2DC \uC554\uD638\uD654 payload\uB97C \uC9C1\uC811 \uB2E4\uB8E8\uB294 \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.\n */\n readRequestBody<T = Record<string, unknown>>(\n body: ArrayBuffer | Uint8Array | string | T | null | undefined,\n contentType = \"application/json\",\n requireEncrypted = false,\n ): T {\n const lowered = contentType.toLowerCase();\n const isEncrypted = lowered.includes(\"application/octet-stream\");\n\n if (requireEncrypted && !isEncrypted) {\n throw new Error(\n \"Encrypted request required: Content-Type must be application/octet-stream\",\n );\n }\n\n if (isEncrypted) {\n if (body == null) {\n throw new Error(\"Encrypted request body is empty\");\n }\n if (body instanceof ArrayBuffer) {\n return this.decryptPacket<T>(body);\n }\n if (body instanceof Uint8Array) {\n const sliced = body.buffer.slice(\n body.byteOffset,\n body.byteOffset + body.byteLength,\n );\n return this.decryptPacket<T>(sliced as ArrayBuffer);\n }\n throw new Error(\n \"Encrypted request body must be ArrayBuffer or Uint8Array\",\n );\n }\n\n if (body == null || body === \"\") return {} as T;\n if (typeof body === \"string\") return JSON.parse(body) as T;\n return body as T;\n }\n\n /**\n * \uACF5\uD1B5 HTTP \uC694\uCCAD \uD568\uC218\uC785\uB2C8\uB2E4.\n *\n * - `encryptRequests`\uAC00 \uD65C\uC131\uD654\uB41C \uC778\uC99D \uC694\uCCAD\uC758 POST \uBC14\uB514\uB97C \uC790\uB3D9 \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * - \uC751\uB2F5\uC774 `application/octet-stream`\uC774\uBA74 \uC790\uB3D9 \uBCF5\uD638\uD654\uD569\uB2C8\uB2E4.\n * - JSON \uC751\uB2F5\uC758 `ok`\uAC00 false\uC774\uBA74 \uC5D0\uB7EC\uB97C \uB358\uC9D1\uB2C8\uB2E4.\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n withAuth = true,\n extraHeaders: Record<string, string> = {},\n ): Promise<T> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...extraHeaders,\n };\n if (withAuth && this.token) {\n headers.Authorization = `Bearer ${this.token}`;\n }\n\n // \uC694\uCCAD \uBC14\uB514 \uACB0\uC815: encryptRequests \uD65C\uC131\uD654 \uC2DC POST \uBC14\uB514\uB97C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n // - \uB85C\uADF8\uC778/\uD1A0\uD070 \uAC31\uC2E0(withAuth=false)\uC740 \uC554\uD638\uD654\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.\n // - GET \uC740 \uBC14\uB514\uAC00 \uC5C6\uC73C\uBBC0\uB85C \uAC74\uB108\uB701\uB2C8\uB2E4.\n let fetchBody: string | Uint8Array | null = null;\n if (body != null) {\n const shouldEncrypt =\n this.encryptRequests &&\n withAuth &&\n this.token &&\n method !== \"GET\" &&\n method !== \"HEAD\";\n\n if (shouldEncrypt) {\n const plaintext = new TextEncoder().encode(\n JSON.stringify(body),\n );\n const encrypted = this.encryptPacket(plaintext);\n headers[\"Content-Type\"] = \"application/octet-stream\";\n fetchBody = encrypted;\n } else {\n fetchBody = JSON.stringify(body);\n }\n }\n\n const res = await fetch(this.baseUrl + path, {\n method,\n headers,\n ...(fetchBody != null ? { body: fetchBody as BodyInit } : {}),\n });\n\n const contentType = res.headers.get(\"Content-Type\") ?? \"\";\n\n if (contentType.includes(\"application/octet-stream\")) {\n const buffer = await res.arrayBuffer();\n return this.decryptPacket<T>(buffer);\n }\n\n const data = await res.json();\n if (!data.ok) {\n const err = new Error(\n data.message ?? `EntityServer error (HTTP ${res.status})`,\n );\n (err as { status?: number }).status = res.status;\n throw err;\n }\n return data as T;\n }\n\n /**\n * \uD3C9\uBB38 \uBC14\uC774\uD2B8\uB97C XChaCha20-Poly1305\uB85C \uC554\uD638\uD654\uD569\uB2C8\uB2E4.\n * \uD3EC\uB9F7: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]\n */\n private encryptPacket(plaintext: Uint8Array): Uint8Array {\n const key = sha256(new TextEncoder().encode(this.token));\n const magic = new Uint8Array(this.packetMagicLen);\n const nonce = new Uint8Array(24);\n crypto.getRandomValues(magic);\n crypto.getRandomValues(nonce);\n const cipher = xchacha20poly1305(key, nonce);\n const ciphertext = cipher.encrypt(plaintext);\n const result = new Uint8Array(\n this.packetMagicLen + 24 + ciphertext.length,\n );\n result.set(magic, 0);\n result.set(nonce, this.packetMagicLen);\n result.set(ciphertext, this.packetMagicLen + 24);\n return result;\n }\n\n /** \uC11C\uBC84\uC758 \uC554\uD638\uD654 \uD328\uD0B7\uC744 \uBCF5\uD638\uD654\uD574 JSON \uAC1D\uCCB4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\n private decryptPacket<T>(buffer: ArrayBuffer): T {\n const key = sha256(new TextEncoder().encode(this.token));\n const data = new Uint8Array(buffer);\n\n if (data.length < this.packetMagicLen + 24 + 16) {\n throw new Error(\"Encrypted packet too short\");\n }\n\n const nonce = data.slice(this.packetMagicLen, this.packetMagicLen + 24);\n const ciphertext = data.slice(this.packetMagicLen + 24);\n const cipher = xchacha20poly1305(key, nonce);\n const plaintext = cipher.decrypt(ciphertext);\n return JSON.parse(new TextDecoder().decode(plaintext)) as T;\n }\n}\n\n/** \uCFFC\uB9AC \uD30C\uB77C\uBBF8\uD130 \uAC1D\uCCB4\uB97C URL \uCFFC\uB9AC \uBB38\uC790\uC5F4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. */\nfunction buildQuery(params: Record<string, unknown>): string {\n return Object.entries(params)\n .filter(([, value]) => value != null)\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key === \"orderBy\" ? \"order_by\" : key)}=${encodeURIComponent(String(value))}`,\n )\n .join(\"&\");\n}\n\nexport const entityServer = new EntityServerClient();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["useMemo", "xchacha20poly1305", "sha256", "readEnv", "name", "EntityServerClient", "options", "envBaseUrl", "envMagicLen", "token", "length", "data", "email", "password", "refreshToken", "res", "transactionId", "txId", "entity", "seq", "opts", "q", "params", "conditions", "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", "err", "key", "magic", "nonce", "ciphertext", "result", "value", "entityServer", "useEntityServer", "options", "singleton", "tokenResolver", "baseUrl", "packetMagicLen", "token", "useMemo", "client", "entityServer", "EntityServerClient", "resolvedToken"]
|
|
7
7
|
}
|
package/docs/apis.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
| 구분 | 바로가기 |
|
|
6
6
|
| ------------------ | ---------------------------------------- |
|
|
7
7
|
| import | [import](#import) |
|
|
8
|
+
| 서버 헬스체크 | [서버 헬스체크](#서버-헬스체크) |
|
|
8
9
|
| 인스턴스 생성/설정 | [인스턴스 생성/설정](#인스턴스-생성설정) |
|
|
9
10
|
| 인증 | [인증](#인증) |
|
|
10
11
|
| 트랜잭션 | [트랜잭션](#트랜잭션) |
|
|
@@ -37,15 +38,61 @@ import { useEntityServer } from "entity-server-client/react";
|
|
|
37
38
|
|
|
38
39
|
---
|
|
39
40
|
|
|
41
|
+
## 서버 헬스체크
|
|
42
|
+
|
|
43
|
+
### 개요
|
|
44
|
+
|
|
45
|
+
앱 시작 시 서버의 패킷 암호화 설정을 감지하여 클라이언트 설정을 자동으로 맞춥니다.
|
|
46
|
+
|
|
47
|
+
**헬스체크 응답:**
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"ok": true,
|
|
52
|
+
"packet_encryption": false
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 자동 초기화 (권장)
|
|
57
|
+
|
|
58
|
+
**admin-web 예시:**
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { checkServerHealth, entityServer } from "entity-server-client";
|
|
62
|
+
|
|
63
|
+
// 앱 초기화 시 한 번 실행 (예: App.tsx mount, main.tsx 초기화)
|
|
64
|
+
const health = await checkServerHealth();
|
|
65
|
+
// → 서버의 packet_encryption: true 이면
|
|
66
|
+
// 클라이언트의 encryptRequests: true 로 자동 설정됨
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**수동 초기화:**
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const res = await fetch("/v1/health");
|
|
73
|
+
const { packet_encryption } = await res.json();
|
|
74
|
+
|
|
75
|
+
if (packet_encryption) {
|
|
76
|
+
entityServer.configure({ encryptRequests: true });
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> **중요:** `packetMagicLen` 값은 클라이언트가 미리 알고 있어야 합니다.
|
|
81
|
+
> 빌드 시점에 `VITE_PACKET_MAGIC_LEN` 환경변수로 결정되며,
|
|
82
|
+
> 서버와 일치해야 올바르게 복호화됩니다.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
40
86
|
## 인스턴스 생성/설정
|
|
41
87
|
|
|
42
88
|
### `new EntityServerClient(options?)`
|
|
43
89
|
|
|
44
|
-
| 옵션
|
|
45
|
-
|
|
|
46
|
-
| `baseUrl`
|
|
47
|
-
| `token`
|
|
48
|
-
| `packetMagicLen`
|
|
90
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
91
|
+
| ----------------- | --------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
92
|
+
| `baseUrl` | `string` | `VITE_ENTITY_SERVER_URL` 또는 `http://localhost:47200` | 서버 주소 |
|
|
93
|
+
| `token` | `string` | `""` | JWT Access Token |
|
|
94
|
+
| `packetMagicLen` | `number` | `VITE_PACKET_MAGIC_LEN` 또는 `4` | 암호화 패킷 magic 바이트 길이 |
|
|
95
|
+
| `encryptRequests` | `boolean` | `false` | `true` 로 설정하면 인증된 POST/PUT/PATCH 요청 바디를 XChaCha20-Poly1305 로 암호화하여 전송합니다. 서버에서 `requirePacketEncryption = true` 로 설정된 경우 반드시 활성화해야 합니다. |
|
|
49
96
|
|
|
50
97
|
```ts
|
|
51
98
|
// 직접 생성
|
|
@@ -53,6 +100,7 @@ const client = new EntityServerClient({
|
|
|
53
100
|
baseUrl: "https://api.example.com",
|
|
54
101
|
token: "eyJhbGciOi...",
|
|
55
102
|
packetMagicLen: 4,
|
|
103
|
+
encryptRequests: true, // 요청 바디 암호화 활성화
|
|
56
104
|
});
|
|
57
105
|
|
|
58
106
|
// 싱글톤 (환경변수 자동 읽기)
|
|
@@ -68,6 +116,7 @@ client.configure({
|
|
|
68
116
|
baseUrl: "https://api.example.com",
|
|
69
117
|
token: "new-access-token",
|
|
70
118
|
packetMagicLen: 6,
|
|
119
|
+
encryptRequests: true,
|
|
71
120
|
});
|
|
72
121
|
```
|
|
73
122
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -87,6 +87,15 @@ export interface EntityServerClientOptions {
|
|
|
87
87
|
baseUrl?: string;
|
|
88
88
|
token?: string;
|
|
89
89
|
packetMagicLen?: number;
|
|
90
|
+
/**
|
|
91
|
+
* `true`이면 인증된 POST/PUT 요청 바디를 XChaCha20-Poly1305로 암호화합니다.
|
|
92
|
+
*
|
|
93
|
+
* 서버의 `EnablePacketEncryption`이 활성화된 경우 필수로 설정해야 합니다.
|
|
94
|
+
* 로그인(`login()`)·토큰 갱신(`refreshToken()`)은 인증 전 요청이므로 자동으로 건너뜁니다.
|
|
95
|
+
*
|
|
96
|
+
* 기본값: `false`
|
|
97
|
+
*/
|
|
98
|
+
encryptRequests?: boolean;
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
/**
|
|
@@ -139,6 +148,7 @@ export class EntityServerClient {
|
|
|
139
148
|
private baseUrl: string;
|
|
140
149
|
private token: string;
|
|
141
150
|
private packetMagicLen: number;
|
|
151
|
+
private encryptRequests: boolean;
|
|
142
152
|
private activeTxId: string | null = null;
|
|
143
153
|
|
|
144
154
|
/**
|
|
@@ -146,11 +156,11 @@ export class EntityServerClient {
|
|
|
146
156
|
*
|
|
147
157
|
* 기본값:
|
|
148
158
|
* - `baseUrl`: `VITE_ENTITY_SERVER_URL` 또는 `http://localhost:47200`
|
|
149
|
-
* - `packetMagicLen`: `
|
|
159
|
+
* - `packetMagicLen`: `VITE_ENTITY_SERVER_PACKET_MAGIC_LEN` 또는 `4`
|
|
150
160
|
*/
|
|
151
161
|
constructor(options: EntityServerClientOptions = {}) {
|
|
152
162
|
const envBaseUrl = readEnv("VITE_ENTITY_SERVER_URL");
|
|
153
|
-
const envMagicLen = readEnv("
|
|
163
|
+
const envMagicLen = readEnv("VITE_ENTITY_SERVER_PACKET_MAGIC_LEN");
|
|
154
164
|
|
|
155
165
|
this.baseUrl = (
|
|
156
166
|
options.baseUrl ??
|
|
@@ -161,9 +171,10 @@ export class EntityServerClient {
|
|
|
161
171
|
this.token = options.token ?? "";
|
|
162
172
|
this.packetMagicLen =
|
|
163
173
|
options.packetMagicLen ?? (envMagicLen ? Number(envMagicLen) : 4);
|
|
174
|
+
this.encryptRequests = options.encryptRequests ?? false;
|
|
164
175
|
}
|
|
165
176
|
|
|
166
|
-
/** baseUrl, token, packetMagicLen 값을 런타임에 갱신합니다. */
|
|
177
|
+
/** baseUrl, token, packetMagicLen, encryptRequests 값을 런타임에 갱신합니다. */
|
|
167
178
|
configure(options: Partial<EntityServerClientOptions>): void {
|
|
168
179
|
if (options.baseUrl) {
|
|
169
180
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
@@ -174,6 +185,9 @@ export class EntityServerClient {
|
|
|
174
185
|
if (typeof options.packetMagicLen === "number") {
|
|
175
186
|
this.packetMagicLen = options.packetMagicLen;
|
|
176
187
|
}
|
|
188
|
+
if (typeof options.encryptRequests === "boolean") {
|
|
189
|
+
this.encryptRequests = options.encryptRequests;
|
|
190
|
+
}
|
|
177
191
|
}
|
|
178
192
|
|
|
179
193
|
/** 인증 요청에 사용할 JWT Access Token을 설정합니다. */
|
|
@@ -191,6 +205,33 @@ export class EntityServerClient {
|
|
|
191
205
|
return this.packetMagicLen;
|
|
192
206
|
}
|
|
193
207
|
|
|
208
|
+
/**
|
|
209
|
+
* 서버 헬스 체크를 수행하고 패킷 암호화 활성 여부를 자동으로 감지합니다.
|
|
210
|
+
*
|
|
211
|
+
* 서버가 `packet_encryption: true`를 응답하면 이후 모든 요청에 암호화가 자동 적용됩니다.
|
|
212
|
+
* 초기화 직후 또는 로그인 전에 호출하면 암호화 설정을 자동으로 구성할 수 있습니다.
|
|
213
|
+
*
|
|
214
|
+
* ```ts
|
|
215
|
+
* await client.checkHealth();
|
|
216
|
+
* await client.login(email, password); // 이후 요청은 암호화 자동 적용
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* @returns `{ ok: true }` 또는 `{ ok: true, packet_encryption: true }`
|
|
220
|
+
*/
|
|
221
|
+
async checkHealth(): Promise<{ ok: boolean; packet_encryption?: boolean }> {
|
|
222
|
+
const res = await fetch(`${this.baseUrl}/v1/health`, {
|
|
223
|
+
signal: AbortSignal.timeout(3000),
|
|
224
|
+
});
|
|
225
|
+
const data = (await res.json()) as {
|
|
226
|
+
ok: boolean;
|
|
227
|
+
packet_encryption?: boolean;
|
|
228
|
+
};
|
|
229
|
+
if (data.packet_encryption) {
|
|
230
|
+
this.encryptRequests = true;
|
|
231
|
+
}
|
|
232
|
+
return data;
|
|
233
|
+
}
|
|
234
|
+
|
|
194
235
|
/** 로그인 후 `access_token`을 내부 상태에 저장합니다. */
|
|
195
236
|
async login(
|
|
196
237
|
email: string,
|
|
@@ -524,8 +565,9 @@ export class EntityServerClient {
|
|
|
524
565
|
/**
|
|
525
566
|
* 공통 HTTP 요청 함수입니다.
|
|
526
567
|
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
568
|
+
* - `encryptRequests`가 활성화된 인증 요청의 POST 바디를 자동 암호화합니다.
|
|
569
|
+
* - 응답이 `application/octet-stream`이면 자동 복호화합니다.
|
|
570
|
+
* - JSON 응답의 `ok`가 false이면 에러를 던집니다.
|
|
529
571
|
*/
|
|
530
572
|
private async request<T>(
|
|
531
573
|
method: string,
|
|
@@ -542,10 +584,34 @@ export class EntityServerClient {
|
|
|
542
584
|
headers.Authorization = `Bearer ${this.token}`;
|
|
543
585
|
}
|
|
544
586
|
|
|
587
|
+
// 요청 바디 결정: encryptRequests 활성화 시 POST 바디를 암호화합니다.
|
|
588
|
+
// - 로그인/토큰 갱신(withAuth=false)은 암호화하지 않습니다.
|
|
589
|
+
// - GET 은 바디가 없으므로 건너뜁니다.
|
|
590
|
+
let fetchBody: string | Uint8Array | null = null;
|
|
591
|
+
if (body != null) {
|
|
592
|
+
const shouldEncrypt =
|
|
593
|
+
this.encryptRequests &&
|
|
594
|
+
withAuth &&
|
|
595
|
+
this.token &&
|
|
596
|
+
method !== "GET" &&
|
|
597
|
+
method !== "HEAD";
|
|
598
|
+
|
|
599
|
+
if (shouldEncrypt) {
|
|
600
|
+
const plaintext = new TextEncoder().encode(
|
|
601
|
+
JSON.stringify(body),
|
|
602
|
+
);
|
|
603
|
+
const encrypted = this.encryptPacket(plaintext);
|
|
604
|
+
headers["Content-Type"] = "application/octet-stream";
|
|
605
|
+
fetchBody = encrypted;
|
|
606
|
+
} else {
|
|
607
|
+
fetchBody = JSON.stringify(body);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
545
611
|
const res = await fetch(this.baseUrl + path, {
|
|
546
612
|
method,
|
|
547
613
|
headers,
|
|
548
|
-
...(
|
|
614
|
+
...(fetchBody != null ? { body: fetchBody as BodyInit } : {}),
|
|
549
615
|
});
|
|
550
616
|
|
|
551
617
|
const contentType = res.headers.get("Content-Type") ?? "";
|
|
@@ -566,6 +632,27 @@ export class EntityServerClient {
|
|
|
566
632
|
return data as T;
|
|
567
633
|
}
|
|
568
634
|
|
|
635
|
+
/**
|
|
636
|
+
* 평문 바이트를 XChaCha20-Poly1305로 암호화합니다.
|
|
637
|
+
* 포맷: [random_magic:packetMagicLen][random_nonce:24][ciphertext+tag]
|
|
638
|
+
*/
|
|
639
|
+
private encryptPacket(plaintext: Uint8Array): Uint8Array {
|
|
640
|
+
const key = sha256(new TextEncoder().encode(this.token));
|
|
641
|
+
const magic = new Uint8Array(this.packetMagicLen);
|
|
642
|
+
const nonce = new Uint8Array(24);
|
|
643
|
+
crypto.getRandomValues(magic);
|
|
644
|
+
crypto.getRandomValues(nonce);
|
|
645
|
+
const cipher = xchacha20poly1305(key, nonce);
|
|
646
|
+
const ciphertext = cipher.encrypt(plaintext);
|
|
647
|
+
const result = new Uint8Array(
|
|
648
|
+
this.packetMagicLen + 24 + ciphertext.length,
|
|
649
|
+
);
|
|
650
|
+
result.set(magic, 0);
|
|
651
|
+
result.set(nonce, this.packetMagicLen);
|
|
652
|
+
result.set(ciphertext, this.packetMagicLen + 24);
|
|
653
|
+
return result;
|
|
654
|
+
}
|
|
655
|
+
|
|
569
656
|
/** 서버의 암호화 패킷을 복호화해 JSON 객체로 변환합니다. */
|
|
570
657
|
private decryptPacket<T>(buffer: ArrayBuffer): T {
|
|
571
658
|
const key = sha256(new TextEncoder().encode(this.token));
|