create-entity-server 0.0.15 → 0.0.23
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/bin/create.js +15 -7
- package/package.json +1 -1
- package/template/.env.example +8 -7
- package/template/configs/database.json +173 -10
- package/template/entities/Account/account_audit.json +4 -5
- package/template/entities/System/system_audit_log.json +14 -8
- package/template/samples/README.md +28 -22
- package/template/samples/browser/entity-server-client.js +453 -0
- package/template/samples/browser/example.html +498 -0
- package/template/samples/entities/02_types_and_defaults.json +15 -16
- package/template/samples/entities/04_fk_and_composite_unique.json +0 -2
- package/template/samples/entities/05_cache.json +9 -8
- package/template/samples/entities/06_history_and_hard_delete.json +27 -9
- package/template/samples/entities/07_license_scope.json +40 -31
- package/template/samples/entities/09_hook_entity.json +0 -6
- package/template/samples/entities/10_hook_submit_delete.json +5 -2
- package/template/samples/entities/11_hook_webhook.json +9 -7
- package/template/samples/entities/12_hook_push.json +3 -3
- package/template/samples/entities/13_read_only.json +13 -10
- package/template/samples/entities/15_reset_defaults.json +0 -1
- package/template/samples/entities/16_isolated_license.json +62 -0
- package/template/samples/entities/README.md +36 -39
- package/template/samples/flutter/lib/entity_server_client.dart +170 -48
- package/template/samples/java/EntityServerClient.java +208 -61
- package/template/samples/java/EntityServerExample.java +4 -3
- package/template/samples/kotlin/EntityServerClient.kt +175 -45
- package/template/samples/node/src/EntityServerClient.js +232 -59
- package/template/samples/node/src/example.js +9 -9
- package/template/samples/php/ci4/Config/EntityServer.php +0 -1
- package/template/samples/php/ci4/Libraries/EntityServer.php +206 -53
- package/template/samples/php/laravel/Services/EntityServerService.php +190 -41
- package/template/samples/python/entity_server.py +181 -68
- package/template/samples/python/example.py +7 -6
- package/template/samples/react/src/example.tsx +41 -25
- package/template/samples/swift/EntityServerClient.swift +143 -37
- package/template/scripts/run.ps1 +12 -3
- package/template/scripts/run.sh +12 -8
- package/template/scripts/update-server.ps1 +68 -2
- package/template/scripts/update-server.sh +59 -2
- package/template/samples/entities/order_notification.json +0 -51
- package/template/samples/react/src/api/entityServerClient.ts +0 -413
- package/template/samples/react/src/hooks/useEntity.ts +0 -173
|
@@ -19,6 +19,56 @@ REPO="ehfuse/entity-server"
|
|
|
19
19
|
BINARIES=("entity-server" "entity-cli")
|
|
20
20
|
DIST_DIRS=("scripts" "samples")
|
|
21
21
|
|
|
22
|
+
_find_running_pid() {
|
|
23
|
+
local pid_file="$PROJECT_ROOT/.run/entity-server.pid"
|
|
24
|
+
if [ -f "$pid_file" ]; then
|
|
25
|
+
local pid
|
|
26
|
+
pid=$(cat "$pid_file" 2>/dev/null)
|
|
27
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
28
|
+
echo "$pid"
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
pgrep -f "${PROJECT_ROOT}/(bin/)?entity-server( |$)" | head -n 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_ensure_server_stopped() {
|
|
37
|
+
local pid
|
|
38
|
+
pid="$(_find_running_pid)"
|
|
39
|
+
|
|
40
|
+
if [ -z "$pid" ]; then
|
|
41
|
+
return 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo ""
|
|
45
|
+
echo "⚠️ 현재 Entity Server가 실행 중입니다."
|
|
46
|
+
ps -p "$pid" -o pid,etime,cmd --no-headers 2>/dev/null || true
|
|
47
|
+
echo ""
|
|
48
|
+
read -r -p "업데이트를 위해 서버를 중지할까요? [y/N]: " input
|
|
49
|
+
input=$(echo "$input" | tr '[:upper:]' '[:lower:]')
|
|
50
|
+
|
|
51
|
+
if [ "$input" != "y" ] && [ "$input" != "yes" ]; then
|
|
52
|
+
echo "❌ 업데이트를 취소했습니다."
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [ -x "$PROJECT_ROOT/scripts/run.sh" ]; then
|
|
57
|
+
"$PROJECT_ROOT/scripts/run.sh" stop || true
|
|
58
|
+
else
|
|
59
|
+
kill "$pid" 2>/dev/null || true
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
sleep 0.2
|
|
63
|
+
pid="$(_find_running_pid)"
|
|
64
|
+
if [ -n "$pid" ]; then
|
|
65
|
+
echo "❌ 서버 중지에 실패했습니다. 업데이트를 중단합니다."
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
echo "✅ 서버 중지 완료"
|
|
70
|
+
}
|
|
71
|
+
|
|
22
72
|
# ── 플랫폼 감지 ───────────────────────────────────────────────────────────────
|
|
23
73
|
|
|
24
74
|
OS="$(uname -s)"
|
|
@@ -46,7 +96,10 @@ esac
|
|
|
46
96
|
# ── 현재 버전 확인 ────────────────────────────────────────────────────────────
|
|
47
97
|
|
|
48
98
|
_current_ver() {
|
|
49
|
-
local bin="$PROJECT_ROOT/entity-server"
|
|
99
|
+
local bin="$PROJECT_ROOT/bin/entity-server"
|
|
100
|
+
if [ ! -x "$bin" ] && [ -x "$PROJECT_ROOT/entity-server" ]; then
|
|
101
|
+
bin="$PROJECT_ROOT/entity-server"
|
|
102
|
+
fi
|
|
50
103
|
if [ -x "$bin" ]; then
|
|
51
104
|
"$bin" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "(알 수 없음)"
|
|
52
105
|
else
|
|
@@ -100,15 +153,19 @@ _install() {
|
|
|
100
153
|
local current_ver
|
|
101
154
|
current_ver="$(_current_ver)"
|
|
102
155
|
|
|
156
|
+
_ensure_server_stopped
|
|
157
|
+
|
|
103
158
|
echo ""
|
|
104
159
|
echo "📦 entity-server v${target_ver} 다운로드 중... (${PLATFORM}-${ARCH_TAG})"
|
|
105
160
|
echo ""
|
|
106
161
|
|
|
107
162
|
# 바이너리 다운로드
|
|
163
|
+
mkdir -p "$PROJECT_ROOT/bin"
|
|
164
|
+
|
|
108
165
|
for BIN in "${BINARIES[@]}"; do
|
|
109
166
|
local file="${BIN}-${PLATFORM}-${ARCH_TAG}"
|
|
110
167
|
local url="https://github.com/${REPO}/releases/download/v${target_ver}/${file}"
|
|
111
|
-
local dest="$PROJECT_ROOT/$BIN"
|
|
168
|
+
local dest="$PROJECT_ROOT/bin/$BIN"
|
|
112
169
|
|
|
113
170
|
printf " ↓ %-32s" "$file"
|
|
114
171
|
if _download "$url" "$dest" 2>/dev/null; then
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "order_notification",
|
|
3
|
-
"description": "주문 알림 예제 — 주문 생성 시 고객에게 푸시 전송",
|
|
4
|
-
"index": {
|
|
5
|
-
"customer_seq": {
|
|
6
|
-
"comment": "고객 user seq",
|
|
7
|
-
"required": true
|
|
8
|
-
},
|
|
9
|
-
"order_number": {
|
|
10
|
-
"comment": "주문 번호",
|
|
11
|
-
"required": true
|
|
12
|
-
},
|
|
13
|
-
"status": {
|
|
14
|
-
"comment": "주문 상태",
|
|
15
|
-
"type": ["placed", "confirmed", "shipped", "delivered", "cancelled"],
|
|
16
|
-
"default": "placed"
|
|
17
|
-
},
|
|
18
|
-
"total_amount": {
|
|
19
|
-
"comment": "주문 금액",
|
|
20
|
-
"type": "uint"
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
"hooks": {
|
|
24
|
-
"after_insert": [
|
|
25
|
-
{
|
|
26
|
-
"type": "push",
|
|
27
|
-
"target_user_field": "customer_seq",
|
|
28
|
-
"title": "주문 접수",
|
|
29
|
-
"push_body": "주문 #${new.order_number}이 접수되었습니다. 금액: ${new.total_amount}원",
|
|
30
|
-
"push_data": {
|
|
31
|
-
"action": "order_created",
|
|
32
|
-
"order_seq": "${new.seq}",
|
|
33
|
-
"order_number": "${new.order_number}"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
],
|
|
37
|
-
"after_update": [
|
|
38
|
-
{
|
|
39
|
-
"type": "push",
|
|
40
|
-
"target_user_field": "customer_seq",
|
|
41
|
-
"title": "주문 상태 변경",
|
|
42
|
-
"push_body": "주문 #${new.order_number} 상태: ${new.status}",
|
|
43
|
-
"push_data": {
|
|
44
|
-
"action": "order_updated",
|
|
45
|
-
"order_seq": "${new.seq}",
|
|
46
|
-
"status": "${new.status}"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
]
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entity Server API 클라이언트 (React / 브라우저)
|
|
3
|
-
*
|
|
4
|
-
* 브라우저 환경에서는 HMAC secret을 노출할 수 없으므로 JWT Bearer 토큰 방식을 사용합니다.
|
|
5
|
-
*
|
|
6
|
-
* 패킷 암호화 지원:
|
|
7
|
-
* 서버가 application/octet-stream 으로 응답하면 자동으로 복호화합니다.
|
|
8
|
-
* 복호화 키: sha256(access_token)
|
|
9
|
-
* 의존성: @noble/ciphers, @noble/hashes (npm install @noble/ciphers @noble/hashes)
|
|
10
|
-
*
|
|
11
|
-
* 환경변수 (Vite):
|
|
12
|
-
* VITE_ENTITY_SERVER_URL=http://localhost:47200
|
|
13
|
-
* VITE_PACKET_MAGIC_LEN=4 (서버 packet_magic_len 과 동일하게)
|
|
14
|
-
*
|
|
15
|
-
* 트랜잭션 사용 예:
|
|
16
|
-
* await es.transStart();
|
|
17
|
-
* try {
|
|
18
|
-
* const orderRef = await es.submit('order', { user_seq: 1, total: 9900 }); // seq: "$tx.0"
|
|
19
|
-
* await es.submit('order_item', { order_seq: orderRef.seq, item_seq: 5 }); // "$tx.0" 자동 치환
|
|
20
|
-
* const result = await es.transCommit();
|
|
21
|
-
* const orderSeq = result.results[0].seq; // 실제 seq
|
|
22
|
-
* } catch (e) {
|
|
23
|
-
* await es.transRollback();
|
|
24
|
-
* }
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { xchacha20_poly1305 } from "@noble/ciphers/chacha";
|
|
28
|
-
import { sha256 } from "@noble/hashes/sha2";
|
|
29
|
-
|
|
30
|
-
export interface EntityListParams {
|
|
31
|
-
page?: number;
|
|
32
|
-
limit?: number;
|
|
33
|
-
orderBy?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface EntityQueryFilter {
|
|
37
|
-
field: string;
|
|
38
|
-
op: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "like" | "in";
|
|
39
|
-
value: unknown;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface RegisterPushDeviceOptions {
|
|
43
|
-
platform?: string;
|
|
44
|
-
deviceType?: string;
|
|
45
|
-
browser?: string;
|
|
46
|
-
browserVersion?: string;
|
|
47
|
-
pushEnabled?: boolean;
|
|
48
|
-
transactionId?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export class EntityServerClient {
|
|
52
|
-
private baseUrl: string;
|
|
53
|
-
private token: string;
|
|
54
|
-
private magicLen: number;
|
|
55
|
-
private activeTxId: string | null = null;
|
|
56
|
-
|
|
57
|
-
constructor(baseUrl?: string, token?: string) {
|
|
58
|
-
this.baseUrl = (
|
|
59
|
-
baseUrl ??
|
|
60
|
-
(import.meta as unknown as Record<string, Record<string, string>>)
|
|
61
|
-
.env?.VITE_ENTITY_SERVER_URL ??
|
|
62
|
-
"http://localhost:47200"
|
|
63
|
-
).replace(/\/$/, "");
|
|
64
|
-
this.token = token ?? "";
|
|
65
|
-
const envMagic = (
|
|
66
|
-
import.meta as unknown as Record<string, Record<string, string>>
|
|
67
|
-
).env?.VITE_PACKET_MAGIC_LEN;
|
|
68
|
-
this.magicLen = envMagic ? Number(envMagic) : 4;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
setToken(token: string): void {
|
|
72
|
-
this.token = token;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ─── 인증 ────────────────────────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
async login(
|
|
78
|
-
email: string,
|
|
79
|
-
password: string,
|
|
80
|
-
): Promise<{
|
|
81
|
-
access_token: string;
|
|
82
|
-
refresh_token: string;
|
|
83
|
-
expires_in: number;
|
|
84
|
-
}> {
|
|
85
|
-
const data = await this.request<{
|
|
86
|
-
data: {
|
|
87
|
-
access_token: string;
|
|
88
|
-
refresh_token: string;
|
|
89
|
-
expires_in: number;
|
|
90
|
-
};
|
|
91
|
-
}>("POST", "/v1/auth/login", { email, passwd: password }, false);
|
|
92
|
-
this.token = data.data.access_token;
|
|
93
|
-
return data.data;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async refreshToken(
|
|
97
|
-
refreshToken: string,
|
|
98
|
-
): Promise<{ access_token: string; expires_in: number }> {
|
|
99
|
-
const data = await this.request<{
|
|
100
|
-
data: { access_token: string; expires_in: number };
|
|
101
|
-
}>("POST", "/v1/auth/refresh", { refresh_token: refreshToken }, false);
|
|
102
|
-
this.token = data.data.access_token;
|
|
103
|
-
return data.data;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ─── 트랜잭션 ──────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* 트랜잭션 시작 — 서버에 트랜잭션 큐를 등록하고 transaction_id 를 반환합니다.
|
|
110
|
-
* 이후 submit / delete 가 서버 큐에 쌓이고 transCommit() 시 일괄 처리됩니다.
|
|
111
|
-
*/
|
|
112
|
-
async transStart(): Promise<string> {
|
|
113
|
-
const res = await this.request<{ ok: boolean; transaction_id: string }>(
|
|
114
|
-
"POST",
|
|
115
|
-
"/v1/transaction/start",
|
|
116
|
-
undefined,
|
|
117
|
-
false,
|
|
118
|
-
);
|
|
119
|
-
this.activeTxId = res.transaction_id;
|
|
120
|
-
return this.activeTxId;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* 트랜잭션 단위로 변경사항을 롤백합니다.
|
|
125
|
-
* transactionId 생략 시 transStart() 로 시작한 활성 트랜잭션을 사용합니다.
|
|
126
|
-
*/
|
|
127
|
-
transRollback(transactionId?: string): Promise<{ ok: boolean }> {
|
|
128
|
-
const txId = transactionId ?? this.activeTxId;
|
|
129
|
-
if (!txId)
|
|
130
|
-
return Promise.reject(
|
|
131
|
-
new Error("No active transaction. Call transStart() first."),
|
|
132
|
-
);
|
|
133
|
-
this.activeTxId = null;
|
|
134
|
-
return this.request("POST", `/v1/transaction/rollback/${txId}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 트랜잭션 커밋 — 서버 큐에 쌓인 작업을 단일 DB 트랜잭션으로 일괄 처리합니다.
|
|
139
|
-
* transactionId 생략 시 transStart() 로 시작한 활성 트랜잭션을 사용합니다.
|
|
140
|
-
*/
|
|
141
|
-
transCommit(
|
|
142
|
-
transactionId?: string,
|
|
143
|
-
): Promise<{ ok: boolean; results: unknown[] }> {
|
|
144
|
-
const txId = transactionId ?? this.activeTxId;
|
|
145
|
-
if (!txId)
|
|
146
|
-
return Promise.reject(
|
|
147
|
-
new Error("No active transaction. Call transStart() first."),
|
|
148
|
-
);
|
|
149
|
-
this.activeTxId = null;
|
|
150
|
-
return this.request("POST", `/v1/transaction/commit/${txId}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ─── CRUD ────────────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
get<T = unknown>(
|
|
156
|
-
entity: string,
|
|
157
|
-
seq: number,
|
|
158
|
-
): Promise<{ ok: boolean; data: T }> {
|
|
159
|
-
return this.request("GET", `/v1/entity/${entity}/${seq}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
list<T = unknown>(
|
|
163
|
-
entity: string,
|
|
164
|
-
params: EntityListParams = {},
|
|
165
|
-
): Promise<{ ok: boolean; data: T[]; total: number }> {
|
|
166
|
-
const q = buildQuery({ page: 1, limit: 20, ...params });
|
|
167
|
-
return this.request("GET", `/v1/entity/${entity}/list?${q}`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
count(entity: string): Promise<{ ok: boolean; count: number }> {
|
|
171
|
-
return this.request("GET", `/v1/entity/${entity}/count`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
query<T = unknown>(
|
|
175
|
-
entity: string,
|
|
176
|
-
filter: EntityQueryFilter[] = [],
|
|
177
|
-
params: EntityListParams = {},
|
|
178
|
-
): Promise<{ ok: boolean; data: T[]; total: number }> {
|
|
179
|
-
const q = buildQuery({ page: 1, limit: 20, ...params });
|
|
180
|
-
return this.request("POST", `/v1/entity/${entity}/query?${q}`, filter);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
submit<T = unknown>(
|
|
184
|
-
entity: string,
|
|
185
|
-
data: Record<string, unknown>,
|
|
186
|
-
opts: { transactionId?: string } = {},
|
|
187
|
-
): Promise<{ ok: boolean; seq?: number; data?: T }> {
|
|
188
|
-
const txId = opts.transactionId ?? this.activeTxId;
|
|
189
|
-
const extra = txId ? { "X-Transaction-ID": txId } : {};
|
|
190
|
-
return this.request(
|
|
191
|
-
"POST",
|
|
192
|
-
`/v1/entity/${entity}/submit`,
|
|
193
|
-
data,
|
|
194
|
-
true,
|
|
195
|
-
extra,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
delete(
|
|
200
|
-
entity: string,
|
|
201
|
-
seq: number,
|
|
202
|
-
opts: { transactionId?: string; hard?: boolean } = {},
|
|
203
|
-
): Promise<{ ok: boolean }> {
|
|
204
|
-
const q = opts.hard ? "?hard=true" : "";
|
|
205
|
-
const txId = opts.transactionId ?? this.activeTxId;
|
|
206
|
-
const extra = txId ? { "X-Transaction-ID": txId } : {};
|
|
207
|
-
return this.request(
|
|
208
|
-
"DELETE",
|
|
209
|
-
`/v1/entity/${entity}/delete/${seq}${q}`,
|
|
210
|
-
undefined,
|
|
211
|
-
true,
|
|
212
|
-
extra,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
history<T = unknown>(
|
|
217
|
-
entity: string,
|
|
218
|
-
seq: number,
|
|
219
|
-
params: EntityListParams = {},
|
|
220
|
-
): Promise<{ ok: boolean; data: T[] }> {
|
|
221
|
-
const q = buildQuery({ page: 1, limit: 50, ...params });
|
|
222
|
-
return this.request("GET", `/v1/entity/${entity}/history/${seq}?${q}`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {
|
|
226
|
-
return this.request(
|
|
227
|
-
"POST",
|
|
228
|
-
`/v1/entity/${entity}/rollback/${historySeq}`,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
push<T = unknown>(
|
|
233
|
-
pushEntity: string,
|
|
234
|
-
payload: Record<string, unknown>,
|
|
235
|
-
opts: { transactionId?: string } = {},
|
|
236
|
-
): Promise<{ ok: boolean; seq?: number; data?: T }> {
|
|
237
|
-
return this.submit<T>(pushEntity, payload, opts);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
pushLogList<T = unknown>(
|
|
241
|
-
params: EntityListParams = {},
|
|
242
|
-
): Promise<{ ok: boolean; data: T[]; total: number }> {
|
|
243
|
-
return this.list<T>("push_log", params);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
registerPushDevice<T = unknown>(
|
|
247
|
-
accountSeq: number,
|
|
248
|
-
deviceId: string,
|
|
249
|
-
pushToken: string,
|
|
250
|
-
opts: RegisterPushDeviceOptions = {},
|
|
251
|
-
): Promise<{ ok: boolean; seq?: number; data?: T }> {
|
|
252
|
-
const {
|
|
253
|
-
platform,
|
|
254
|
-
deviceType,
|
|
255
|
-
browser,
|
|
256
|
-
browserVersion,
|
|
257
|
-
pushEnabled = true,
|
|
258
|
-
transactionId,
|
|
259
|
-
} = opts;
|
|
260
|
-
|
|
261
|
-
return this.submit<T>(
|
|
262
|
-
"account_device",
|
|
263
|
-
{
|
|
264
|
-
id: deviceId,
|
|
265
|
-
account_seq: accountSeq,
|
|
266
|
-
push_token: pushToken,
|
|
267
|
-
push_enabled: pushEnabled,
|
|
268
|
-
...(platform ? { platform } : {}),
|
|
269
|
-
...(deviceType ? { device_type: deviceType } : {}),
|
|
270
|
-
...(browser ? { browser } : {}),
|
|
271
|
-
...(browserVersion ? { browser_version: browserVersion } : {}),
|
|
272
|
-
},
|
|
273
|
-
{ transactionId },
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
updatePushDeviceToken<T = unknown>(
|
|
278
|
-
deviceSeq: number,
|
|
279
|
-
pushToken: string,
|
|
280
|
-
opts: { pushEnabled?: boolean; transactionId?: string } = {},
|
|
281
|
-
): Promise<{ ok: boolean; seq?: number; data?: T }> {
|
|
282
|
-
const { pushEnabled = true, transactionId } = opts;
|
|
283
|
-
return this.submit<T>(
|
|
284
|
-
"account_device",
|
|
285
|
-
{
|
|
286
|
-
seq: deviceSeq,
|
|
287
|
-
push_token: pushToken,
|
|
288
|
-
push_enabled: pushEnabled,
|
|
289
|
-
},
|
|
290
|
-
{ transactionId },
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
disablePushDevice<T = unknown>(
|
|
295
|
-
deviceSeq: number,
|
|
296
|
-
opts: { transactionId?: string } = {},
|
|
297
|
-
): Promise<{ ok: boolean; seq?: number; data?: T }> {
|
|
298
|
-
return this.submit<T>(
|
|
299
|
-
"account_device",
|
|
300
|
-
{
|
|
301
|
-
seq: deviceSeq,
|
|
302
|
-
push_enabled: false,
|
|
303
|
-
},
|
|
304
|
-
{ transactionId: opts.transactionId },
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
readRequestBody<T = Record<string, unknown>>(
|
|
309
|
-
body: ArrayBuffer | Uint8Array | string | T | null | undefined,
|
|
310
|
-
contentType = "application/json",
|
|
311
|
-
requireEncrypted = false,
|
|
312
|
-
): T {
|
|
313
|
-
const lowered = contentType.toLowerCase();
|
|
314
|
-
const isEncrypted = lowered.includes("application/octet-stream");
|
|
315
|
-
|
|
316
|
-
if (requireEncrypted && !isEncrypted) {
|
|
317
|
-
throw new Error(
|
|
318
|
-
"Encrypted request required: Content-Type must be application/octet-stream",
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (isEncrypted) {
|
|
323
|
-
if (body == null) {
|
|
324
|
-
throw new Error("Encrypted request body is empty");
|
|
325
|
-
}
|
|
326
|
-
if (body instanceof ArrayBuffer) {
|
|
327
|
-
return this.decryptPacket<T>(body);
|
|
328
|
-
}
|
|
329
|
-
if (body instanceof Uint8Array) {
|
|
330
|
-
const sliced = body.buffer.slice(
|
|
331
|
-
body.byteOffset,
|
|
332
|
-
body.byteOffset + body.byteLength,
|
|
333
|
-
);
|
|
334
|
-
return this.decryptPacket<T>(sliced);
|
|
335
|
-
}
|
|
336
|
-
throw new Error(
|
|
337
|
-
"Encrypted request body must be ArrayBuffer or Uint8Array",
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (body == null || body === "") return {} as T;
|
|
342
|
-
if (typeof body === "string") return JSON.parse(body) as T;
|
|
343
|
-
return body as T;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private async request<T>(
|
|
347
|
-
method: string,
|
|
348
|
-
path: string,
|
|
349
|
-
body?: unknown,
|
|
350
|
-
withAuth = true,
|
|
351
|
-
extraHeaders: Record<string, string> = {},
|
|
352
|
-
): Promise<T> {
|
|
353
|
-
const headers: Record<string, string> = {
|
|
354
|
-
"Content-Type": "application/json",
|
|
355
|
-
...extraHeaders,
|
|
356
|
-
};
|
|
357
|
-
if (withAuth && this.token) {
|
|
358
|
-
headers["Authorization"] = `Bearer ${this.token}`;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const res = await fetch(this.baseUrl + path, {
|
|
362
|
-
method,
|
|
363
|
-
headers,
|
|
364
|
-
...(body != null ? { body: JSON.stringify(body) } : {}),
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
const contentType = res.headers.get("Content-Type") ?? "";
|
|
368
|
-
|
|
369
|
-
// 패킷 암호화 응답: application/octet-stream → 복호화
|
|
370
|
-
if (contentType.includes("application/octet-stream")) {
|
|
371
|
-
const buffer = await res.arrayBuffer();
|
|
372
|
-
return this.decryptPacket<T>(buffer);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const data = await res.json();
|
|
376
|
-
if (!data.ok) {
|
|
377
|
-
const err = new Error(
|
|
378
|
-
data.message ?? `EntityServer error (HTTP ${res.status})`,
|
|
379
|
-
);
|
|
380
|
-
(err as { status?: number }).status = res.status;
|
|
381
|
-
throw err;
|
|
382
|
-
}
|
|
383
|
-
return data as T;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* XChaCha20-Poly1305 패킷 복호화
|
|
388
|
-
* 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
|
|
389
|
-
* 키: sha256(access_token)
|
|
390
|
-
*/
|
|
391
|
-
private decryptPacket<T>(buffer: ArrayBuffer): T {
|
|
392
|
-
const key = sha256(new TextEncoder().encode(this.token));
|
|
393
|
-
const data = new Uint8Array(buffer);
|
|
394
|
-
const nonce = data.slice(this.magicLen, this.magicLen + 24);
|
|
395
|
-
const ciphertext = data.slice(this.magicLen + 24);
|
|
396
|
-
const cipher = xchacha20_poly1305(key, nonce);
|
|
397
|
-
const plaintext = cipher.decrypt(ciphertext);
|
|
398
|
-
return JSON.parse(new TextDecoder().decode(plaintext)) as T;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function buildQuery(params: Record<string, unknown>): string {
|
|
403
|
-
return Object.entries(params)
|
|
404
|
-
.filter(([, v]) => v != null)
|
|
405
|
-
.map(
|
|
406
|
-
([k, v]) =>
|
|
407
|
-
`${encodeURIComponent(k === "orderBy" ? "order_by" : k)}=${encodeURIComponent(String(v))}`,
|
|
408
|
-
)
|
|
409
|
-
.join("&");
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/** 싱글턴 인스턴스 (앱 전체 공유) */
|
|
413
|
-
export const entityServer = new EntityServerClient();
|