connectbase-client 3.9.0 → 3.10.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 +35 -0
- package/dist/connect-base.umd.js +4 -3
- package/dist/index.d.mts +239 -3
- package/dist/index.d.ts +239 -3
- package/dist/index.js +245 -6
- package/dist/index.mjs +245 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -90,6 +90,40 @@ function createTimeoutController(options = {}) {
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// src/core/recent-calls.ts
|
|
94
|
+
var DEFAULT_CAPACITY = 20;
|
|
95
|
+
var MAX_CAPACITY = 50;
|
|
96
|
+
var RecentCallsBuffer = class {
|
|
97
|
+
constructor(capacity = DEFAULT_CAPACITY) {
|
|
98
|
+
this.buf = [];
|
|
99
|
+
this.capacity = Math.min(Math.max(1, capacity), MAX_CAPACITY);
|
|
100
|
+
}
|
|
101
|
+
push(call) {
|
|
102
|
+
if (this.buf.length >= this.capacity) {
|
|
103
|
+
this.buf.shift();
|
|
104
|
+
}
|
|
105
|
+
this.buf.push(call);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 시간순(오래된 → 최신) 복사본 반환. 호출자가 mutate 해도 내부에 영향 없음.
|
|
109
|
+
*/
|
|
110
|
+
snapshot() {
|
|
111
|
+
return this.buf.slice();
|
|
112
|
+
}
|
|
113
|
+
clear() {
|
|
114
|
+
this.buf = [];
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
function sanitizePathForBreadcrumb(rawUrl) {
|
|
118
|
+
try {
|
|
119
|
+
const u = rawUrl.startsWith("http") ? new URL(rawUrl) : new URL(rawUrl, "http://x");
|
|
120
|
+
return u.pathname || rawUrl;
|
|
121
|
+
} catch {
|
|
122
|
+
const idx = rawUrl.indexOf("?");
|
|
123
|
+
return idx >= 0 ? rawUrl.slice(0, idx) : rawUrl;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
93
127
|
// src/core/http.ts
|
|
94
128
|
var TOKEN_STORAGE_KEY = "cb_auth_tokens";
|
|
95
129
|
var HttpClient = class {
|
|
@@ -100,11 +134,24 @@ var HttpClient = class {
|
|
|
100
134
|
// 밀리초 단위로 서버에 재시도 요청이 쏟아지는 것을 차단.
|
|
101
135
|
this.refreshFailureCount = 0;
|
|
102
136
|
this.refreshLockedUntil = 0;
|
|
137
|
+
/**
|
|
138
|
+
* 최근 API 호출 breadcrumb (PII strip 후 저장). platform_issue 발행 시 자동 첨부 가능.
|
|
139
|
+
* `client.support.getRecentCalls()` 로 외부 노출.
|
|
140
|
+
*/
|
|
141
|
+
this.recentCalls = new RecentCallsBuffer();
|
|
103
142
|
this.config = { ...config };
|
|
104
143
|
this.storageKey = this.buildStorageKey();
|
|
105
144
|
this.warnIfUnsafePersistence();
|
|
106
145
|
this.restoreTokens();
|
|
107
146
|
}
|
|
147
|
+
/** 최근 호출 ring buffer 스냅샷 (시간순). */
|
|
148
|
+
getRecentCalls() {
|
|
149
|
+
return this.recentCalls.snapshot();
|
|
150
|
+
}
|
|
151
|
+
/** 최근 호출 buffer clear (테스트/프라이버시 처리). */
|
|
152
|
+
clearRecentCalls() {
|
|
153
|
+
this.recentCalls.clear();
|
|
154
|
+
}
|
|
108
155
|
warnIfUnsafePersistence() {
|
|
109
156
|
if (typeof window === "undefined") return;
|
|
110
157
|
if (this.config.persistence === "localStorage") {
|
|
@@ -261,6 +308,7 @@ var HttpClient = class {
|
|
|
261
308
|
const { signal, cleanup } = createTimeoutController({
|
|
262
309
|
timeout: this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
263
310
|
});
|
|
311
|
+
let failureKind = "transient";
|
|
264
312
|
try {
|
|
265
313
|
const headers = {
|
|
266
314
|
"Content-Type": "application/json"
|
|
@@ -279,7 +327,28 @@ var HttpClient = class {
|
|
|
279
327
|
signal
|
|
280
328
|
});
|
|
281
329
|
if (!response.ok) {
|
|
282
|
-
|
|
330
|
+
const status = response.status;
|
|
331
|
+
let oauthError;
|
|
332
|
+
try {
|
|
333
|
+
const errBody = await response.clone().json();
|
|
334
|
+
if (errBody && typeof errBody.error === "string") {
|
|
335
|
+
oauthError = errBody.error;
|
|
336
|
+
} else if (errBody && typeof errBody.code === "string") {
|
|
337
|
+
oauthError = errBody.code;
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
if (status >= 500) {
|
|
342
|
+
throw new Error(`Token refresh failed (${status})`);
|
|
343
|
+
}
|
|
344
|
+
if (status === 401 || status === 403 || status === 400 && (oauthError === "invalid_grant" || oauthError === "invalid_token")) {
|
|
345
|
+
failureKind = "permanent";
|
|
346
|
+
} else {
|
|
347
|
+
failureKind = "client_bug";
|
|
348
|
+
}
|
|
349
|
+
throw new Error(
|
|
350
|
+
`Token refresh failed (${status}${oauthError ? ` ${oauthError}` : ""})`
|
|
351
|
+
);
|
|
283
352
|
}
|
|
284
353
|
const data = await response.json();
|
|
285
354
|
if (!data || typeof data.access_token !== "string") {
|
|
@@ -299,17 +368,35 @@ var HttpClient = class {
|
|
|
299
368
|
this.refreshFailureCount = 0;
|
|
300
369
|
this.refreshLockedUntil = 0;
|
|
301
370
|
return data.access_token;
|
|
302
|
-
} catch {
|
|
371
|
+
} catch (e) {
|
|
372
|
+
const baseMsg = e instanceof Error ? e.message : "Token refresh failed";
|
|
303
373
|
this.refreshFailureCount++;
|
|
304
374
|
const backoffMs = Math.min(
|
|
305
375
|
500 * 2 ** Math.max(0, this.refreshFailureCount - 1),
|
|
306
376
|
3e4
|
|
307
377
|
);
|
|
308
378
|
this.refreshLockedUntil = Date.now() + backoffMs;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
379
|
+
if (failureKind === "permanent") {
|
|
380
|
+
this.clearTokens();
|
|
381
|
+
this.config.onTokenExpired?.();
|
|
382
|
+
const error2 = new AuthError(`${baseMsg}. Please login again.`);
|
|
383
|
+
this.emitError(error2);
|
|
384
|
+
this.config.onAuthError?.(error2);
|
|
385
|
+
throw error2;
|
|
386
|
+
}
|
|
387
|
+
if (failureKind === "client_bug") {
|
|
388
|
+
const error2 = new AuthError(
|
|
389
|
+
`${baseMsg}. Client request invalid; tokens preserved.`
|
|
390
|
+
);
|
|
391
|
+
this.emitError(error2);
|
|
392
|
+
this.config.onAuthError?.(error2);
|
|
393
|
+
throw error2;
|
|
394
|
+
}
|
|
395
|
+
const error = new AuthError(
|
|
396
|
+
`${baseMsg}. Transient failure; tokens preserved, will retry after backoff.`
|
|
397
|
+
);
|
|
312
398
|
this.emitError(error);
|
|
399
|
+
this.config.onTransientRefreshFailure?.(error);
|
|
313
400
|
this.config.onAuthError?.(error);
|
|
314
401
|
throw error;
|
|
315
402
|
} finally {
|
|
@@ -438,14 +525,24 @@ var HttpClient = class {
|
|
|
438
525
|
timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
439
526
|
signal: config?.signal
|
|
440
527
|
});
|
|
528
|
+
const startedAt = Date.now();
|
|
529
|
+
let status = 0;
|
|
441
530
|
try {
|
|
442
531
|
const response = await fetch(`${this.config.baseUrl}${url}`, {
|
|
443
532
|
...init,
|
|
444
533
|
credentials: "include",
|
|
445
534
|
signal
|
|
446
535
|
});
|
|
536
|
+
status = response.status;
|
|
447
537
|
return await this.handleResponse(response);
|
|
448
538
|
} finally {
|
|
539
|
+
this.recentCalls.push({
|
|
540
|
+
method: (init.method || "GET").toUpperCase(),
|
|
541
|
+
path: sanitizePathForBreadcrumb(url),
|
|
542
|
+
status,
|
|
543
|
+
duration_ms: Date.now() - startedAt,
|
|
544
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
545
|
+
});
|
|
449
546
|
cleanup();
|
|
450
547
|
}
|
|
451
548
|
}
|
|
@@ -9007,7 +9104,148 @@ var SupportAPI = class {
|
|
|
9007
9104
|
if (req.recaptchaToken) body.recaptcha_token = req.recaptchaToken;
|
|
9008
9105
|
return this.http.post("/v1/public/reports", body);
|
|
9009
9106
|
}
|
|
9107
|
+
/**
|
|
9108
|
+
* ConnectBase 플랫폼 자체 버그/요청/문의를 발행한다.
|
|
9109
|
+
*
|
|
9110
|
+
* `reportIssue` 와 다른 점: target 이 앱 운영자가 아닌 **ConnectBase 운영팀**.
|
|
9111
|
+
* SDK 가 자체 버그를 던지거나, 결제/문서/플랫폼 동작이 이상할 때 사용.
|
|
9112
|
+
*
|
|
9113
|
+
* 자동 첨부 (opt-out 가능):
|
|
9114
|
+
* - SDK 버전 + 플랫폼 (web/node)
|
|
9115
|
+
* - 마지막 N(20)개 API 호출 breadcrumb (PII strip 후)
|
|
9116
|
+
* - error 객체의 stack trace (sanitize)
|
|
9117
|
+
*
|
|
9118
|
+
* @example SDK 가 throw 한 에러를 자동 첨부해서 발행
|
|
9119
|
+
* ```typescript
|
|
9120
|
+
* try {
|
|
9121
|
+
* await cb.functions.invoke('foo', {})
|
|
9122
|
+
* } catch (err) {
|
|
9123
|
+
* await cb.support.reportPlatformBug({
|
|
9124
|
+
* title: 'functions.invoke 가 504 만 반환',
|
|
9125
|
+
* body: '같은 인자로 5분 째 504. 콘솔에선 정상.',
|
|
9126
|
+
* category: 'sdk',
|
|
9127
|
+
* severity: 'high',
|
|
9128
|
+
* error: err as Error,
|
|
9129
|
+
* })
|
|
9130
|
+
* }
|
|
9131
|
+
* ```
|
|
9132
|
+
*/
|
|
9133
|
+
async reportPlatformBug(req) {
|
|
9134
|
+
const attachContext = req.attachAutomaticContext !== false;
|
|
9135
|
+
const body = {
|
|
9136
|
+
title: req.title,
|
|
9137
|
+
body: req.body,
|
|
9138
|
+
category: req.category ?? "other",
|
|
9139
|
+
severity: req.severity ?? "medium",
|
|
9140
|
+
metadata: req.metadata
|
|
9141
|
+
};
|
|
9142
|
+
if (req.reporterEmail) body.reporter_email = req.reporterEmail;
|
|
9143
|
+
if (req.recaptchaToken) body.recaptcha_token = req.recaptchaToken;
|
|
9144
|
+
if (attachContext) {
|
|
9145
|
+
body.sdk_version = req.sdkVersion ?? detectSdkVersion();
|
|
9146
|
+
body.sdk_platform = req.sdkPlatform ?? detectSdkPlatform();
|
|
9147
|
+
body.environment = req.environment ?? "unknown";
|
|
9148
|
+
if (req.error) {
|
|
9149
|
+
body.stack_trace = sanitizeStackTrace(req.error.stack ?? String(req.error));
|
|
9150
|
+
} else if (req.stackTrace) {
|
|
9151
|
+
body.stack_trace = sanitizeStackTrace(req.stackTrace);
|
|
9152
|
+
}
|
|
9153
|
+
const calls = req.recentApiCalls ?? this.http.getRecentCalls();
|
|
9154
|
+
if (calls.length > 0) {
|
|
9155
|
+
body.recent_api_calls = calls;
|
|
9156
|
+
}
|
|
9157
|
+
}
|
|
9158
|
+
return this.http.post(
|
|
9159
|
+
"/v1/public/platform-issues",
|
|
9160
|
+
body
|
|
9161
|
+
);
|
|
9162
|
+
}
|
|
9163
|
+
/** 디버깅용: 마지막 N개 API 호출 breadcrumb. PII 제거 후 저장돼있다. */
|
|
9164
|
+
getRecentApiCalls() {
|
|
9165
|
+
return this.http.getRecentCalls();
|
|
9166
|
+
}
|
|
9167
|
+
/** breadcrumb buffer 비우기 (사용자 명시적 요청 / 프라이버시). */
|
|
9168
|
+
clearRecentApiCalls() {
|
|
9169
|
+
this.http.clearRecentCalls();
|
|
9170
|
+
}
|
|
9171
|
+
/**
|
|
9172
|
+
* Platform issue 의 reply thread 조회.
|
|
9173
|
+
*
|
|
9174
|
+
* 본인이 발행한 issue 만 조회 가능 (server-side ownership guard). admin 의 internal 메모는 응답 제외.
|
|
9175
|
+
*
|
|
9176
|
+
* @param issueId - `reportPlatformBug` 가 반환한 id
|
|
9177
|
+
* @throws ApiError 404 — 본인 issue 가 아니거나 존재하지 않음
|
|
9178
|
+
*/
|
|
9179
|
+
async listPlatformIssueReplies(issueId) {
|
|
9180
|
+
const res = await this.http.get(
|
|
9181
|
+
`/v1/public/platform-issues/${issueId}/comments`
|
|
9182
|
+
);
|
|
9183
|
+
return res.comments;
|
|
9184
|
+
}
|
|
9185
|
+
/**
|
|
9186
|
+
* Platform issue 에 follow-up reply 작성.
|
|
9187
|
+
*
|
|
9188
|
+
* 단말 상태(resolved/wontfix/duplicate) issue 는 reply 거부 — 새 issue 발행 권장.
|
|
9189
|
+
*/
|
|
9190
|
+
async replyToPlatformIssue(issueId, body) {
|
|
9191
|
+
return this.http.post(
|
|
9192
|
+
`/v1/public/platform-issues/${issueId}/comments`,
|
|
9193
|
+
{ body }
|
|
9194
|
+
);
|
|
9195
|
+
}
|
|
9196
|
+
/**
|
|
9197
|
+
* 본인이 발행한 platform issue 의 처리 진행 상황을 단건 조회.
|
|
9198
|
+
*
|
|
9199
|
+
* AI 가 "내가 발행한 이슈 처리됐어?" 를 폴링하는 표준 경로. status / resolution_note /
|
|
9200
|
+
* triage_summary / external_links 로 ConnectBase 운영팀의 처리 상태를 확인.
|
|
9201
|
+
*
|
|
9202
|
+
* @throws ApiError 404 — 본인 issue 가 아니거나 존재하지 않음
|
|
9203
|
+
*/
|
|
9204
|
+
async getPlatformIssue(issueId) {
|
|
9205
|
+
return this.http.get(
|
|
9206
|
+
`/v1/public/platform-issues/${issueId}`
|
|
9207
|
+
);
|
|
9208
|
+
}
|
|
9209
|
+
/**
|
|
9210
|
+
* 본인 app 으로 발행한 platform issue 목록 (cursor 페이지네이션).
|
|
9211
|
+
*
|
|
9212
|
+
* status/severity/category 필터 + `since_updated_at` 으로 미해결만 폴링하는 사용 패턴 권장:
|
|
9213
|
+
*
|
|
9214
|
+
* ```typescript
|
|
9215
|
+
* const { issues } = await cb.support.listMyPlatformIssues({ status: ['open', 'triaged', 'in_progress'] })
|
|
9216
|
+
* ```
|
|
9217
|
+
*/
|
|
9218
|
+
async listMyPlatformIssues(opts = {}) {
|
|
9219
|
+
const params = new URLSearchParams();
|
|
9220
|
+
for (const s of opts.status ?? []) params.append("status", s);
|
|
9221
|
+
for (const s of opts.severity ?? []) params.append("severity", s);
|
|
9222
|
+
for (const s of opts.category ?? []) params.append("category", s);
|
|
9223
|
+
if (opts.sinceUpdatedAt) params.set("since_updated_at", opts.sinceUpdatedAt);
|
|
9224
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
9225
|
+
if (typeof opts.limit === "number") params.set("limit", String(opts.limit));
|
|
9226
|
+
const query = params.toString();
|
|
9227
|
+
return this.http.get(
|
|
9228
|
+
`/v1/public/platform-issues${query ? "?" + query : ""}`
|
|
9229
|
+
);
|
|
9230
|
+
}
|
|
9010
9231
|
};
|
|
9232
|
+
function detectSdkVersion() {
|
|
9233
|
+
if (typeof __SDK_VERSION__ !== "undefined" && __SDK_VERSION__) {
|
|
9234
|
+
return __SDK_VERSION__;
|
|
9235
|
+
}
|
|
9236
|
+
return "unknown";
|
|
9237
|
+
}
|
|
9238
|
+
function detectSdkPlatform() {
|
|
9239
|
+
return typeof window !== "undefined" && typeof document !== "undefined" ? "web" : "node";
|
|
9240
|
+
}
|
|
9241
|
+
function sanitizeStackTrace(raw) {
|
|
9242
|
+
let s = raw;
|
|
9243
|
+
s = s.replace(/(\(|\s|^)(?:file:\/\/)?\/[^\s)]+\/([^\s/)]+)(:\d+:\d+)?/g, "$1$2$3");
|
|
9244
|
+
s = s.replace(/(https?:\/\/[^\s)?]+)\?[^\s)]*/g, "$1");
|
|
9245
|
+
const max = 32 * 1024;
|
|
9246
|
+
if (s.length > max) s = s.slice(0, max) + "\n\u2026[truncated]";
|
|
9247
|
+
return s;
|
|
9248
|
+
}
|
|
9011
9249
|
|
|
9012
9250
|
// src/types/knowledge.ts
|
|
9013
9251
|
var AUTH_MEMBER_ID_TOKEN = "$auth.member_id";
|
|
@@ -9673,7 +9911,8 @@ var ConnectBase = class {
|
|
|
9673
9911
|
onError: config.onError,
|
|
9674
9912
|
onTokenRefresh: config.onTokenRefresh,
|
|
9675
9913
|
onAuthError: config.onAuthError,
|
|
9676
|
-
onTokenExpired: config.onTokenExpired
|
|
9914
|
+
onTokenExpired: config.onTokenExpired,
|
|
9915
|
+
onTransientRefreshFailure: config.onTransientRefreshFailure
|
|
9677
9916
|
};
|
|
9678
9917
|
this.http = new HttpClient(httpConfig);
|
|
9679
9918
|
this.auth = new AuthAPI(this.http);
|
package/dist/index.mjs
CHANGED
|
@@ -49,6 +49,40 @@ function createTimeoutController(options = {}) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// src/core/recent-calls.ts
|
|
53
|
+
var DEFAULT_CAPACITY = 20;
|
|
54
|
+
var MAX_CAPACITY = 50;
|
|
55
|
+
var RecentCallsBuffer = class {
|
|
56
|
+
constructor(capacity = DEFAULT_CAPACITY) {
|
|
57
|
+
this.buf = [];
|
|
58
|
+
this.capacity = Math.min(Math.max(1, capacity), MAX_CAPACITY);
|
|
59
|
+
}
|
|
60
|
+
push(call) {
|
|
61
|
+
if (this.buf.length >= this.capacity) {
|
|
62
|
+
this.buf.shift();
|
|
63
|
+
}
|
|
64
|
+
this.buf.push(call);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 시간순(오래된 → 최신) 복사본 반환. 호출자가 mutate 해도 내부에 영향 없음.
|
|
68
|
+
*/
|
|
69
|
+
snapshot() {
|
|
70
|
+
return this.buf.slice();
|
|
71
|
+
}
|
|
72
|
+
clear() {
|
|
73
|
+
this.buf = [];
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
function sanitizePathForBreadcrumb(rawUrl) {
|
|
77
|
+
try {
|
|
78
|
+
const u = rawUrl.startsWith("http") ? new URL(rawUrl) : new URL(rawUrl, "http://x");
|
|
79
|
+
return u.pathname || rawUrl;
|
|
80
|
+
} catch {
|
|
81
|
+
const idx = rawUrl.indexOf("?");
|
|
82
|
+
return idx >= 0 ? rawUrl.slice(0, idx) : rawUrl;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
52
86
|
// src/core/http.ts
|
|
53
87
|
var TOKEN_STORAGE_KEY = "cb_auth_tokens";
|
|
54
88
|
var HttpClient = class {
|
|
@@ -59,11 +93,24 @@ var HttpClient = class {
|
|
|
59
93
|
// 밀리초 단위로 서버에 재시도 요청이 쏟아지는 것을 차단.
|
|
60
94
|
this.refreshFailureCount = 0;
|
|
61
95
|
this.refreshLockedUntil = 0;
|
|
96
|
+
/**
|
|
97
|
+
* 최근 API 호출 breadcrumb (PII strip 후 저장). platform_issue 발행 시 자동 첨부 가능.
|
|
98
|
+
* `client.support.getRecentCalls()` 로 외부 노출.
|
|
99
|
+
*/
|
|
100
|
+
this.recentCalls = new RecentCallsBuffer();
|
|
62
101
|
this.config = { ...config };
|
|
63
102
|
this.storageKey = this.buildStorageKey();
|
|
64
103
|
this.warnIfUnsafePersistence();
|
|
65
104
|
this.restoreTokens();
|
|
66
105
|
}
|
|
106
|
+
/** 최근 호출 ring buffer 스냅샷 (시간순). */
|
|
107
|
+
getRecentCalls() {
|
|
108
|
+
return this.recentCalls.snapshot();
|
|
109
|
+
}
|
|
110
|
+
/** 최근 호출 buffer clear (테스트/프라이버시 처리). */
|
|
111
|
+
clearRecentCalls() {
|
|
112
|
+
this.recentCalls.clear();
|
|
113
|
+
}
|
|
67
114
|
warnIfUnsafePersistence() {
|
|
68
115
|
if (typeof window === "undefined") return;
|
|
69
116
|
if (this.config.persistence === "localStorage") {
|
|
@@ -220,6 +267,7 @@ var HttpClient = class {
|
|
|
220
267
|
const { signal, cleanup } = createTimeoutController({
|
|
221
268
|
timeout: this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
222
269
|
});
|
|
270
|
+
let failureKind = "transient";
|
|
223
271
|
try {
|
|
224
272
|
const headers = {
|
|
225
273
|
"Content-Type": "application/json"
|
|
@@ -238,7 +286,28 @@ var HttpClient = class {
|
|
|
238
286
|
signal
|
|
239
287
|
});
|
|
240
288
|
if (!response.ok) {
|
|
241
|
-
|
|
289
|
+
const status = response.status;
|
|
290
|
+
let oauthError;
|
|
291
|
+
try {
|
|
292
|
+
const errBody = await response.clone().json();
|
|
293
|
+
if (errBody && typeof errBody.error === "string") {
|
|
294
|
+
oauthError = errBody.error;
|
|
295
|
+
} else if (errBody && typeof errBody.code === "string") {
|
|
296
|
+
oauthError = errBody.code;
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
if (status >= 500) {
|
|
301
|
+
throw new Error(`Token refresh failed (${status})`);
|
|
302
|
+
}
|
|
303
|
+
if (status === 401 || status === 403 || status === 400 && (oauthError === "invalid_grant" || oauthError === "invalid_token")) {
|
|
304
|
+
failureKind = "permanent";
|
|
305
|
+
} else {
|
|
306
|
+
failureKind = "client_bug";
|
|
307
|
+
}
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Token refresh failed (${status}${oauthError ? ` ${oauthError}` : ""})`
|
|
310
|
+
);
|
|
242
311
|
}
|
|
243
312
|
const data = await response.json();
|
|
244
313
|
if (!data || typeof data.access_token !== "string") {
|
|
@@ -258,17 +327,35 @@ var HttpClient = class {
|
|
|
258
327
|
this.refreshFailureCount = 0;
|
|
259
328
|
this.refreshLockedUntil = 0;
|
|
260
329
|
return data.access_token;
|
|
261
|
-
} catch {
|
|
330
|
+
} catch (e) {
|
|
331
|
+
const baseMsg = e instanceof Error ? e.message : "Token refresh failed";
|
|
262
332
|
this.refreshFailureCount++;
|
|
263
333
|
const backoffMs = Math.min(
|
|
264
334
|
500 * 2 ** Math.max(0, this.refreshFailureCount - 1),
|
|
265
335
|
3e4
|
|
266
336
|
);
|
|
267
337
|
this.refreshLockedUntil = Date.now() + backoffMs;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
338
|
+
if (failureKind === "permanent") {
|
|
339
|
+
this.clearTokens();
|
|
340
|
+
this.config.onTokenExpired?.();
|
|
341
|
+
const error2 = new AuthError(`${baseMsg}. Please login again.`);
|
|
342
|
+
this.emitError(error2);
|
|
343
|
+
this.config.onAuthError?.(error2);
|
|
344
|
+
throw error2;
|
|
345
|
+
}
|
|
346
|
+
if (failureKind === "client_bug") {
|
|
347
|
+
const error2 = new AuthError(
|
|
348
|
+
`${baseMsg}. Client request invalid; tokens preserved.`
|
|
349
|
+
);
|
|
350
|
+
this.emitError(error2);
|
|
351
|
+
this.config.onAuthError?.(error2);
|
|
352
|
+
throw error2;
|
|
353
|
+
}
|
|
354
|
+
const error = new AuthError(
|
|
355
|
+
`${baseMsg}. Transient failure; tokens preserved, will retry after backoff.`
|
|
356
|
+
);
|
|
271
357
|
this.emitError(error);
|
|
358
|
+
this.config.onTransientRefreshFailure?.(error);
|
|
272
359
|
this.config.onAuthError?.(error);
|
|
273
360
|
throw error;
|
|
274
361
|
} finally {
|
|
@@ -397,14 +484,24 @@ var HttpClient = class {
|
|
|
397
484
|
timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
398
485
|
signal: config?.signal
|
|
399
486
|
});
|
|
487
|
+
const startedAt = Date.now();
|
|
488
|
+
let status = 0;
|
|
400
489
|
try {
|
|
401
490
|
const response = await fetch(`${this.config.baseUrl}${url}`, {
|
|
402
491
|
...init,
|
|
403
492
|
credentials: "include",
|
|
404
493
|
signal
|
|
405
494
|
});
|
|
495
|
+
status = response.status;
|
|
406
496
|
return await this.handleResponse(response);
|
|
407
497
|
} finally {
|
|
498
|
+
this.recentCalls.push({
|
|
499
|
+
method: (init.method || "GET").toUpperCase(),
|
|
500
|
+
path: sanitizePathForBreadcrumb(url),
|
|
501
|
+
status,
|
|
502
|
+
duration_ms: Date.now() - startedAt,
|
|
503
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
504
|
+
});
|
|
408
505
|
cleanup();
|
|
409
506
|
}
|
|
410
507
|
}
|
|
@@ -8966,7 +9063,148 @@ var SupportAPI = class {
|
|
|
8966
9063
|
if (req.recaptchaToken) body.recaptcha_token = req.recaptchaToken;
|
|
8967
9064
|
return this.http.post("/v1/public/reports", body);
|
|
8968
9065
|
}
|
|
9066
|
+
/**
|
|
9067
|
+
* ConnectBase 플랫폼 자체 버그/요청/문의를 발행한다.
|
|
9068
|
+
*
|
|
9069
|
+
* `reportIssue` 와 다른 점: target 이 앱 운영자가 아닌 **ConnectBase 운영팀**.
|
|
9070
|
+
* SDK 가 자체 버그를 던지거나, 결제/문서/플랫폼 동작이 이상할 때 사용.
|
|
9071
|
+
*
|
|
9072
|
+
* 자동 첨부 (opt-out 가능):
|
|
9073
|
+
* - SDK 버전 + 플랫폼 (web/node)
|
|
9074
|
+
* - 마지막 N(20)개 API 호출 breadcrumb (PII strip 후)
|
|
9075
|
+
* - error 객체의 stack trace (sanitize)
|
|
9076
|
+
*
|
|
9077
|
+
* @example SDK 가 throw 한 에러를 자동 첨부해서 발행
|
|
9078
|
+
* ```typescript
|
|
9079
|
+
* try {
|
|
9080
|
+
* await cb.functions.invoke('foo', {})
|
|
9081
|
+
* } catch (err) {
|
|
9082
|
+
* await cb.support.reportPlatformBug({
|
|
9083
|
+
* title: 'functions.invoke 가 504 만 반환',
|
|
9084
|
+
* body: '같은 인자로 5분 째 504. 콘솔에선 정상.',
|
|
9085
|
+
* category: 'sdk',
|
|
9086
|
+
* severity: 'high',
|
|
9087
|
+
* error: err as Error,
|
|
9088
|
+
* })
|
|
9089
|
+
* }
|
|
9090
|
+
* ```
|
|
9091
|
+
*/
|
|
9092
|
+
async reportPlatformBug(req) {
|
|
9093
|
+
const attachContext = req.attachAutomaticContext !== false;
|
|
9094
|
+
const body = {
|
|
9095
|
+
title: req.title,
|
|
9096
|
+
body: req.body,
|
|
9097
|
+
category: req.category ?? "other",
|
|
9098
|
+
severity: req.severity ?? "medium",
|
|
9099
|
+
metadata: req.metadata
|
|
9100
|
+
};
|
|
9101
|
+
if (req.reporterEmail) body.reporter_email = req.reporterEmail;
|
|
9102
|
+
if (req.recaptchaToken) body.recaptcha_token = req.recaptchaToken;
|
|
9103
|
+
if (attachContext) {
|
|
9104
|
+
body.sdk_version = req.sdkVersion ?? detectSdkVersion();
|
|
9105
|
+
body.sdk_platform = req.sdkPlatform ?? detectSdkPlatform();
|
|
9106
|
+
body.environment = req.environment ?? "unknown";
|
|
9107
|
+
if (req.error) {
|
|
9108
|
+
body.stack_trace = sanitizeStackTrace(req.error.stack ?? String(req.error));
|
|
9109
|
+
} else if (req.stackTrace) {
|
|
9110
|
+
body.stack_trace = sanitizeStackTrace(req.stackTrace);
|
|
9111
|
+
}
|
|
9112
|
+
const calls = req.recentApiCalls ?? this.http.getRecentCalls();
|
|
9113
|
+
if (calls.length > 0) {
|
|
9114
|
+
body.recent_api_calls = calls;
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
return this.http.post(
|
|
9118
|
+
"/v1/public/platform-issues",
|
|
9119
|
+
body
|
|
9120
|
+
);
|
|
9121
|
+
}
|
|
9122
|
+
/** 디버깅용: 마지막 N개 API 호출 breadcrumb. PII 제거 후 저장돼있다. */
|
|
9123
|
+
getRecentApiCalls() {
|
|
9124
|
+
return this.http.getRecentCalls();
|
|
9125
|
+
}
|
|
9126
|
+
/** breadcrumb buffer 비우기 (사용자 명시적 요청 / 프라이버시). */
|
|
9127
|
+
clearRecentApiCalls() {
|
|
9128
|
+
this.http.clearRecentCalls();
|
|
9129
|
+
}
|
|
9130
|
+
/**
|
|
9131
|
+
* Platform issue 의 reply thread 조회.
|
|
9132
|
+
*
|
|
9133
|
+
* 본인이 발행한 issue 만 조회 가능 (server-side ownership guard). admin 의 internal 메모는 응답 제외.
|
|
9134
|
+
*
|
|
9135
|
+
* @param issueId - `reportPlatformBug` 가 반환한 id
|
|
9136
|
+
* @throws ApiError 404 — 본인 issue 가 아니거나 존재하지 않음
|
|
9137
|
+
*/
|
|
9138
|
+
async listPlatformIssueReplies(issueId) {
|
|
9139
|
+
const res = await this.http.get(
|
|
9140
|
+
`/v1/public/platform-issues/${issueId}/comments`
|
|
9141
|
+
);
|
|
9142
|
+
return res.comments;
|
|
9143
|
+
}
|
|
9144
|
+
/**
|
|
9145
|
+
* Platform issue 에 follow-up reply 작성.
|
|
9146
|
+
*
|
|
9147
|
+
* 단말 상태(resolved/wontfix/duplicate) issue 는 reply 거부 — 새 issue 발행 권장.
|
|
9148
|
+
*/
|
|
9149
|
+
async replyToPlatformIssue(issueId, body) {
|
|
9150
|
+
return this.http.post(
|
|
9151
|
+
`/v1/public/platform-issues/${issueId}/comments`,
|
|
9152
|
+
{ body }
|
|
9153
|
+
);
|
|
9154
|
+
}
|
|
9155
|
+
/**
|
|
9156
|
+
* 본인이 발행한 platform issue 의 처리 진행 상황을 단건 조회.
|
|
9157
|
+
*
|
|
9158
|
+
* AI 가 "내가 발행한 이슈 처리됐어?" 를 폴링하는 표준 경로. status / resolution_note /
|
|
9159
|
+
* triage_summary / external_links 로 ConnectBase 운영팀의 처리 상태를 확인.
|
|
9160
|
+
*
|
|
9161
|
+
* @throws ApiError 404 — 본인 issue 가 아니거나 존재하지 않음
|
|
9162
|
+
*/
|
|
9163
|
+
async getPlatformIssue(issueId) {
|
|
9164
|
+
return this.http.get(
|
|
9165
|
+
`/v1/public/platform-issues/${issueId}`
|
|
9166
|
+
);
|
|
9167
|
+
}
|
|
9168
|
+
/**
|
|
9169
|
+
* 본인 app 으로 발행한 platform issue 목록 (cursor 페이지네이션).
|
|
9170
|
+
*
|
|
9171
|
+
* status/severity/category 필터 + `since_updated_at` 으로 미해결만 폴링하는 사용 패턴 권장:
|
|
9172
|
+
*
|
|
9173
|
+
* ```typescript
|
|
9174
|
+
* const { issues } = await cb.support.listMyPlatformIssues({ status: ['open', 'triaged', 'in_progress'] })
|
|
9175
|
+
* ```
|
|
9176
|
+
*/
|
|
9177
|
+
async listMyPlatformIssues(opts = {}) {
|
|
9178
|
+
const params = new URLSearchParams();
|
|
9179
|
+
for (const s of opts.status ?? []) params.append("status", s);
|
|
9180
|
+
for (const s of opts.severity ?? []) params.append("severity", s);
|
|
9181
|
+
for (const s of opts.category ?? []) params.append("category", s);
|
|
9182
|
+
if (opts.sinceUpdatedAt) params.set("since_updated_at", opts.sinceUpdatedAt);
|
|
9183
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
9184
|
+
if (typeof opts.limit === "number") params.set("limit", String(opts.limit));
|
|
9185
|
+
const query = params.toString();
|
|
9186
|
+
return this.http.get(
|
|
9187
|
+
`/v1/public/platform-issues${query ? "?" + query : ""}`
|
|
9188
|
+
);
|
|
9189
|
+
}
|
|
8969
9190
|
};
|
|
9191
|
+
function detectSdkVersion() {
|
|
9192
|
+
if (typeof __SDK_VERSION__ !== "undefined" && __SDK_VERSION__) {
|
|
9193
|
+
return __SDK_VERSION__;
|
|
9194
|
+
}
|
|
9195
|
+
return "unknown";
|
|
9196
|
+
}
|
|
9197
|
+
function detectSdkPlatform() {
|
|
9198
|
+
return typeof window !== "undefined" && typeof document !== "undefined" ? "web" : "node";
|
|
9199
|
+
}
|
|
9200
|
+
function sanitizeStackTrace(raw) {
|
|
9201
|
+
let s = raw;
|
|
9202
|
+
s = s.replace(/(\(|\s|^)(?:file:\/\/)?\/[^\s)]+\/([^\s/)]+)(:\d+:\d+)?/g, "$1$2$3");
|
|
9203
|
+
s = s.replace(/(https?:\/\/[^\s)?]+)\?[^\s)]*/g, "$1");
|
|
9204
|
+
const max = 32 * 1024;
|
|
9205
|
+
if (s.length > max) s = s.slice(0, max) + "\n\u2026[truncated]";
|
|
9206
|
+
return s;
|
|
9207
|
+
}
|
|
8970
9208
|
|
|
8971
9209
|
// src/types/knowledge.ts
|
|
8972
9210
|
var AUTH_MEMBER_ID_TOKEN = "$auth.member_id";
|
|
@@ -9632,7 +9870,8 @@ var ConnectBase = class {
|
|
|
9632
9870
|
onError: config.onError,
|
|
9633
9871
|
onTokenRefresh: config.onTokenRefresh,
|
|
9634
9872
|
onAuthError: config.onAuthError,
|
|
9635
|
-
onTokenExpired: config.onTokenExpired
|
|
9873
|
+
onTokenExpired: config.onTokenExpired,
|
|
9874
|
+
onTransientRefreshFailure: config.onTransientRefreshFailure
|
|
9636
9875
|
};
|
|
9637
9876
|
this.http = new HttpClient(httpConfig);
|
|
9638
9877
|
this.auth = new AuthAPI(this.http);
|