connectbase-client 6.10.0 → 6.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 +47 -0
- package/README.md +2 -0
- package/dist/cli.js +230 -35
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +118 -40
- package/dist/index.d.ts +118 -40
- package/dist/index.js +537 -118
- package/dist/index.mjs +537 -118
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -571,6 +571,89 @@ var AIAPI = class {
|
|
|
571
571
|
}
|
|
572
572
|
};
|
|
573
573
|
|
|
574
|
+
// src/core/url-secrets.ts
|
|
575
|
+
var CREDENTIAL_PARAMS = /* @__PURE__ */ new Set([
|
|
576
|
+
"access_token",
|
|
577
|
+
"refresh_token",
|
|
578
|
+
"id_token",
|
|
579
|
+
"token",
|
|
580
|
+
"auth_token",
|
|
581
|
+
"session_token",
|
|
582
|
+
"jwt",
|
|
583
|
+
"client_secret",
|
|
584
|
+
"secret_key",
|
|
585
|
+
"api_key",
|
|
586
|
+
"apikey",
|
|
587
|
+
"code_verifier",
|
|
588
|
+
"password"
|
|
589
|
+
]);
|
|
590
|
+
var TOKEN_CALLBACK_COMPANIONS = /* @__PURE__ */ new Set([
|
|
591
|
+
"member_id",
|
|
592
|
+
"is_new_member",
|
|
593
|
+
"email",
|
|
594
|
+
"state"
|
|
595
|
+
]);
|
|
596
|
+
var CREDENTIAL_IN_TEXT = /\b(access_token|refresh_token|id_token|token|auth_token|session_token|jwt|client_secret|secret_key|api_key|apikey|code_verifier)=[^&#\s"'<>)\]]+/gi;
|
|
597
|
+
function paramName(pair) {
|
|
598
|
+
const raw = pair.split("=", 1)[0].replace(/\+/g, " ");
|
|
599
|
+
try {
|
|
600
|
+
return decodeURIComponent(raw).toLowerCase();
|
|
601
|
+
} catch {
|
|
602
|
+
return raw.toLowerCase();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function filterParams(raw, shouldRemove) {
|
|
606
|
+
if (!raw) return raw;
|
|
607
|
+
const pairs = raw.split("&");
|
|
608
|
+
const kept = pairs.filter((pair) => !shouldRemove(paramName(pair)));
|
|
609
|
+
return kept.length === pairs.length ? raw : kept.join("&");
|
|
610
|
+
}
|
|
611
|
+
function scrubParams(raw) {
|
|
612
|
+
if (!raw) return raw;
|
|
613
|
+
const names = new Set(raw.split("&").map(paramName));
|
|
614
|
+
const hasToken = names.has("access_token") || names.has("refresh_token");
|
|
615
|
+
const isCodeCallback = names.has("code") && names.has("state");
|
|
616
|
+
return filterParams(
|
|
617
|
+
raw,
|
|
618
|
+
(name) => CREDENTIAL_PARAMS.has(name) || hasToken && TOKEN_CALLBACK_COMPANIONS.has(name) || isCodeCallback && (name === "code" || name === "state")
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
function scrubFragment(fragment) {
|
|
622
|
+
const queryAt = fragment.indexOf("?");
|
|
623
|
+
if (queryAt >= 0) {
|
|
624
|
+
const head = fragment.slice(0, queryAt);
|
|
625
|
+
const clean = scrubParams(fragment.slice(queryAt + 1));
|
|
626
|
+
return clean ? `${head}?${clean}` : head;
|
|
627
|
+
}
|
|
628
|
+
return fragment.includes("=") ? scrubParams(fragment) : fragment;
|
|
629
|
+
}
|
|
630
|
+
function mapUrlParts(url, mapQuery, mapFragment) {
|
|
631
|
+
const hashAt = url.indexOf("#");
|
|
632
|
+
const beforeHash = hashAt >= 0 ? url.slice(0, hashAt) : url;
|
|
633
|
+
const queryAt = beforeHash.indexOf("?");
|
|
634
|
+
const path = queryAt >= 0 ? beforeHash.slice(0, queryAt) : beforeHash;
|
|
635
|
+
const rawQuery = queryAt >= 0 ? beforeHash.slice(queryAt + 1) : "";
|
|
636
|
+
const rawFragment = hashAt >= 0 ? url.slice(hashAt + 1) : "";
|
|
637
|
+
const query = mapQuery(rawQuery);
|
|
638
|
+
const fragment = mapFragment(rawFragment);
|
|
639
|
+
if (query === rawQuery && fragment === rawFragment) return url;
|
|
640
|
+
return `${path}${query ? `?${query}` : ""}${fragment ? `#${fragment}` : ""}`;
|
|
641
|
+
}
|
|
642
|
+
function scrubUrlSecrets(url) {
|
|
643
|
+
return url ? mapUrlParts(url, scrubParams, scrubFragment) : url;
|
|
644
|
+
}
|
|
645
|
+
function scrubSecretsInText(text) {
|
|
646
|
+
return text.replace(CREDENTIAL_IN_TEXT, "$1=[redacted]");
|
|
647
|
+
}
|
|
648
|
+
function removeQueryParams(url, names) {
|
|
649
|
+
const targets = new Set(names);
|
|
650
|
+
return mapUrlParts(
|
|
651
|
+
url,
|
|
652
|
+
(query) => filterParams(query, (name) => targets.has(name)),
|
|
653
|
+
(fragment) => fragment
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
574
657
|
// src/api/analytics.ts
|
|
575
658
|
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
576
659
|
var SESSION_KEY = "__cb_session";
|
|
@@ -836,9 +919,9 @@ var AnalyticsAPI = class {
|
|
|
836
919
|
this.session.touch();
|
|
837
920
|
const event = this.createBaseEvent("page_view");
|
|
838
921
|
event.page_path = path || window.location.pathname;
|
|
839
|
-
event.page_url = window.location.href;
|
|
922
|
+
event.page_url = scrubUrlSecrets(window.location.href);
|
|
840
923
|
event.page_title = document.title;
|
|
841
|
-
event.referrer = document.referrer || void 0;
|
|
924
|
+
event.referrer = scrubUrlSecrets(document.referrer) || void 0;
|
|
842
925
|
event.screen_width = window.screen.width;
|
|
843
926
|
event.screen_height = window.screen.height;
|
|
844
927
|
Object.assign(event, this.utm);
|
|
@@ -854,7 +937,7 @@ var AnalyticsAPI = class {
|
|
|
854
937
|
event.event_name = name;
|
|
855
938
|
event.event_properties = properties;
|
|
856
939
|
event.page_path = window.location.pathname;
|
|
857
|
-
event.page_url = window.location.href;
|
|
940
|
+
event.page_url = scrubUrlSecrets(window.location.href);
|
|
858
941
|
this.enqueue(event);
|
|
859
942
|
}
|
|
860
943
|
/**
|
|
@@ -1340,8 +1423,8 @@ var AnalyticsAPI = class {
|
|
|
1340
1423
|
trackSessionStart() {
|
|
1341
1424
|
const event = this.createBaseEvent("session_start");
|
|
1342
1425
|
event.page_path = window.location.pathname;
|
|
1343
|
-
event.page_url = window.location.href;
|
|
1344
|
-
event.referrer = document.referrer || void 0;
|
|
1426
|
+
event.page_url = scrubUrlSecrets(window.location.href);
|
|
1427
|
+
event.referrer = scrubUrlSecrets(document.referrer) || void 0;
|
|
1345
1428
|
event.screen_width = window.screen.width;
|
|
1346
1429
|
event.screen_height = window.screen.height;
|
|
1347
1430
|
Object.assign(event, this.utm);
|
|
@@ -2506,6 +2589,8 @@ var DatabaseAPI = class {
|
|
|
2506
2589
|
* `options.autoCreate: true` 시 테이블이 없으면 빈 schemaless 테이블을
|
|
2507
2590
|
* 자동으로 생성한 뒤 insert. opt-in 이며 프로토타입/AI 자동화 워크플로에
|
|
2508
2591
|
* 권장합니다. production 앱은 명시적 `createTable` 호출 권장.
|
|
2592
|
+
* 테이블 생성은 `secretKey` 나 service_role 함수에서만 됩니다. Public Key 만으로는
|
|
2593
|
+
* 옵션이 무시되어, 없는 테이블이면 404 입니다 (이미 있는 테이블에는 그대로 씁니다).
|
|
2509
2594
|
*/
|
|
2510
2595
|
async createData(tableIdOrName, data, options) {
|
|
2511
2596
|
const prefix = this.getPublicPrefix();
|
|
@@ -2979,13 +3064,13 @@ var DatabaseAPI = class {
|
|
|
2979
3064
|
* // 자기 앱 테이블 구독 (AppMember JWT)
|
|
2980
3065
|
* cb.database.connectRealtime({
|
|
2981
3066
|
* accessToken: 'your-jwt-token',
|
|
2982
|
-
* dataServerUrl: 'https://
|
|
3067
|
+
* dataServerUrl: 'https://api.connectbase.world'
|
|
2983
3068
|
* })
|
|
2984
3069
|
*
|
|
2985
3070
|
* // cross-app: Provider 앱의 테이블 구독 (OAuth access token)
|
|
2986
3071
|
* cb.database.connectRealtime({
|
|
2987
3072
|
* accessToken: oauthAccessToken,
|
|
2988
|
-
* dataServerUrl: 'https://
|
|
3073
|
+
* dataServerUrl: 'https://api.connectbase.world'
|
|
2989
3074
|
* })
|
|
2990
3075
|
*
|
|
2991
3076
|
* // 테이블 구독
|
|
@@ -3689,6 +3774,18 @@ async function drainBody(res) {
|
|
|
3689
3774
|
}
|
|
3690
3775
|
|
|
3691
3776
|
// src/api/error-tracker.ts
|
|
3777
|
+
function scrubReport(report) {
|
|
3778
|
+
const text = (value) => typeof value === "string" ? scrubSecretsInText(value) : value;
|
|
3779
|
+
const url = (value) => typeof value === "string" ? scrubUrlSecrets(value) : value;
|
|
3780
|
+
return {
|
|
3781
|
+
...report,
|
|
3782
|
+
message: text(report.message),
|
|
3783
|
+
stack: text(report.stack),
|
|
3784
|
+
url: url(report.url),
|
|
3785
|
+
referrer: url(report.referrer),
|
|
3786
|
+
source: url(report.source)
|
|
3787
|
+
};
|
|
3788
|
+
}
|
|
3692
3789
|
var ErrorTrackerAPI = class {
|
|
3693
3790
|
constructor(http, config = {}) {
|
|
3694
3791
|
this.storageWebId = null;
|
|
@@ -3867,7 +3964,7 @@ var ErrorTrackerAPI = class {
|
|
|
3867
3964
|
return filtered;
|
|
3868
3965
|
}
|
|
3869
3966
|
queueError(report) {
|
|
3870
|
-
this.errorQueue.push(report);
|
|
3967
|
+
this.errorQueue.push(scrubReport(report));
|
|
3871
3968
|
this.log("Error queued", {
|
|
3872
3969
|
message: report.message,
|
|
3873
3970
|
queueSize: this.errorQueue.length
|
|
@@ -4179,6 +4276,43 @@ function toCreateRoomWire(config) {
|
|
|
4179
4276
|
if (config.metadata) out.metadata = config.metadata;
|
|
4180
4277
|
return out;
|
|
4181
4278
|
}
|
|
4279
|
+
var FORBIDDEN_STATE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
|
|
4280
|
+
"__proto__",
|
|
4281
|
+
"constructor",
|
|
4282
|
+
"prototype"
|
|
4283
|
+
]);
|
|
4284
|
+
var hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key);
|
|
4285
|
+
function applyGameStateChange(state, change) {
|
|
4286
|
+
const segments = typeof change.path === "string" ? change.path.split(".") : null;
|
|
4287
|
+
if (!segments || segments.some((segment) => FORBIDDEN_STATE_PATH_SEGMENTS.has(segment))) {
|
|
4288
|
+
console.warn(
|
|
4289
|
+
"[connect-base game] \uC704\uD5D8\uD55C \uACBD\uB85C\uC758 \uC0C1\uD0DC \uBCC0\uACBD\uC744 \uBB34\uC2DC\uD588\uC2B5\uB2C8\uB2E4:",
|
|
4290
|
+
String(change.path).slice(0, 100)
|
|
4291
|
+
);
|
|
4292
|
+
return false;
|
|
4293
|
+
}
|
|
4294
|
+
const isDelete = change.operation === "delete";
|
|
4295
|
+
let current = state;
|
|
4296
|
+
for (const key of segments.slice(0, -1)) {
|
|
4297
|
+
const next = hasOwn(current, key) ? current[key] : void 0;
|
|
4298
|
+
if (next !== null && typeof next === "object") {
|
|
4299
|
+
current = next;
|
|
4300
|
+
} else if (isDelete) {
|
|
4301
|
+
return true;
|
|
4302
|
+
} else {
|
|
4303
|
+
const created = {};
|
|
4304
|
+
current[key] = created;
|
|
4305
|
+
current = created;
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
const lastKey = segments[segments.length - 1];
|
|
4309
|
+
if (isDelete) {
|
|
4310
|
+
if (hasOwn(current, lastKey)) delete current[lastKey];
|
|
4311
|
+
} else {
|
|
4312
|
+
current[lastKey] = change.value;
|
|
4313
|
+
}
|
|
4314
|
+
return true;
|
|
4315
|
+
}
|
|
4182
4316
|
var GameRoom = class {
|
|
4183
4317
|
constructor(config) {
|
|
4184
4318
|
this.ws = null;
|
|
@@ -4695,21 +4829,7 @@ var GameRoom = class {
|
|
|
4695
4829
|
}
|
|
4696
4830
|
applyChange(change) {
|
|
4697
4831
|
if (!this._state) return;
|
|
4698
|
-
|
|
4699
|
-
let current = this._state.state;
|
|
4700
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
4701
|
-
const key = path[i];
|
|
4702
|
-
if (!(key in current)) {
|
|
4703
|
-
current[key] = {};
|
|
4704
|
-
}
|
|
4705
|
-
current = current[key];
|
|
4706
|
-
}
|
|
4707
|
-
const lastKey = path[path.length - 1];
|
|
4708
|
-
if (change.operation === "delete") {
|
|
4709
|
-
delete current[lastKey];
|
|
4710
|
-
} else {
|
|
4711
|
-
current[lastKey] = change.value;
|
|
4712
|
-
}
|
|
4832
|
+
applyGameStateChange(this._state.state, change);
|
|
4713
4833
|
}
|
|
4714
4834
|
handlePlayerEvent(msg) {
|
|
4715
4835
|
const player = {
|
|
@@ -5391,6 +5511,7 @@ var KnowledgeAPI = class {
|
|
|
5391
5511
|
* 문서 추가
|
|
5392
5512
|
*
|
|
5393
5513
|
* 지식 베이스에 새 문서를 추가합니다. 텍스트 소스인 경우 즉시 처리됩니다.
|
|
5514
|
+
* 로그인한 회원 토큰(본인 문서)이나 `secretKey` 가 필요합니다. Public Key 만으로는 401 입니다.
|
|
5394
5515
|
*
|
|
5395
5516
|
* @param kbID - 지식 베이스 ID
|
|
5396
5517
|
* @param data - 문서 생성 요청
|
|
@@ -5546,6 +5667,7 @@ var KnowledgeAPI = class {
|
|
|
5546
5667
|
* 문서 삭제
|
|
5547
5668
|
*
|
|
5548
5669
|
* 문서와 관련된 모든 청크도 함께 삭제됩니다.
|
|
5670
|
+
* 로그인한 회원 토큰(본인 문서)이나 `secretKey` 가 필요합니다. Public Key 만으로는 401 입니다.
|
|
5549
5671
|
*
|
|
5550
5672
|
* @param kbID - 지식 베이스 ID
|
|
5551
5673
|
* @param docID - 문서 ID
|
|
@@ -6484,6 +6606,77 @@ var NativeAPI = class {
|
|
|
6484
6606
|
}
|
|
6485
6607
|
};
|
|
6486
6608
|
|
|
6609
|
+
// src/core/oauth-state.ts
|
|
6610
|
+
var STORAGE_KEY = "cb_oauth_pending_states";
|
|
6611
|
+
var PENDING_TTL_MS = 60 * 60 * 1e3;
|
|
6612
|
+
var MAX_PENDING = 8;
|
|
6613
|
+
var OAUTH_STATE_MISMATCH = "oauth_state_mismatch";
|
|
6614
|
+
function browserStorage() {
|
|
6615
|
+
try {
|
|
6616
|
+
return typeof window !== "undefined" ? window.localStorage : null;
|
|
6617
|
+
} catch {
|
|
6618
|
+
return null;
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
function readPending(storage, now) {
|
|
6622
|
+
try {
|
|
6623
|
+
const parsed = JSON.parse(storage.getItem(STORAGE_KEY) || "[]");
|
|
6624
|
+
if (!Array.isArray(parsed)) return [];
|
|
6625
|
+
return parsed.filter(
|
|
6626
|
+
(entry) => typeof entry?.nonce === "string" && typeof entry?.expiresAt === "number" && entry.expiresAt > now
|
|
6627
|
+
);
|
|
6628
|
+
} catch {
|
|
6629
|
+
return [];
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
function writePending(storage, entries) {
|
|
6633
|
+
if (entries.length === 0) {
|
|
6634
|
+
storage.removeItem(STORAGE_KEY);
|
|
6635
|
+
} else {
|
|
6636
|
+
storage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
function randomNonce() {
|
|
6640
|
+
const bytes = new Uint8Array(16);
|
|
6641
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
6642
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
6643
|
+
}
|
|
6644
|
+
function createPendingOAuthState(appState) {
|
|
6645
|
+
const storage = browserStorage();
|
|
6646
|
+
const nonce = randomNonce();
|
|
6647
|
+
try {
|
|
6648
|
+
if (!storage) throw new Error("storage unavailable");
|
|
6649
|
+
const now = Date.now();
|
|
6650
|
+
const entries = readPending(storage, now);
|
|
6651
|
+
entries.push({
|
|
6652
|
+
nonce,
|
|
6653
|
+
expiresAt: now + PENDING_TTL_MS,
|
|
6654
|
+
...appState ? { appState } : {}
|
|
6655
|
+
});
|
|
6656
|
+
writePending(storage, entries.slice(-MAX_PENDING));
|
|
6657
|
+
} catch {
|
|
6658
|
+
throw new Error(
|
|
6659
|
+
"\uBE0C\uB77C\uC6B0\uC800 \uC800\uC7A5\uC18C\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC5B4 \uC18C\uC15C \uB85C\uADF8\uC778\uC744 \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uCFE0\uD0A4\uC640 \uC0AC\uC774\uD2B8 \uB370\uC774\uD130 \uCC28\uB2E8\uC744 \uD574\uC81C\uD55C \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694."
|
|
6660
|
+
);
|
|
6661
|
+
}
|
|
6662
|
+
return nonce;
|
|
6663
|
+
}
|
|
6664
|
+
function consumePendingOAuthState(state) {
|
|
6665
|
+
if (!state) return { valid: false };
|
|
6666
|
+
const storage = browserStorage();
|
|
6667
|
+
if (!storage) return { valid: false };
|
|
6668
|
+
try {
|
|
6669
|
+
const entries = readPending(storage, Date.now());
|
|
6670
|
+
const index = entries.findIndex((entry) => entry.nonce === state);
|
|
6671
|
+
if (index < 0) return { valid: false };
|
|
6672
|
+
const [matched] = entries.splice(index, 1);
|
|
6673
|
+
writePending(storage, entries);
|
|
6674
|
+
return { valid: true, appState: matched.appState };
|
|
6675
|
+
} catch {
|
|
6676
|
+
return { valid: false };
|
|
6677
|
+
}
|
|
6678
|
+
}
|
|
6679
|
+
|
|
6487
6680
|
// src/api/oauth.ts
|
|
6488
6681
|
function getNativeOAuthBridge() {
|
|
6489
6682
|
if (typeof window === "undefined") return null;
|
|
@@ -6493,6 +6686,71 @@ function getNativeOAuthBridge() {
|
|
|
6493
6686
|
}
|
|
6494
6687
|
return bridge;
|
|
6495
6688
|
}
|
|
6689
|
+
var POPUP_OPENER_NOT_ALLOWED = "popup_opener_not_allowed";
|
|
6690
|
+
var TOKEN_CALLBACK_PARAMS = [
|
|
6691
|
+
"access_token",
|
|
6692
|
+
"refresh_token",
|
|
6693
|
+
"member_id",
|
|
6694
|
+
"is_new_member",
|
|
6695
|
+
"email",
|
|
6696
|
+
"state"
|
|
6697
|
+
];
|
|
6698
|
+
var CODE_CALLBACK_PARAMS = ["code", "state"];
|
|
6699
|
+
var capturedTokenCallbacks = /* @__PURE__ */ new WeakMap();
|
|
6700
|
+
function hasSameOriginOpener() {
|
|
6701
|
+
try {
|
|
6702
|
+
return !!window.opener && window.opener.location.origin === window.location.origin;
|
|
6703
|
+
} catch {
|
|
6704
|
+
return false;
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
function stripParamsFromUrl(names) {
|
|
6708
|
+
try {
|
|
6709
|
+
const { pathname, search, hash } = window.location;
|
|
6710
|
+
window.history.replaceState(
|
|
6711
|
+
window.history.state,
|
|
6712
|
+
"",
|
|
6713
|
+
removeQueryParams(`${pathname}${search}${hash}`, names)
|
|
6714
|
+
);
|
|
6715
|
+
} catch {
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
function captureTokenCallback() {
|
|
6719
|
+
if (typeof window === "undefined" || !window.location) return null;
|
|
6720
|
+
const existing = capturedTokenCallbacks.get(window);
|
|
6721
|
+
const params = new URLSearchParams(window.location.search);
|
|
6722
|
+
const accessToken = params.get("access_token");
|
|
6723
|
+
const refreshToken = params.get("refresh_token");
|
|
6724
|
+
const inUrl = !!accessToken && !!refreshToken;
|
|
6725
|
+
if (!inUrl) {
|
|
6726
|
+
return existing && existing.href === window.location.href ? existing : null;
|
|
6727
|
+
}
|
|
6728
|
+
if (existing?.accessToken === accessToken) return existing;
|
|
6729
|
+
stripParamsFromUrl(TOKEN_CALLBACK_PARAMS);
|
|
6730
|
+
const memberId = params.get("member_id");
|
|
6731
|
+
if (!memberId) return null;
|
|
6732
|
+
const state = params.get("state");
|
|
6733
|
+
const check = consumePendingOAuthState(state);
|
|
6734
|
+
const captured = {
|
|
6735
|
+
// state 가 맞으면 opener 가 있어도 리다이렉트다(같은 출처 창이 연 창에서 signIn 을 부른 경우).
|
|
6736
|
+
kind: check.valid ? "redirect" : hasSameOriginOpener() ? "popup" : "rejected",
|
|
6737
|
+
accessToken,
|
|
6738
|
+
refreshToken,
|
|
6739
|
+
memberId,
|
|
6740
|
+
isNewMember: params.get("is_new_member") === "true",
|
|
6741
|
+
email: params.get("email"),
|
|
6742
|
+
appState: check.valid ? check.appState : state || void 0,
|
|
6743
|
+
href: window.location.href
|
|
6744
|
+
};
|
|
6745
|
+
capturedTokenCallbacks.set(window, captured);
|
|
6746
|
+
if (captured.kind === "rejected") warnStateMismatch();
|
|
6747
|
+
return captured;
|
|
6748
|
+
}
|
|
6749
|
+
function warnStateMismatch() {
|
|
6750
|
+
console.warn(
|
|
6751
|
+
"[connect-base-client] OAuth \uCF5C\uBC31\uC744 \uBC1B\uC544\uB4E4\uC774\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC774 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC2DC\uC791\uD55C \uB85C\uADF8\uC778\uC758 \uACB0\uACFC\uC784\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4(state \uBD88\uC77C\uCE58). \uB0A8\uC774 \uBCF4\uB0B8 \uB9C1\uD06C\uB77C\uBA74 \uC815\uC0C1 \uB3D9\uC791\uC785\uB2C8\uB2E4. \uC815\uC0C1 \uB85C\uADF8\uC778\uC774 \uAC70\uBD80\uB41C\uB2E4\uBA74 cb.oauth.signIn() \uC744 \uBD80\uB978 \uD398\uC774\uC9C0\uC640 \uCF5C\uBC31 \uC8FC\uC18C\uC758 \uCD9C\uCC98\uAC00 \uAC19\uC740\uC9C0, \uC778\uAC00 \uC8FC\uC18C\uB97C \uC9C1\uC811 \uB9CC\uB4E0\uB2E4\uBA74 cb.oauth.createCallbackState() \uAC12\uC744 state \uB85C \uB118\uACBC\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694."
|
|
6752
|
+
);
|
|
6753
|
+
}
|
|
6496
6754
|
function detectInAppBrowser() {
|
|
6497
6755
|
if (typeof navigator === "undefined") return null;
|
|
6498
6756
|
const ua = navigator.userAgent || "";
|
|
@@ -6530,6 +6788,10 @@ var OAuthAPI = class {
|
|
|
6530
6788
|
* 발화하지 않도록 이 promise 를 공유한다 (이중 re-issue/rotation 방지).
|
|
6531
6789
|
*/
|
|
6532
6790
|
this.bootConsumePromise = null;
|
|
6791
|
+
/** 이 인스턴스가 `getCallbackResult()` 로 이미 적재한 콜백. 다시 불려도 토큰을 되돌리지 않는다. */
|
|
6792
|
+
this.loadedCallback = null;
|
|
6793
|
+
/** 진행 중이거나 끝난 code 교환. 주소에서 code 를 지운 뒤의 재호출이 같은 결과를 받게 한다. */
|
|
6794
|
+
this.codeExchange = null;
|
|
6533
6795
|
}
|
|
6534
6796
|
/**
|
|
6535
6797
|
* 활성화된 OAuth 프로바이더 목록 조회
|
|
@@ -6559,7 +6821,8 @@ var OAuthAPI = class {
|
|
|
6559
6821
|
*
|
|
6560
6822
|
* @param provider - OAuth 프로바이더 (google, naver, github, discord)
|
|
6561
6823
|
* @param callbackUrl - OAuth 완료 후 리다이렉트될 앱의 URL
|
|
6562
|
-
* @param state -
|
|
6824
|
+
* @param state - 앱이 콜백까지 들고 가고 싶은 값(선택). `getCallbackResult()` 결과의 `state` 로 돌아온다.
|
|
6825
|
+
* 로그인 CSRF 방어는 SDK 가 별도의 일회용 값으로 항상 수행하므로 이 값이 없어도 안전하다.
|
|
6563
6826
|
* @param options - `createIfNotExists: true` 면 미가입 사용자를 자동 가입시킨다([signUp] 과 동일 동작).
|
|
6564
6827
|
*
|
|
6565
6828
|
* @example
|
|
@@ -6604,9 +6867,9 @@ var OAuthAPI = class {
|
|
|
6604
6867
|
const callbackScheme = bridge.getCallbackScheme();
|
|
6605
6868
|
const bridgeParams = new URLSearchParams({
|
|
6606
6869
|
app_callback: callbackScheme,
|
|
6607
|
-
intent
|
|
6870
|
+
intent,
|
|
6871
|
+
state: this.createCallbackStateFor(callbackUrl, state)
|
|
6608
6872
|
});
|
|
6609
|
-
if (state) bridgeParams.append("state", state);
|
|
6610
6873
|
const bridgeResp = await this.http.get(
|
|
6611
6874
|
`/v1/public/oauth/${provider}/authorize/central?${bridgeParams.toString()}`
|
|
6612
6875
|
);
|
|
@@ -6631,15 +6894,52 @@ var OAuthAPI = class {
|
|
|
6631
6894
|
`\uD604\uC7AC ${inApp.type === "kakaotalk" ? "\uCE74\uCE74\uC624\uD1A1" : inApp.type === "line" ? "\uB77C\uC778" : "\uC778\uC571"} \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C\uB294 Google/Apple \uB85C\uADF8\uC778\uC774 \uC81C\uD55C\uB429\uB2C8\uB2E4. \uC6B0\uCE21 \uC0C1\uB2E8 \uBA54\uB274\uC5D0\uC11C "\uB2E4\uB978 \uBE0C\uB77C\uC6B0\uC800\uB85C \uC5F4\uAE30"\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694.`
|
|
6632
6895
|
);
|
|
6633
6896
|
}
|
|
6634
|
-
const params = new URLSearchParams({
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6897
|
+
const params = new URLSearchParams({
|
|
6898
|
+
app_callback: callbackUrl,
|
|
6899
|
+
intent,
|
|
6900
|
+
state: this.createCallbackStateFor(callbackUrl, state)
|
|
6901
|
+
});
|
|
6638
6902
|
const response = await this.http.get(
|
|
6639
6903
|
`/v1/public/oauth/${provider}/authorize/central?${params.toString()}`
|
|
6640
6904
|
);
|
|
6641
6905
|
window.location.href = response.authorization_url;
|
|
6642
6906
|
}
|
|
6907
|
+
/**
|
|
6908
|
+
* 콜백에서 확인할 일회용 state 를 만들어 이 브라우저에 저장합니다.
|
|
6909
|
+
*
|
|
6910
|
+
* `signIn()` / `signUp()` 은 이미 내부에서 부르므로 호출할 필요가 없습니다. 인가 주소를 SDK 밖에서
|
|
6911
|
+
* 만드는 앱(`GET /v1/public/oauth/:provider/authorize/central` 직접 호출 등)만, 돌려받은 값을 `state`
|
|
6912
|
+
* 쿼리로 넘기세요. 이 값 없이 돌아온 콜백의 토큰은 남이 보낸 링크와 구분할 수 없어 거부됩니다.
|
|
6913
|
+
*
|
|
6914
|
+
* @param appState - 콜백까지 들고 갈 앱의 값(선택). 서버로 보내지 않고 `getCallbackResult()` 의 `state` 로 돌아옵니다.
|
|
6915
|
+
* @returns 인가 요청의 `state` 쿼리에 넣을 값
|
|
6916
|
+
* @throws 브라우저 저장소를 쓸 수 없을 때
|
|
6917
|
+
*
|
|
6918
|
+
* @example
|
|
6919
|
+
* ```typescript
|
|
6920
|
+
* const state = cb.oauth.createCallbackState()
|
|
6921
|
+
* const res = await fetch(`${API}/v1/public/oauth/google/authorize/central?app_callback=${callback}&state=${state}`, {
|
|
6922
|
+
* headers: { 'X-Public-Key': PUBLIC_KEY },
|
|
6923
|
+
* })
|
|
6924
|
+
* ```
|
|
6925
|
+
*/
|
|
6926
|
+
createCallbackState(appState) {
|
|
6927
|
+
return createPendingOAuthState(appState);
|
|
6928
|
+
}
|
|
6929
|
+
/** 콜백 주소가 다른 출처면 그쪽 저장소에는 state 가 없어 콜백에서 거부된다는 점을 시작할 때 알린다. */
|
|
6930
|
+
createCallbackStateFor(callbackUrl, appState) {
|
|
6931
|
+
const state = createPendingOAuthState(appState);
|
|
6932
|
+
try {
|
|
6933
|
+
const target = new URL(callbackUrl, window.location.href);
|
|
6934
|
+
if (target.origin !== window.location.origin) {
|
|
6935
|
+
console.warn(
|
|
6936
|
+
`[connect-base-client] \uCF5C\uBC31 \uC8FC\uC18C(${target.origin})\uAC00 \uC9C0\uAE08 \uD398\uC774\uC9C0\uC640 \uCD9C\uCC98\uAC00 \uB2EC\uB77C SDK \uAC00 \uD1A0\uD070\uC744 \uBC1B\uC544\uB4E4\uC774\uC9C0 \uBABB\uD569\uB2C8\uB2E4. \uB85C\uADF8\uC778\uC744 \uC2DC\uC791\uD55C \uD398\uC774\uC9C0\uC640 \uAC19\uC740 \uCD9C\uCC98\uB97C \uCF5C\uBC31\uC73C\uB85C \uC9C0\uC815\uD558\uC138\uC694.`
|
|
6937
|
+
);
|
|
6938
|
+
}
|
|
6939
|
+
} catch {
|
|
6940
|
+
}
|
|
6941
|
+
return state;
|
|
6942
|
+
}
|
|
6643
6943
|
/**
|
|
6644
6944
|
* 소셜 로그인 (팝업 방식)
|
|
6645
6945
|
* 팝업 창에서 소셜 로그인을 처리하고 결과를 Promise로 반환합니다.
|
|
@@ -6652,6 +6952,7 @@ var OAuthAPI = class {
|
|
|
6652
6952
|
*
|
|
6653
6953
|
* @param provider - OAuth 프로바이더
|
|
6654
6954
|
* @param callbackUrl - (선택) OAuth 완료 후 리다이렉트될 팝업의 URL. 미지정 시 서버 제공 콜백 사용.
|
|
6955
|
+
* 지정한다면 이 메서드를 부르는 페이지와 같은 출처여야 합니다(콜백 페이지는 토큰을 같은 출처 창에만 넘깁니다).
|
|
6655
6956
|
* @returns 로그인 결과
|
|
6656
6957
|
*
|
|
6657
6958
|
* @example
|
|
@@ -6710,10 +7011,24 @@ var OAuthAPI = class {
|
|
|
6710
7011
|
params.set("intent", intent);
|
|
6711
7012
|
if (callbackUrl) {
|
|
6712
7013
|
params.set("app_callback", callbackUrl);
|
|
7014
|
+
} else {
|
|
7015
|
+
params.set("opener_origin", window.location.origin);
|
|
7016
|
+
}
|
|
7017
|
+
let response;
|
|
7018
|
+
try {
|
|
7019
|
+
response = await this.http.get(
|
|
7020
|
+
`/v1/public/oauth/${provider}/authorize/central?${params.toString()}`
|
|
7021
|
+
);
|
|
7022
|
+
} catch (err) {
|
|
7023
|
+
if (err instanceof ApiError && err.code === POPUP_OPENER_NOT_ALLOWED) {
|
|
7024
|
+
throw new ApiError(
|
|
7025
|
+
err.statusCode,
|
|
7026
|
+
`\uC774 \uD398\uC774\uC9C0(${window.location.origin})\uC5D0\uC11C\uB294 \uD31D\uC5C5 \uB85C\uADF8\uC778\uC744 \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774 \uC571\uC758 \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC8FC\uC18C\uB098 \uC5F0\uACB0\uB41C \uCEE4\uC2A4\uD140 \uB3C4\uBA54\uC778\uC5D0\uC11C \uC5EC\uC138\uC694. \uB2E4\uB978 \uC8FC\uC18C\uB77C\uBA74 \uCF58\uC194\uC758 \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uBCF4\uC548 \uC124\uC815\uC5D0 \uC774 \uCD9C\uCC98\uB97C \uD5C8\uC6A9 origin \uC73C\uB85C \uCD94\uAC00\uD558\uAC70\uB098 \uB9AC\uB2E4\uC774\uB809\uD2B8 \uBC29\uC2DD(cb.oauth.signIn)\uC744 \uC4F0\uC138\uC694.`,
|
|
7027
|
+
POPUP_OPENER_NOT_ALLOWED
|
|
7028
|
+
);
|
|
7029
|
+
}
|
|
7030
|
+
throw err;
|
|
6713
7031
|
}
|
|
6714
|
-
const response = await this.http.get(
|
|
6715
|
-
`/v1/public/oauth/${provider}/authorize/central?${params.toString()}`
|
|
6716
|
-
);
|
|
6717
7032
|
const width = 500;
|
|
6718
7033
|
const height = 600;
|
|
6719
7034
|
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
@@ -6726,6 +7041,10 @@ var OAuthAPI = class {
|
|
|
6726
7041
|
if (!popup) {
|
|
6727
7042
|
throw new Error("\uD31D\uC5C5\uC774 \uCC28\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uD31D\uC5C5 \uCC28\uB2E8\uC744 \uD574\uC81C\uD574\uC8FC\uC138\uC694.");
|
|
6728
7043
|
}
|
|
7044
|
+
const allowedOrigins = this.popupMessageOrigins(
|
|
7045
|
+
response.authorization_url,
|
|
7046
|
+
callbackUrl
|
|
7047
|
+
);
|
|
6729
7048
|
return new Promise((resolve, reject) => {
|
|
6730
7049
|
let settled = false;
|
|
6731
7050
|
const cleanup = () => {
|
|
@@ -6735,6 +7054,8 @@ var OAuthAPI = class {
|
|
|
6735
7054
|
clearTimeout(timeout);
|
|
6736
7055
|
};
|
|
6737
7056
|
const handleMessage = async (event) => {
|
|
7057
|
+
if (event.source !== popup) return;
|
|
7058
|
+
if (!allowedOrigins.has(event.origin)) return;
|
|
6738
7059
|
if (event.data?.type !== "oauth-callback") return;
|
|
6739
7060
|
if (settled) return;
|
|
6740
7061
|
cleanup();
|
|
@@ -6742,14 +7063,13 @@ var OAuthAPI = class {
|
|
|
6742
7063
|
reject(new Error(event.data.error));
|
|
6743
7064
|
return;
|
|
6744
7065
|
}
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
};
|
|
7066
|
+
let result;
|
|
7067
|
+
try {
|
|
7068
|
+
result = await this.readPopupResult(event.data);
|
|
7069
|
+
} catch (err) {
|
|
7070
|
+
reject(err);
|
|
7071
|
+
return;
|
|
7072
|
+
}
|
|
6753
7073
|
this.http.setTokens(result.access_token, result.refresh_token);
|
|
6754
7074
|
try {
|
|
6755
7075
|
await this.http.bootstrapRefreshCookie();
|
|
@@ -6782,6 +7102,51 @@ var OAuthAPI = class {
|
|
|
6782
7102
|
}, 18e4);
|
|
6783
7103
|
});
|
|
6784
7104
|
}
|
|
7105
|
+
/**
|
|
7106
|
+
* 팝업 메시지를 보낼 수 있는 출처: 서버 제공 콜백 페이지(API 서버)와 앱이 지정한 콜백 페이지.
|
|
7107
|
+
*
|
|
7108
|
+
* 서버 제공 콜백의 출처는 `baseUrl` 과 다를 수 있어(`/cb-api` 패스스루) 서버가 정한 redirect_uri 도
|
|
7109
|
+
* 함께 본다. 인가 주소는 우리 API 응답이므로 믿을 수 있다.
|
|
7110
|
+
*/
|
|
7111
|
+
popupMessageOrigins(authorizationUrl, callbackUrl) {
|
|
7112
|
+
const origins = /* @__PURE__ */ new Set();
|
|
7113
|
+
const add = (url) => {
|
|
7114
|
+
if (!url) return;
|
|
7115
|
+
try {
|
|
7116
|
+
origins.add(new URL(url, window.location.href).origin);
|
|
7117
|
+
} catch {
|
|
7118
|
+
}
|
|
7119
|
+
};
|
|
7120
|
+
add(this.http.getBaseUrl());
|
|
7121
|
+
try {
|
|
7122
|
+
add(new URL(authorizationUrl).searchParams.get("redirect_uri"));
|
|
7123
|
+
} catch {
|
|
7124
|
+
}
|
|
7125
|
+
add(callbackUrl);
|
|
7126
|
+
return origins;
|
|
7127
|
+
}
|
|
7128
|
+
/**
|
|
7129
|
+
* 팝업 메시지에서 로그인 결과를 만든다. code 가 있으면 교환한다(opener_origin 을 보낸 요청의 기본 응답).
|
|
7130
|
+
* 토큰을 직접 싣는 메시지는 opener_origin 을 모르는 구버전 서버 호환이다.
|
|
7131
|
+
*/
|
|
7132
|
+
async readPopupResult(data) {
|
|
7133
|
+
if (typeof data.code === "string" && data.code) {
|
|
7134
|
+
return this.http.post("/v1/auth/oauth/exchange", {
|
|
7135
|
+
code: data.code
|
|
7136
|
+
});
|
|
7137
|
+
}
|
|
7138
|
+
const { access_token, refresh_token, member_id, email } = data;
|
|
7139
|
+
if (typeof access_token !== "string" || typeof refresh_token !== "string" || typeof member_id !== "string" || !access_token || !refresh_token || !member_id) {
|
|
7140
|
+
throw new Error("OAuth \uC751\uB2F5\uC5D0 \uD1A0\uD070\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
7141
|
+
}
|
|
7142
|
+
return {
|
|
7143
|
+
member_id,
|
|
7144
|
+
access_token,
|
|
7145
|
+
refresh_token,
|
|
7146
|
+
is_new_member: data.is_new_member === "true" || data.is_new_member === true,
|
|
7147
|
+
...typeof email === "string" && email.length > 0 ? { email } : {}
|
|
7148
|
+
};
|
|
7149
|
+
}
|
|
6785
7150
|
/**
|
|
6786
7151
|
* 콜백 URL에서 OAuth 결과 추출
|
|
6787
7152
|
* 중앙 콜백 방식에서 리다이렉트 후 URL 파라미터에서 결과를 추출합니다.
|
|
@@ -6797,6 +7162,10 @@ var OAuthAPI = class {
|
|
|
6797
7162
|
* 없는 상태로 `getMe()` 가 401 으로 떨어지는 회귀가 있었다 (platform-issue
|
|
6798
7163
|
* 019e638d, 2026-05-26).
|
|
6799
7164
|
*
|
|
7165
|
+
* 토큰은 이 브라우저에서 `signIn()` / `signUp()` 으로 시작한 로그인의 결과일 때만 받아들입니다. 확인되지
|
|
7166
|
+
* 않으면(남이 보낸 링크 등) 토큰을 적재하지 않고 `{ error: 'oauth_state_mismatch' }` 를 돌려줍니다.
|
|
7167
|
+
* 토큰은 SDK 가 생성될 때 주소에서 지우므로 `location.search` 로 직접 읽을 수 없습니다.
|
|
7168
|
+
*
|
|
6800
7169
|
* @returns OAuth 결과 (토큰, member_id 등) 또는 null
|
|
6801
7170
|
*
|
|
6802
7171
|
* @example
|
|
@@ -6813,36 +7182,40 @@ var OAuthAPI = class {
|
|
|
6813
7182
|
* ```
|
|
6814
7183
|
*/
|
|
6815
7184
|
async getCallbackResult() {
|
|
7185
|
+
const callback = captureTokenCallback();
|
|
6816
7186
|
const params = new URLSearchParams(window.location.search);
|
|
6817
7187
|
const error = params.get("error");
|
|
6818
7188
|
if (error) {
|
|
7189
|
+
consumePendingOAuthState(params.get("state"));
|
|
6819
7190
|
const errorResult = { error };
|
|
6820
|
-
if (
|
|
7191
|
+
if (hasSameOriginOpener()) {
|
|
6821
7192
|
window.opener.postMessage(
|
|
6822
7193
|
{ type: "oauth-callback", ...errorResult },
|
|
6823
|
-
|
|
7194
|
+
window.location.origin
|
|
6824
7195
|
);
|
|
6825
7196
|
window.close();
|
|
6826
7197
|
}
|
|
6827
7198
|
return errorResult;
|
|
6828
7199
|
}
|
|
6829
|
-
|
|
6830
|
-
const refreshToken = params.get("refresh_token");
|
|
6831
|
-
const memberId = params.get("member_id");
|
|
6832
|
-
if (!accessToken || !refreshToken || !memberId) {
|
|
7200
|
+
if (!callback) {
|
|
6833
7201
|
return null;
|
|
6834
7202
|
}
|
|
6835
|
-
|
|
7203
|
+
if (callback.kind === "rejected") {
|
|
7204
|
+
return { error: OAUTH_STATE_MISMATCH };
|
|
7205
|
+
}
|
|
6836
7206
|
const result = {
|
|
6837
|
-
access_token: accessToken,
|
|
6838
|
-
refresh_token: refreshToken,
|
|
6839
|
-
member_id: memberId,
|
|
6840
|
-
is_new_member:
|
|
6841
|
-
...
|
|
6842
|
-
state:
|
|
7207
|
+
access_token: callback.accessToken,
|
|
7208
|
+
refresh_token: callback.refreshToken,
|
|
7209
|
+
member_id: callback.memberId,
|
|
7210
|
+
is_new_member: callback.isNewMember,
|
|
7211
|
+
...callback.email ? { email: callback.email } : {},
|
|
7212
|
+
state: callback.appState
|
|
6843
7213
|
};
|
|
6844
|
-
if (
|
|
6845
|
-
window.opener.postMessage(
|
|
7214
|
+
if (callback.kind === "popup") {
|
|
7215
|
+
window.opener.postMessage(
|
|
7216
|
+
{ type: "oauth-callback", ...result },
|
|
7217
|
+
window.location.origin
|
|
7218
|
+
);
|
|
6846
7219
|
window.close();
|
|
6847
7220
|
return result;
|
|
6848
7221
|
}
|
|
@@ -6852,10 +7225,35 @@ var OAuthAPI = class {
|
|
|
6852
7225
|
return result;
|
|
6853
7226
|
}
|
|
6854
7227
|
}
|
|
6855
|
-
this.
|
|
6856
|
-
await this.http.bootstrapRefreshCookie();
|
|
7228
|
+
await this.loadCallbackOnce(callback);
|
|
6857
7229
|
return result;
|
|
6858
7230
|
}
|
|
7231
|
+
/**
|
|
7232
|
+
* 콜백 토큰을 이 인스턴스에 한 번만 적재한다. 앱이 `getCallbackResult()` 를 다시 불러도 그사이 회전된
|
|
7233
|
+
* 토큰을 콜백의 옛 토큰으로 되돌리지 않는다.
|
|
7234
|
+
*/
|
|
7235
|
+
loadCallbackOnce(callback) {
|
|
7236
|
+
if (this.loadedCallback?.callback !== callback) {
|
|
7237
|
+
this.http.setTokens(callback.accessToken, callback.refreshToken);
|
|
7238
|
+
this.loadedCallback = {
|
|
7239
|
+
callback,
|
|
7240
|
+
done: this.http.bootstrapRefreshCookie()
|
|
7241
|
+
};
|
|
7242
|
+
}
|
|
7243
|
+
return this.loadedCallback.done;
|
|
7244
|
+
}
|
|
7245
|
+
/**
|
|
7246
|
+
* (SDK 내부용 — `ConnectBase` 생성자가 호출) 주소의 토큰 콜백을 회수해 지우고 그 종류를 알린다.
|
|
7247
|
+
*
|
|
7248
|
+
* @returns `redirect` 면 부팅 안전망 대상, `popup` 이면 팝업 콜백, `none` 이면 토큰 콜백이 아니거나 거부됨.
|
|
7249
|
+
*/
|
|
7250
|
+
captureCallbackOnBoot() {
|
|
7251
|
+
const callback = captureTokenCallback();
|
|
7252
|
+
if (callback?.kind === "redirect" || callback?.kind === "popup") {
|
|
7253
|
+
return callback.kind;
|
|
7254
|
+
}
|
|
7255
|
+
return "none";
|
|
7256
|
+
}
|
|
6859
7257
|
/**
|
|
6860
7258
|
* (SDK 내부용 — `ConnectBase` 생성자가 호출) 부팅 안전망.
|
|
6861
7259
|
*
|
|
@@ -6869,10 +7267,9 @@ var OAuthAPI = class {
|
|
|
6869
7267
|
* 이 안전망은 콜백 처리 코드를 빠뜨린 앱도 동작하게 한다.
|
|
6870
7268
|
*
|
|
6871
7269
|
* **token rotation race(2026-05-16) 가 없는 이유:** 사전 `re-issue`(cookie 복구) 없이 URL
|
|
6872
|
-
* 토큰을 *직접* 소비한다 — `getCallbackResult()` 의 리다이렉트 경로와 동일한 동작이다. 팝업
|
|
6873
|
-
* (`
|
|
6874
|
-
*
|
|
6875
|
-
* 콜백도 생성자에서 제외된다 (네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
7270
|
+
* 토큰을 *직접* 소비한다 — `getCallbackResult()` 의 리다이렉트 경로와 동일한 동작이다. 팝업 콜백과
|
|
7271
|
+
* 거부된 콜백(`captureCallbackOnBoot()` 참고)은 생성자에서 제외되어 호출되지 않는다. code-only(`?code=`),
|
|
7272
|
+
* 에러(`?error=`) 콜백도 생성자에서 제외된다 (네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
6876
7273
|
*
|
|
6877
7274
|
* @returns 토큰 적재 성공 시 true, no-op(토큰 없음 등)이면 false.
|
|
6878
7275
|
*/
|
|
@@ -6884,14 +7281,9 @@ var OAuthAPI = class {
|
|
|
6884
7281
|
}
|
|
6885
7282
|
async doConsumeRedirectOnBoot() {
|
|
6886
7283
|
try {
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
const accessToken = params.get("access_token");
|
|
6891
|
-
const refreshToken = params.get("refresh_token");
|
|
6892
|
-
const memberId = params.get("member_id");
|
|
6893
|
-
if (!accessToken || !refreshToken || !memberId) return false;
|
|
6894
|
-
this.http.setTokens(accessToken, refreshToken);
|
|
7284
|
+
const callback = captureTokenCallback();
|
|
7285
|
+
if (callback?.kind !== "redirect") return false;
|
|
7286
|
+
this.http.setTokens(callback.accessToken, callback.refreshToken);
|
|
6895
7287
|
await this.http.bootstrapRefreshCookie();
|
|
6896
7288
|
return true;
|
|
6897
7289
|
} catch {
|
|
@@ -6908,6 +7300,9 @@ var OAuthAPI = class {
|
|
|
6908
7300
|
* 1) `cb.oauth.exchangeCodeFromCallback()` 먼저 — code-only 모드면 성공
|
|
6909
7301
|
* 2) null 반환 시 `cb.oauth.getCallbackResult()` 폴백 (legacy 토큰-in-URL 모드)
|
|
6910
7302
|
*
|
|
7303
|
+
* `getCallbackResult()` 와 같은 이유로, 이 브라우저에서 시작한 로그인의 `code` 만 교환합니다. 확인되지
|
|
7304
|
+
* 않으면 `oauth_state_mismatch` 오류를 던지고 교환하지 않습니다.
|
|
7305
|
+
*
|
|
6911
7306
|
* @example
|
|
6912
7307
|
* ```typescript
|
|
6913
7308
|
* const result = await cb.oauth.exchangeCodeFromCallback()
|
|
@@ -6917,8 +7312,27 @@ var OAuthAPI = class {
|
|
|
6917
7312
|
async exchangeCodeFromCallback() {
|
|
6918
7313
|
const params = new URLSearchParams(window.location.search);
|
|
6919
7314
|
const code = params.get("code");
|
|
6920
|
-
if (!code)
|
|
7315
|
+
if (!code) {
|
|
7316
|
+
return this.codeExchange?.href === window.location.href ? this.codeExchange.result : null;
|
|
7317
|
+
}
|
|
6921
7318
|
if (params.get("error")) return null;
|
|
7319
|
+
const state = params.get("state");
|
|
7320
|
+
const check = consumePendingOAuthState(state);
|
|
7321
|
+
const isPopup = !check.valid && hasSameOriginOpener();
|
|
7322
|
+
if (!check.valid && !isPopup) {
|
|
7323
|
+
warnStateMismatch();
|
|
7324
|
+
throw new Error(OAUTH_STATE_MISMATCH);
|
|
7325
|
+
}
|
|
7326
|
+
stripParamsFromUrl(CODE_CALLBACK_PARAMS);
|
|
7327
|
+
const result = this.exchangeCode(
|
|
7328
|
+
code,
|
|
7329
|
+
check.valid ? check.appState : state || void 0,
|
|
7330
|
+
isPopup
|
|
7331
|
+
);
|
|
7332
|
+
this.codeExchange = { href: window.location.href, result };
|
|
7333
|
+
return result;
|
|
7334
|
+
}
|
|
7335
|
+
async exchangeCode(code, appState, isPopup) {
|
|
6922
7336
|
const resp = await this.http.post("/v1/auth/oauth/exchange", { code });
|
|
6923
7337
|
const result = {
|
|
6924
7338
|
access_token: resp.access_token,
|
|
@@ -6926,10 +7340,13 @@ var OAuthAPI = class {
|
|
|
6926
7340
|
member_id: resp.member_id,
|
|
6927
7341
|
is_new_member: resp.is_new_member,
|
|
6928
7342
|
...resp.email ? { email: resp.email } : {},
|
|
6929
|
-
state:
|
|
7343
|
+
state: appState
|
|
6930
7344
|
};
|
|
6931
|
-
if (
|
|
6932
|
-
window.opener.postMessage(
|
|
7345
|
+
if (isPopup) {
|
|
7346
|
+
window.opener.postMessage(
|
|
7347
|
+
{ type: "oauth-callback", ...result },
|
|
7348
|
+
window.location.origin
|
|
7349
|
+
);
|
|
6933
7350
|
window.close();
|
|
6934
7351
|
return result;
|
|
6935
7352
|
}
|
|
@@ -7295,6 +7712,9 @@ var PaymentAPI = class {
|
|
|
7295
7712
|
* 결제 취소
|
|
7296
7713
|
* 완료된 결제를 취소합니다. 부분 취소도 가능합니다.
|
|
7297
7714
|
*
|
|
7715
|
+
* **서버 전용.** 환불은 돈이 나가는 동작이라 `secretKey`(`cb_sk_*`, `app:update` 권한)가 필요합니다.
|
|
7716
|
+
* 브라우저에서 Public Key 만으로 호출하면 401 `SECRET_KEY_REQUIRED` 입니다.
|
|
7717
|
+
*
|
|
7298
7718
|
* @param paymentId - 결제 ID (Connect Base에서 발급한 ID)
|
|
7299
7719
|
* @param data - 취소 정보 (사유, 부분 취소 금액)
|
|
7300
7720
|
* @returns 취소된 결제 정보
|
|
@@ -7324,6 +7744,10 @@ var PaymentAPI = class {
|
|
|
7324
7744
|
* 주문 ID로 결제 조회
|
|
7325
7745
|
* 특정 주문의 결제 상태를 조회합니다.
|
|
7326
7746
|
*
|
|
7747
|
+
* Public Key 만으로 호출하면 상태와 금액만 돌려줍니다. `customer_email`, `customer_name`, `customer_id`, `receipt_url`,
|
|
7748
|
+
* `metadata`, `country`, `settlement_*`, `cancel_reason` 은 그 결제의 회원 본인(로그인 상태), `secretKey`(`app:read` 권한),
|
|
7749
|
+
* 또는 `payment:read` 스코프의 service_role 함수에만 옵니다.
|
|
7750
|
+
*
|
|
7327
7751
|
* @param orderId - 주문 ID
|
|
7328
7752
|
* @returns 결제 상세 정보
|
|
7329
7753
|
*
|
|
@@ -11182,8 +11606,8 @@ var SupportAPI = class {
|
|
|
11182
11606
|
}
|
|
11183
11607
|
};
|
|
11184
11608
|
function detectSdkVersion() {
|
|
11185
|
-
if ("6.
|
|
11186
|
-
return "6.
|
|
11609
|
+
if ("6.11.0") {
|
|
11610
|
+
return "6.11.0";
|
|
11187
11611
|
}
|
|
11188
11612
|
return "unknown";
|
|
11189
11613
|
}
|
|
@@ -12883,6 +13307,7 @@ var HttpClient = class {
|
|
|
12883
13307
|
this.config = { ...config };
|
|
12884
13308
|
this.storageKey = this.buildStorageKey();
|
|
12885
13309
|
this.warnIfUnsafePersistence();
|
|
13310
|
+
this.warnIfSecretKeyInBrowser();
|
|
12886
13311
|
this.restoreTokens();
|
|
12887
13312
|
}
|
|
12888
13313
|
/**
|
|
@@ -12916,6 +13341,20 @@ var HttpClient = class {
|
|
|
12916
13341
|
clearRecentCalls() {
|
|
12917
13342
|
this.recentCalls.clear();
|
|
12918
13343
|
}
|
|
13344
|
+
/**
|
|
13345
|
+
* Secret Key 는 계정 권한의 자격증명이다. 브라우저 번들에 넣으면 페이지를 여는 누구나 읽는다.
|
|
13346
|
+
* 막지는 않는다(Electron 등 신뢰된 환경이 있다) — 대신 무엇이 위험한지 분명히 알린다.
|
|
13347
|
+
*/
|
|
13348
|
+
warnIfSecretKeyInBrowser() {
|
|
13349
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
13350
|
+
return;
|
|
13351
|
+
}
|
|
13352
|
+
const misplaced = this.config.publicKey?.startsWith("cb_sk_") === true;
|
|
13353
|
+
if (!this.config.secretKey && !misplaced) return;
|
|
13354
|
+
console.warn(
|
|
13355
|
+
"[connect-base-client] \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C Secret Key(cb_sk_)\uB97C \uC4F0\uACE0 \uC788\uC2B5\uB2C8\uB2E4. \uD398\uC774\uC9C0\uB97C \uC5EC\uB294 \uB204\uAD6C\uB098 \uC774 \uD0A4\uB97C \uC77D\uC5B4 \uACC4\uC815 \uAD8C\uD55C\uC73C\uB85C API \uB97C \uD638\uCD9C\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uBE0C\uB77C\uC6B0\uC800\uC5D0\uB294 publicKey(cb_pk_)\uB9CC \uB123\uACE0 Secret Key \uB294 \uC11C\uBC84\uB098 CLI \uC5D0\uC11C\uB9CC \uC4F0\uC138\uC694. \uC774\uBBF8 \uBC30\uD3EC\uB410\uB2E4\uBA74 \uCF58\uC194\uC5D0\uC11C \uD0A4\uB97C \uD3D0\uAE30\uD558\uACE0 \uC0C8\uB85C \uBC1C\uAE09\uD558\uC138\uC694."
|
|
13356
|
+
);
|
|
13357
|
+
}
|
|
12919
13358
|
warnIfUnsafePersistence() {
|
|
12920
13359
|
if (typeof window === "undefined") return;
|
|
12921
13360
|
if (this.config.persistence === "localStorage") {
|
|
@@ -13250,15 +13689,23 @@ var HttpClient = class {
|
|
|
13250
13689
|
if (credential) {
|
|
13251
13690
|
headers["X-Public-Key"] = credential;
|
|
13252
13691
|
}
|
|
13692
|
+
const cookieOnly = !this.config.refreshToken;
|
|
13253
13693
|
const response = await withAbort(
|
|
13254
|
-
fetch(
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
13694
|
+
fetch(
|
|
13695
|
+
`${this.config.baseUrl}/v1/auth/re-issue${cookieOnly ? "?quiet=1" : ""}`,
|
|
13696
|
+
{
|
|
13697
|
+
method: "POST",
|
|
13698
|
+
headers,
|
|
13699
|
+
credentials: "include",
|
|
13700
|
+
signal
|
|
13701
|
+
}
|
|
13702
|
+
),
|
|
13260
13703
|
signal
|
|
13261
13704
|
);
|
|
13705
|
+
if (cookieOnly && response.status === 204) {
|
|
13706
|
+
failureKind = "permanent";
|
|
13707
|
+
throw new Error("Token refresh failed (no session)");
|
|
13708
|
+
}
|
|
13262
13709
|
if (!response.ok) {
|
|
13263
13710
|
const status = response.status;
|
|
13264
13711
|
let oauthError;
|
|
@@ -14442,21 +14889,7 @@ var GameRoomTransport = class {
|
|
|
14442
14889
|
}
|
|
14443
14890
|
applyChange(change) {
|
|
14444
14891
|
if (!this._state) return;
|
|
14445
|
-
|
|
14446
|
-
let current = this._state.state;
|
|
14447
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
14448
|
-
const key = path[i];
|
|
14449
|
-
if (!(key in current)) {
|
|
14450
|
-
current[key] = {};
|
|
14451
|
-
}
|
|
14452
|
-
current = current[key];
|
|
14453
|
-
}
|
|
14454
|
-
const lastKey = path[path.length - 1];
|
|
14455
|
-
if (change.operation === "delete") {
|
|
14456
|
-
delete current[lastKey];
|
|
14457
|
-
} else {
|
|
14458
|
-
current[lastKey] = change.value;
|
|
14459
|
-
}
|
|
14892
|
+
applyGameStateChange(this._state.state, change);
|
|
14460
14893
|
}
|
|
14461
14894
|
handlePlayerEvent(msg) {
|
|
14462
14895
|
const player = {
|
|
@@ -14571,14 +15004,15 @@ var ConnectBase = class {
|
|
|
14571
15004
|
this.endpoint = new EndpointAPI(this.http);
|
|
14572
15005
|
this.support = new SupportAPI(this.http);
|
|
14573
15006
|
this.auth._attachAnalytics(this.analytics);
|
|
15007
|
+
const tokenCallback = typeof window !== "undefined" ? this.oauth.captureCallbackOnBoot() : "none";
|
|
14574
15008
|
const shouldAutoRestore = config.autoRestoreSession ?? true;
|
|
14575
15009
|
if (shouldAutoRestore && typeof window !== "undefined") {
|
|
14576
|
-
if (
|
|
15010
|
+
if (tokenCallback === "redirect") {
|
|
14577
15011
|
this.http.setBootRestorePromise(
|
|
14578
15012
|
this.oauth.consumeRedirectCallbackOnBoot(),
|
|
14579
15013
|
{ alwaysAwait: true }
|
|
14580
15014
|
);
|
|
14581
|
-
} else if (!this.isOAuthCallbackUrl()) {
|
|
15015
|
+
} else if (tokenCallback !== "popup" && !this.isOAuthCallbackUrl()) {
|
|
14582
15016
|
this.http.setBootRestorePromise(
|
|
14583
15017
|
this.http.tryRestoreSessionFromCookie()
|
|
14584
15018
|
);
|
|
@@ -14586,34 +15020,19 @@ var ConnectBase = class {
|
|
|
14586
15020
|
}
|
|
14587
15021
|
}
|
|
14588
15022
|
/**
|
|
14589
|
-
* 현재 페이지가 OAuth
|
|
15023
|
+
* 현재 페이지가 앱이 직접 처리할 OAuth 콜백 페이지인지 판별.
|
|
14590
15024
|
*
|
|
14591
|
-
* `?
|
|
14592
|
-
*
|
|
14593
|
-
*
|
|
15025
|
+
* `?code=...` (OAUTH_CODE_ONLY 흐름) 또는 OAuth 에러 응답 (`?error=...&state=...`) 패턴이면 true.
|
|
15026
|
+
* 콜백 페이지에서 일반 `autoRestoreSession` 을 건너뛰어 rotation race 를 피한다. 토큰 콜백은
|
|
15027
|
+
* 이미 주소에서 회수됐으므로 여기서 보지 않는다(`oauth.captureCallbackOnBoot`).
|
|
14594
15028
|
*/
|
|
14595
15029
|
isOAuthCallbackUrl() {
|
|
14596
15030
|
if (typeof window === "undefined" || !window.location) return false;
|
|
14597
15031
|
const params = new URLSearchParams(window.location.search);
|
|
14598
|
-
if (params.has("access_token") && params.has("refresh_token")) return true;
|
|
14599
15032
|
if (params.has("code") && params.has("state")) return true;
|
|
14600
15033
|
if (params.has("error") && params.has("state")) return true;
|
|
14601
15034
|
return false;
|
|
14602
15035
|
}
|
|
14603
|
-
/**
|
|
14604
|
-
* 현재 페이지가 토큰-in-URL OAuth 리다이렉트 콜백(팝업 아님)인지 판별.
|
|
14605
|
-
*
|
|
14606
|
-
* `?access_token=&refresh_token=` 이 있고 `window.opener` 가 없으면 true — 부팅 안전망
|
|
14607
|
-
* (`oauth.consumeRedirectCallbackOnBoot`)으로 토큰을 자동 소비할 대상이다. 팝업 콜백
|
|
14608
|
-
* (`window.opener` 존재)은 `getCallbackResult()` 의 postMessage 경로가 전담하므로 제외한다.
|
|
14609
|
-
* code-only(`?code=`)·에러(`?error=`) 콜백도 제외(네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
14610
|
-
*/
|
|
14611
|
-
isOAuthRedirectCallbackUrl() {
|
|
14612
|
-
if (typeof window === "undefined" || !window.location) return false;
|
|
14613
|
-
if (window.opener) return false;
|
|
14614
|
-
const params = new URLSearchParams(window.location.search);
|
|
14615
|
-
return params.has("access_token") && params.has("refresh_token");
|
|
14616
|
-
}
|
|
14617
15036
|
/**
|
|
14618
15037
|
* 새로고침/탭 재개 후 cookie 만으로 세션을 복원한다 (브라우저 전용).
|
|
14619
15038
|
*
|