connectbase-client 3.9.0 → 3.11.0
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/CHANGELOG.md +79 -0
- package/README.md +4 -1
- package/dist/connect-base.umd.js +4 -3
- package/dist/index.d.mts +259 -4
- package/dist/index.d.ts +259 -4
- package/dist/index.js +261 -10
- package/dist/index.mjs +259 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -35,7 +35,8 @@ __export(index_exports, {
|
|
|
35
35
|
SessionManager: () => SessionManager,
|
|
36
36
|
VideoProcessingError: () => VideoProcessingError,
|
|
37
37
|
default: () => index_default,
|
|
38
|
-
isWebTransportSupported: () => isWebTransportSupported
|
|
38
|
+
isWebTransportSupported: () => isWebTransportSupported,
|
|
39
|
+
toCreateRoomWire: () => toCreateRoomWire
|
|
39
40
|
});
|
|
40
41
|
module.exports = __toCommonJS(index_exports);
|
|
41
42
|
|
|
@@ -90,6 +91,40 @@ function createTimeoutController(options = {}) {
|
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
// src/core/recent-calls.ts
|
|
95
|
+
var DEFAULT_CAPACITY = 20;
|
|
96
|
+
var MAX_CAPACITY = 50;
|
|
97
|
+
var RecentCallsBuffer = class {
|
|
98
|
+
constructor(capacity = DEFAULT_CAPACITY) {
|
|
99
|
+
this.buf = [];
|
|
100
|
+
this.capacity = Math.min(Math.max(1, capacity), MAX_CAPACITY);
|
|
101
|
+
}
|
|
102
|
+
push(call) {
|
|
103
|
+
if (this.buf.length >= this.capacity) {
|
|
104
|
+
this.buf.shift();
|
|
105
|
+
}
|
|
106
|
+
this.buf.push(call);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 시간순(오래된 → 최신) 복사본 반환. 호출자가 mutate 해도 내부에 영향 없음.
|
|
110
|
+
*/
|
|
111
|
+
snapshot() {
|
|
112
|
+
return this.buf.slice();
|
|
113
|
+
}
|
|
114
|
+
clear() {
|
|
115
|
+
this.buf = [];
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function sanitizePathForBreadcrumb(rawUrl) {
|
|
119
|
+
try {
|
|
120
|
+
const u = rawUrl.startsWith("http") ? new URL(rawUrl) : new URL(rawUrl, "http://x");
|
|
121
|
+
return u.pathname || rawUrl;
|
|
122
|
+
} catch {
|
|
123
|
+
const idx = rawUrl.indexOf("?");
|
|
124
|
+
return idx >= 0 ? rawUrl.slice(0, idx) : rawUrl;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
93
128
|
// src/core/http.ts
|
|
94
129
|
var TOKEN_STORAGE_KEY = "cb_auth_tokens";
|
|
95
130
|
var HttpClient = class {
|
|
@@ -100,11 +135,24 @@ var HttpClient = class {
|
|
|
100
135
|
// 밀리초 단위로 서버에 재시도 요청이 쏟아지는 것을 차단.
|
|
101
136
|
this.refreshFailureCount = 0;
|
|
102
137
|
this.refreshLockedUntil = 0;
|
|
138
|
+
/**
|
|
139
|
+
* 최근 API 호출 breadcrumb (PII strip 후 저장). platform_issue 발행 시 자동 첨부 가능.
|
|
140
|
+
* `client.support.getRecentCalls()` 로 외부 노출.
|
|
141
|
+
*/
|
|
142
|
+
this.recentCalls = new RecentCallsBuffer();
|
|
103
143
|
this.config = { ...config };
|
|
104
144
|
this.storageKey = this.buildStorageKey();
|
|
105
145
|
this.warnIfUnsafePersistence();
|
|
106
146
|
this.restoreTokens();
|
|
107
147
|
}
|
|
148
|
+
/** 최근 호출 ring buffer 스냅샷 (시간순). */
|
|
149
|
+
getRecentCalls() {
|
|
150
|
+
return this.recentCalls.snapshot();
|
|
151
|
+
}
|
|
152
|
+
/** 최근 호출 buffer clear (테스트/프라이버시 처리). */
|
|
153
|
+
clearRecentCalls() {
|
|
154
|
+
this.recentCalls.clear();
|
|
155
|
+
}
|
|
108
156
|
warnIfUnsafePersistence() {
|
|
109
157
|
if (typeof window === "undefined") return;
|
|
110
158
|
if (this.config.persistence === "localStorage") {
|
|
@@ -261,6 +309,7 @@ var HttpClient = class {
|
|
|
261
309
|
const { signal, cleanup } = createTimeoutController({
|
|
262
310
|
timeout: this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
263
311
|
});
|
|
312
|
+
let failureKind = "transient";
|
|
264
313
|
try {
|
|
265
314
|
const headers = {
|
|
266
315
|
"Content-Type": "application/json"
|
|
@@ -279,7 +328,28 @@ var HttpClient = class {
|
|
|
279
328
|
signal
|
|
280
329
|
});
|
|
281
330
|
if (!response.ok) {
|
|
282
|
-
|
|
331
|
+
const status = response.status;
|
|
332
|
+
let oauthError;
|
|
333
|
+
try {
|
|
334
|
+
const errBody = await response.clone().json();
|
|
335
|
+
if (errBody && typeof errBody.error === "string") {
|
|
336
|
+
oauthError = errBody.error;
|
|
337
|
+
} else if (errBody && typeof errBody.code === "string") {
|
|
338
|
+
oauthError = errBody.code;
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
if (status >= 500) {
|
|
343
|
+
throw new Error(`Token refresh failed (${status})`);
|
|
344
|
+
}
|
|
345
|
+
if (status === 401 || status === 403 || status === 400 && (oauthError === "invalid_grant" || oauthError === "invalid_token")) {
|
|
346
|
+
failureKind = "permanent";
|
|
347
|
+
} else {
|
|
348
|
+
failureKind = "client_bug";
|
|
349
|
+
}
|
|
350
|
+
throw new Error(
|
|
351
|
+
`Token refresh failed (${status}${oauthError ? ` ${oauthError}` : ""})`
|
|
352
|
+
);
|
|
283
353
|
}
|
|
284
354
|
const data = await response.json();
|
|
285
355
|
if (!data || typeof data.access_token !== "string") {
|
|
@@ -299,17 +369,35 @@ var HttpClient = class {
|
|
|
299
369
|
this.refreshFailureCount = 0;
|
|
300
370
|
this.refreshLockedUntil = 0;
|
|
301
371
|
return data.access_token;
|
|
302
|
-
} catch {
|
|
372
|
+
} catch (e) {
|
|
373
|
+
const baseMsg = e instanceof Error ? e.message : "Token refresh failed";
|
|
303
374
|
this.refreshFailureCount++;
|
|
304
375
|
const backoffMs = Math.min(
|
|
305
376
|
500 * 2 ** Math.max(0, this.refreshFailureCount - 1),
|
|
306
377
|
3e4
|
|
307
378
|
);
|
|
308
379
|
this.refreshLockedUntil = Date.now() + backoffMs;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
380
|
+
if (failureKind === "permanent") {
|
|
381
|
+
this.clearTokens();
|
|
382
|
+
this.config.onTokenExpired?.();
|
|
383
|
+
const error2 = new AuthError(`${baseMsg}. Please login again.`);
|
|
384
|
+
this.emitError(error2);
|
|
385
|
+
this.config.onAuthError?.(error2);
|
|
386
|
+
throw error2;
|
|
387
|
+
}
|
|
388
|
+
if (failureKind === "client_bug") {
|
|
389
|
+
const error2 = new AuthError(
|
|
390
|
+
`${baseMsg}. Client request invalid; tokens preserved.`
|
|
391
|
+
);
|
|
392
|
+
this.emitError(error2);
|
|
393
|
+
this.config.onAuthError?.(error2);
|
|
394
|
+
throw error2;
|
|
395
|
+
}
|
|
396
|
+
const error = new AuthError(
|
|
397
|
+
`${baseMsg}. Transient failure; tokens preserved, will retry after backoff.`
|
|
398
|
+
);
|
|
312
399
|
this.emitError(error);
|
|
400
|
+
this.config.onTransientRefreshFailure?.(error);
|
|
313
401
|
this.config.onAuthError?.(error);
|
|
314
402
|
throw error;
|
|
315
403
|
} finally {
|
|
@@ -438,14 +526,24 @@ var HttpClient = class {
|
|
|
438
526
|
timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
439
527
|
signal: config?.signal
|
|
440
528
|
});
|
|
529
|
+
const startedAt = Date.now();
|
|
530
|
+
let status = 0;
|
|
441
531
|
try {
|
|
442
532
|
const response = await fetch(`${this.config.baseUrl}${url}`, {
|
|
443
533
|
...init,
|
|
444
534
|
credentials: "include",
|
|
445
535
|
signal
|
|
446
536
|
});
|
|
537
|
+
status = response.status;
|
|
447
538
|
return await this.handleResponse(response);
|
|
448
539
|
} finally {
|
|
540
|
+
this.recentCalls.push({
|
|
541
|
+
method: (init.method || "GET").toUpperCase(),
|
|
542
|
+
path: sanitizePathForBreadcrumb(url),
|
|
543
|
+
status,
|
|
544
|
+
duration_ms: Date.now() - startedAt,
|
|
545
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
546
|
+
});
|
|
449
547
|
cleanup();
|
|
450
548
|
}
|
|
451
549
|
}
|
|
@@ -6173,6 +6271,16 @@ var getDefaultGameServerUrl = () => {
|
|
|
6173
6271
|
}
|
|
6174
6272
|
return "wss://game.connectbase.world";
|
|
6175
6273
|
};
|
|
6274
|
+
function toCreateRoomWire(config) {
|
|
6275
|
+
const out = {};
|
|
6276
|
+
if (config.roomId) out.room_id = config.roomId;
|
|
6277
|
+
if (config.categoryId) out.category_id = config.categoryId;
|
|
6278
|
+
if (typeof config.tickRate === "number") out.tick_rate = config.tickRate;
|
|
6279
|
+
if (typeof config.maxPlayers === "number") out.max_players = config.maxPlayers;
|
|
6280
|
+
if (config.scriptName) out.script_name = config.scriptName;
|
|
6281
|
+
if (config.metadata) out.metadata = config.metadata;
|
|
6282
|
+
return out;
|
|
6283
|
+
}
|
|
6176
6284
|
var GameRoom = class {
|
|
6177
6285
|
constructor(config) {
|
|
6178
6286
|
this.ws = null;
|
|
@@ -6291,7 +6399,7 @@ var GameRoom = class {
|
|
|
6291
6399
|
}
|
|
6292
6400
|
return false;
|
|
6293
6401
|
};
|
|
6294
|
-
this.sendWithHandler("create_room", config, handler, 15e3, reject);
|
|
6402
|
+
this.sendWithHandler("create_room", toCreateRoomWire(config), handler, 15e3, reject);
|
|
6295
6403
|
});
|
|
6296
6404
|
}
|
|
6297
6405
|
/**
|
|
@@ -9007,7 +9115,148 @@ var SupportAPI = class {
|
|
|
9007
9115
|
if (req.recaptchaToken) body.recaptcha_token = req.recaptchaToken;
|
|
9008
9116
|
return this.http.post("/v1/public/reports", body);
|
|
9009
9117
|
}
|
|
9118
|
+
/**
|
|
9119
|
+
* ConnectBase 플랫폼 자체 버그/요청/문의를 발행한다.
|
|
9120
|
+
*
|
|
9121
|
+
* `reportIssue` 와 다른 점: target 이 앱 운영자가 아닌 **ConnectBase 운영팀**.
|
|
9122
|
+
* SDK 가 자체 버그를 던지거나, 결제/문서/플랫폼 동작이 이상할 때 사용.
|
|
9123
|
+
*
|
|
9124
|
+
* 자동 첨부 (opt-out 가능):
|
|
9125
|
+
* - SDK 버전 + 플랫폼 (web/node)
|
|
9126
|
+
* - 마지막 N(20)개 API 호출 breadcrumb (PII strip 후)
|
|
9127
|
+
* - error 객체의 stack trace (sanitize)
|
|
9128
|
+
*
|
|
9129
|
+
* @example SDK 가 throw 한 에러를 자동 첨부해서 발행
|
|
9130
|
+
* ```typescript
|
|
9131
|
+
* try {
|
|
9132
|
+
* await cb.functions.invoke('foo', {})
|
|
9133
|
+
* } catch (err) {
|
|
9134
|
+
* await cb.support.reportPlatformBug({
|
|
9135
|
+
* title: 'functions.invoke 가 504 만 반환',
|
|
9136
|
+
* body: '같은 인자로 5분 째 504. 콘솔에선 정상.',
|
|
9137
|
+
* category: 'sdk',
|
|
9138
|
+
* severity: 'high',
|
|
9139
|
+
* error: err as Error,
|
|
9140
|
+
* })
|
|
9141
|
+
* }
|
|
9142
|
+
* ```
|
|
9143
|
+
*/
|
|
9144
|
+
async reportPlatformBug(req) {
|
|
9145
|
+
const attachContext = req.attachAutomaticContext !== false;
|
|
9146
|
+
const body = {
|
|
9147
|
+
title: req.title,
|
|
9148
|
+
body: req.body,
|
|
9149
|
+
category: req.category ?? "other",
|
|
9150
|
+
severity: req.severity ?? "medium",
|
|
9151
|
+
metadata: req.metadata
|
|
9152
|
+
};
|
|
9153
|
+
if (req.reporterEmail) body.reporter_email = req.reporterEmail;
|
|
9154
|
+
if (req.recaptchaToken) body.recaptcha_token = req.recaptchaToken;
|
|
9155
|
+
if (attachContext) {
|
|
9156
|
+
body.sdk_version = req.sdkVersion ?? detectSdkVersion();
|
|
9157
|
+
body.sdk_platform = req.sdkPlatform ?? detectSdkPlatform();
|
|
9158
|
+
body.environment = req.environment ?? "unknown";
|
|
9159
|
+
if (req.error) {
|
|
9160
|
+
body.stack_trace = sanitizeStackTrace(req.error.stack ?? String(req.error));
|
|
9161
|
+
} else if (req.stackTrace) {
|
|
9162
|
+
body.stack_trace = sanitizeStackTrace(req.stackTrace);
|
|
9163
|
+
}
|
|
9164
|
+
const calls = req.recentApiCalls ?? this.http.getRecentCalls();
|
|
9165
|
+
if (calls.length > 0) {
|
|
9166
|
+
body.recent_api_calls = calls;
|
|
9167
|
+
}
|
|
9168
|
+
}
|
|
9169
|
+
return this.http.post(
|
|
9170
|
+
"/v1/public/platform-issues",
|
|
9171
|
+
body
|
|
9172
|
+
);
|
|
9173
|
+
}
|
|
9174
|
+
/** 디버깅용: 마지막 N개 API 호출 breadcrumb. PII 제거 후 저장돼있다. */
|
|
9175
|
+
getRecentApiCalls() {
|
|
9176
|
+
return this.http.getRecentCalls();
|
|
9177
|
+
}
|
|
9178
|
+
/** breadcrumb buffer 비우기 (사용자 명시적 요청 / 프라이버시). */
|
|
9179
|
+
clearRecentApiCalls() {
|
|
9180
|
+
this.http.clearRecentCalls();
|
|
9181
|
+
}
|
|
9182
|
+
/**
|
|
9183
|
+
* Platform issue 의 reply thread 조회.
|
|
9184
|
+
*
|
|
9185
|
+
* 본인이 발행한 issue 만 조회 가능 (server-side ownership guard). admin 의 internal 메모는 응답 제외.
|
|
9186
|
+
*
|
|
9187
|
+
* @param issueId - `reportPlatformBug` 가 반환한 id
|
|
9188
|
+
* @throws ApiError 404 — 본인 issue 가 아니거나 존재하지 않음
|
|
9189
|
+
*/
|
|
9190
|
+
async listPlatformIssueReplies(issueId) {
|
|
9191
|
+
const res = await this.http.get(
|
|
9192
|
+
`/v1/public/platform-issues/${issueId}/comments`
|
|
9193
|
+
);
|
|
9194
|
+
return res.comments;
|
|
9195
|
+
}
|
|
9196
|
+
/**
|
|
9197
|
+
* Platform issue 에 follow-up reply 작성.
|
|
9198
|
+
*
|
|
9199
|
+
* 단말 상태(resolved/wontfix/duplicate) issue 는 reply 거부 — 새 issue 발행 권장.
|
|
9200
|
+
*/
|
|
9201
|
+
async replyToPlatformIssue(issueId, body) {
|
|
9202
|
+
return this.http.post(
|
|
9203
|
+
`/v1/public/platform-issues/${issueId}/comments`,
|
|
9204
|
+
{ body }
|
|
9205
|
+
);
|
|
9206
|
+
}
|
|
9207
|
+
/**
|
|
9208
|
+
* 본인이 발행한 platform issue 의 처리 진행 상황을 단건 조회.
|
|
9209
|
+
*
|
|
9210
|
+
* AI 가 "내가 발행한 이슈 처리됐어?" 를 폴링하는 표준 경로. status / resolution_note /
|
|
9211
|
+
* triage_summary / external_links 로 ConnectBase 운영팀의 처리 상태를 확인.
|
|
9212
|
+
*
|
|
9213
|
+
* @throws ApiError 404 — 본인 issue 가 아니거나 존재하지 않음
|
|
9214
|
+
*/
|
|
9215
|
+
async getPlatformIssue(issueId) {
|
|
9216
|
+
return this.http.get(
|
|
9217
|
+
`/v1/public/platform-issues/${issueId}`
|
|
9218
|
+
);
|
|
9219
|
+
}
|
|
9220
|
+
/**
|
|
9221
|
+
* 본인 app 으로 발행한 platform issue 목록 (cursor 페이지네이션).
|
|
9222
|
+
*
|
|
9223
|
+
* status/severity/category 필터 + `since_updated_at` 으로 미해결만 폴링하는 사용 패턴 권장:
|
|
9224
|
+
*
|
|
9225
|
+
* ```typescript
|
|
9226
|
+
* const { issues } = await cb.support.listMyPlatformIssues({ status: ['open', 'triaged', 'in_progress'] })
|
|
9227
|
+
* ```
|
|
9228
|
+
*/
|
|
9229
|
+
async listMyPlatformIssues(opts = {}) {
|
|
9230
|
+
const params = new URLSearchParams();
|
|
9231
|
+
for (const s of opts.status ?? []) params.append("status", s);
|
|
9232
|
+
for (const s of opts.severity ?? []) params.append("severity", s);
|
|
9233
|
+
for (const s of opts.category ?? []) params.append("category", s);
|
|
9234
|
+
if (opts.sinceUpdatedAt) params.set("since_updated_at", opts.sinceUpdatedAt);
|
|
9235
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
9236
|
+
if (typeof opts.limit === "number") params.set("limit", String(opts.limit));
|
|
9237
|
+
const query = params.toString();
|
|
9238
|
+
return this.http.get(
|
|
9239
|
+
`/v1/public/platform-issues${query ? "?" + query : ""}`
|
|
9240
|
+
);
|
|
9241
|
+
}
|
|
9010
9242
|
};
|
|
9243
|
+
function detectSdkVersion() {
|
|
9244
|
+
if (typeof __SDK_VERSION__ !== "undefined" && __SDK_VERSION__) {
|
|
9245
|
+
return __SDK_VERSION__;
|
|
9246
|
+
}
|
|
9247
|
+
return "unknown";
|
|
9248
|
+
}
|
|
9249
|
+
function detectSdkPlatform() {
|
|
9250
|
+
return typeof window !== "undefined" && typeof document !== "undefined" ? "web" : "node";
|
|
9251
|
+
}
|
|
9252
|
+
function sanitizeStackTrace(raw) {
|
|
9253
|
+
let s = raw;
|
|
9254
|
+
s = s.replace(/(\(|\s|^)(?:file:\/\/)?\/[^\s)]+\/([^\s/)]+)(:\d+:\d+)?/g, "$1$2$3");
|
|
9255
|
+
s = s.replace(/(https?:\/\/[^\s)?]+)\?[^\s)]*/g, "$1");
|
|
9256
|
+
const max = 32 * 1024;
|
|
9257
|
+
if (s.length > max) s = s.slice(0, max) + "\n\u2026[truncated]";
|
|
9258
|
+
return s;
|
|
9259
|
+
}
|
|
9011
9260
|
|
|
9012
9261
|
// src/types/knowledge.ts
|
|
9013
9262
|
var AUTH_MEMBER_ID_TOKEN = "$auth.member_id";
|
|
@@ -9391,7 +9640,7 @@ var GameRoomTransport = class {
|
|
|
9391
9640
|
reject(new Error(msg.data.message));
|
|
9392
9641
|
}
|
|
9393
9642
|
};
|
|
9394
|
-
this.sendWithHandler("create_room", config, handler);
|
|
9643
|
+
this.sendWithHandler("create_room", toCreateRoomWire(config), handler);
|
|
9395
9644
|
});
|
|
9396
9645
|
}
|
|
9397
9646
|
/**
|
|
@@ -9673,7 +9922,8 @@ var ConnectBase = class {
|
|
|
9673
9922
|
onError: config.onError,
|
|
9674
9923
|
onTokenRefresh: config.onTokenRefresh,
|
|
9675
9924
|
onAuthError: config.onAuthError,
|
|
9676
|
-
onTokenExpired: config.onTokenExpired
|
|
9925
|
+
onTokenExpired: config.onTokenExpired,
|
|
9926
|
+
onTransientRefreshFailure: config.onTransientRefreshFailure
|
|
9677
9927
|
};
|
|
9678
9928
|
this.http = new HttpClient(httpConfig);
|
|
9679
9929
|
this.auth = new AuthAPI(this.http);
|
|
@@ -9751,5 +10001,6 @@ var index_default = ConnectBase;
|
|
|
9751
10001
|
NativeAPI,
|
|
9752
10002
|
SessionManager,
|
|
9753
10003
|
VideoProcessingError,
|
|
9754
|
-
isWebTransportSupported
|
|
10004
|
+
isWebTransportSupported,
|
|
10005
|
+
toCreateRoomWire
|
|
9755
10006
|
});
|