@rscc/common-core 0.3.0 → 0.5.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/README.md +219 -14
- package/dist/index.cjs +543 -23
- package/dist/index.d.cts +454 -9
- package/dist/index.d.ts +454 -9
- package/dist/index.js +523 -23
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -20,6 +20,108 @@ var ResultCode = {
|
|
|
20
20
|
INTERNAL_SERVER_ERROR: "500"
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
// src/errorCodes.ts
|
|
24
|
+
var COMMON_CODES = new Set(Object.values(ResultCode));
|
|
25
|
+
function isCommonResultCode(code) {
|
|
26
|
+
return COMMON_CODES.has(code);
|
|
27
|
+
}
|
|
28
|
+
function resultCodeForStatus(status) {
|
|
29
|
+
const exact = String(status);
|
|
30
|
+
if (exact !== ResultCode.SUCCESS && COMMON_CODES.has(exact)) return exact;
|
|
31
|
+
if (status >= 400 && status < 500) return ResultCode.BAD_REQUEST;
|
|
32
|
+
return ResultCode.INTERNAL_SERVER_ERROR;
|
|
33
|
+
}
|
|
34
|
+
function toCommonResultCode(code, status) {
|
|
35
|
+
return isCommonResultCode(code) ? code : resultCodeForStatus(status);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/csrf.ts
|
|
39
|
+
var DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
|
|
40
|
+
var DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
|
|
41
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
|
|
42
|
+
var SENTINEL_ORIGIN = "http://rscc-csrf.invalid";
|
|
43
|
+
var ABSOLUTE_URL = /^([a-zA-Z][a-zA-Z\d+.-]*):\/\/([^/?#\\]*)/;
|
|
44
|
+
var HAS_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
|
|
45
|
+
var PROTOCOL_RELATIVE = /^[\\/]{2}/;
|
|
46
|
+
var DEFAULT_PORTS = { http: "80", https: "443", ws: "80", wss: "443" };
|
|
47
|
+
function readCookie(name, cookieString) {
|
|
48
|
+
const source = cookieString ?? documentCookie();
|
|
49
|
+
if (!source) return null;
|
|
50
|
+
for (const part of source.split(";")) {
|
|
51
|
+
const pair = part.trim();
|
|
52
|
+
const eq = pair.indexOf("=");
|
|
53
|
+
if (eq < 0) continue;
|
|
54
|
+
if (pair.slice(0, eq) === name) return pair.slice(eq + 1);
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function isUnsafeMethod(method) {
|
|
59
|
+
return !SAFE_METHODS.has((method || "GET").toUpperCase());
|
|
60
|
+
}
|
|
61
|
+
function csrfHeaderFor(url, method, options) {
|
|
62
|
+
if (!isUnsafeMethod(method)) return null;
|
|
63
|
+
const opts = options === true || options === void 0 ? {} : options;
|
|
64
|
+
const cookieString = opts.readCookie ? opts.readCookie() : documentCookie();
|
|
65
|
+
if (!cookieString) return null;
|
|
66
|
+
const token = readCookie(opts.cookieName ?? DEFAULT_CSRF_COOKIE_NAME, cookieString);
|
|
67
|
+
if (!token) return null;
|
|
68
|
+
if (!isAllowedTarget(url, opts.allowedOrigins)) return null;
|
|
69
|
+
return [opts.headerName ?? DEFAULT_CSRF_HEADER_NAME, token];
|
|
70
|
+
}
|
|
71
|
+
function documentCookie() {
|
|
72
|
+
if (typeof document === "undefined") return null;
|
|
73
|
+
try {
|
|
74
|
+
return typeof document.cookie === "string" ? document.cookie : null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function currentLocation() {
|
|
80
|
+
if (typeof location === "undefined") return null;
|
|
81
|
+
try {
|
|
82
|
+
const { href, origin } = location;
|
|
83
|
+
return typeof href === "string" && typeof origin === "string" ? { href, origin } : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isAllowedTarget(url, allowedOrigins) {
|
|
89
|
+
const loc = currentLocation();
|
|
90
|
+
const base = loc ? loc.href : SENTINEL_ORIGIN;
|
|
91
|
+
const selfOrigin = loc ? loc.origin : SENTINEL_ORIGIN;
|
|
92
|
+
const target = originOf(url, base, selfOrigin);
|
|
93
|
+
if (target === null || target === "null") return false;
|
|
94
|
+
if (selfOrigin !== "null" && target === selfOrigin) return true;
|
|
95
|
+
if (!allowedOrigins || allowedOrigins.length === 0) return false;
|
|
96
|
+
return allowedOrigins.some((entry) => originOf(entry, base, selfOrigin) === target);
|
|
97
|
+
}
|
|
98
|
+
function originOf(url, base, baseOrigin) {
|
|
99
|
+
try {
|
|
100
|
+
if (typeof URL === "function") {
|
|
101
|
+
const origin = new URL(url, base).origin;
|
|
102
|
+
if (typeof origin === "string") return origin;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
return looseOriginOf(url, baseOrigin);
|
|
107
|
+
}
|
|
108
|
+
function looseOriginOf(url, baseOrigin) {
|
|
109
|
+
const trimmed = url.trim();
|
|
110
|
+
const m = ABSOLUTE_URL.exec(trimmed);
|
|
111
|
+
if (m) {
|
|
112
|
+
const scheme = m[1].toLowerCase();
|
|
113
|
+
const authority = m[2];
|
|
114
|
+
const hostPort = authority.slice(authority.lastIndexOf("@") + 1).toLowerCase();
|
|
115
|
+
const hp = /^(.*?)(?::(\d*))?$/.exec(hostPort);
|
|
116
|
+
const host = hp?.[1] ?? "";
|
|
117
|
+
const port = hp?.[2] ?? "";
|
|
118
|
+
if (!host) return null;
|
|
119
|
+
return port === "" || port === DEFAULT_PORTS[scheme] ? `${scheme}://${host}` : `${scheme}://${host}:${port}`;
|
|
120
|
+
}
|
|
121
|
+
if (PROTOCOL_RELATIVE.test(trimmed) || HAS_SCHEME.test(trimmed)) return null;
|
|
122
|
+
return baseOrigin;
|
|
123
|
+
}
|
|
124
|
+
|
|
23
125
|
// src/idempotency.ts
|
|
24
126
|
var FALLBACK_GROUPS = [8, 4, 4, 4, 12];
|
|
25
127
|
function generateIdempotencyKey() {
|
|
@@ -32,6 +134,141 @@ function generateIdempotencyKey() {
|
|
|
32
134
|
}).join("-");
|
|
33
135
|
}
|
|
34
136
|
|
|
137
|
+
// src/messages.ts
|
|
138
|
+
var KO = Object.freeze({
|
|
139
|
+
"rscc.result.SUCCESS": "\uC694\uCCAD\uC774 \uC131\uACF5\uC801\uC73C\uB85C \uCC98\uB9AC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
|
|
140
|
+
"rscc.result.BAD_REQUEST": "\uC798\uBABB\uB41C \uC694\uCCAD\uC785\uB2C8\uB2E4.",
|
|
141
|
+
"rscc.result.UNAUTHORIZED": "\uC778\uC99D\uB418\uC9C0 \uC54A\uC740 \uC0AC\uC6A9\uC790\uC785\uB2C8\uB2E4.",
|
|
142
|
+
"rscc.result.FORBIDDEN": "\uC811\uADFC \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
143
|
+
"rscc.result.NOT_FOUND": "\uB9AC\uC18C\uC2A4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
144
|
+
"rscc.result.METHOD_NOT_ALLOWED": "\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC740 \uC694\uCCAD \uBC29\uC2DD\uC785\uB2C8\uB2E4.",
|
|
145
|
+
"rscc.result.CONFLICT": "\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uB9AC\uC18C\uC2A4\uC785\uB2C8\uB2E4.",
|
|
146
|
+
"rscc.result.UNSUPPORTED_MEDIA_TYPE": "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC694\uCCAD \uD615\uC2DD\uC785\uB2C8\uB2E4.",
|
|
147
|
+
"rscc.result.TOO_MANY_REQUESTS": "\uC694\uCCAD \uD55C\uB3C4\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.",
|
|
148
|
+
"rscc.result.INTERNAL_SERVER_ERROR": "\uC11C\uBC84 \uB0B4\uBD80 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.",
|
|
149
|
+
"rscc.web.missingParameter": "\uD544\uC218 \uC694\uCCAD \uD30C\uB77C\uBBF8\uD130\uAC00 \uB204\uB77D\uB418\uC5C8\uC2B5\uB2C8\uB2E4: {0}",
|
|
150
|
+
"rscc.web.typeMismatch": "\uC694\uCCAD \uD30C\uB77C\uBBF8\uD130 \uD0C0\uC785\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: {0}",
|
|
151
|
+
"rscc.web.unreadableBody": "\uC694\uCCAD \uBCF8\uBB38\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. JSON \uD615\uC2DD\uACFC \uAC01 \uD544\uB4DC\uC758 \uD0C0\uC785\uC744 \uD655\uC778\uD558\uC138\uC694.",
|
|
152
|
+
"rscc.idempotency.conflict": "\uC774\uBBF8 \uCC98\uB9AC\uB418\uC5C8\uAC70\uB098 \uCC98\uB9AC \uC911\uC778 \uC694\uCCAD\uC785\uB2C8\uB2E4.",
|
|
153
|
+
"rscc.idempotency.invalidFormat": "Idempotency-Key \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
|
|
154
|
+
"rscc.idempotency.required": "Idempotency-Key \uD5E4\uB354\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
|
|
155
|
+
"rscc.upload.size": "\uC5C5\uB85C\uB4DC \uAC00\uB2A5\uD55C \uCD5C\uB300 \uD06C\uAE30\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.",
|
|
156
|
+
"rscc.upload.extension": "\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: {0}",
|
|
157
|
+
"rscc.upload.contentMismatch": "\uD30C\uC77C \uB0B4\uC6A9\uC774 \uD655\uC7A5\uC790\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
|
|
158
|
+
"rscc.query.sortField": "\uC815\uB82C\uD560 \uC218 \uC5C6\uB294 \uD544\uB4DC\uC785\uB2C8\uB2E4: {0}",
|
|
159
|
+
"rscc.query.sortDirection": "\uC815\uB82C \uBC29\uD5A5\uC740 asc|desc \uB9CC \uD5C8\uC6A9\uD569\uB2C8\uB2E4: {0}",
|
|
160
|
+
"rscc.security.csrf": "CSRF \uD1A0\uD070\uC774 \uC5C6\uAC70\uB098 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
|
|
161
|
+
"rscc.client.unreadableBody": "\uC751\uB2F5 \uBCF8\uBB38\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
|
|
162
|
+
"rscc.client.invalidJson": "\uC751\uB2F5 \uBCF8\uBB38\uC774 \uC720\uD6A8\uD55C JSON \uC774 \uC544\uB2D9\uB2C8\uB2E4.",
|
|
163
|
+
"rscc.client.httpError": "API \uC624\uB958: {0} {1}"
|
|
164
|
+
});
|
|
165
|
+
var EN = Object.freeze({
|
|
166
|
+
"rscc.result.SUCCESS": "The request was processed successfully.",
|
|
167
|
+
"rscc.result.BAD_REQUEST": "Bad request.",
|
|
168
|
+
"rscc.result.UNAUTHORIZED": "Authentication is required.",
|
|
169
|
+
"rscc.result.FORBIDDEN": "Access is denied.",
|
|
170
|
+
"rscc.result.NOT_FOUND": "Resource not found.",
|
|
171
|
+
"rscc.result.METHOD_NOT_ALLOWED": "Request method is not allowed.",
|
|
172
|
+
"rscc.result.CONFLICT": "The resource already exists.",
|
|
173
|
+
"rscc.result.UNSUPPORTED_MEDIA_TYPE": "Unsupported request content type.",
|
|
174
|
+
"rscc.result.TOO_MANY_REQUESTS": "Too many requests.",
|
|
175
|
+
"rscc.result.INTERNAL_SERVER_ERROR": "An internal server error occurred.",
|
|
176
|
+
"rscc.web.missingParameter": "Required request parameter is missing: {0}",
|
|
177
|
+
"rscc.web.typeMismatch": "Request parameter has an invalid type: {0}",
|
|
178
|
+
"rscc.web.unreadableBody": "The request body could not be read. Check the JSON syntax and field types.",
|
|
179
|
+
"rscc.idempotency.conflict": "The request has already been processed or is in progress.",
|
|
180
|
+
"rscc.idempotency.invalidFormat": "Invalid Idempotency-Key format.",
|
|
181
|
+
"rscc.idempotency.required": "The Idempotency-Key header is required.",
|
|
182
|
+
"rscc.upload.size": "The upload exceeds the maximum allowed size.",
|
|
183
|
+
"rscc.upload.extension": "File type is not allowed: {0}",
|
|
184
|
+
"rscc.upload.contentMismatch": "File content does not match its extension.",
|
|
185
|
+
"rscc.query.sortField": "Field cannot be sorted: {0}",
|
|
186
|
+
"rscc.query.sortDirection": "Sort direction must be asc or desc: {0}",
|
|
187
|
+
"rscc.security.csrf": "The CSRF token is missing or invalid.",
|
|
188
|
+
"rscc.client.unreadableBody": "Failed to read the response body.",
|
|
189
|
+
"rscc.client.invalidJson": "The response body is not valid JSON.",
|
|
190
|
+
"rscc.client.httpError": "API error: {0} {1}"
|
|
191
|
+
});
|
|
192
|
+
var MESSAGES = Object.freeze({ ko: KO, en: EN });
|
|
193
|
+
var FIXED_KEY_LIST = [
|
|
194
|
+
"rscc.idempotency.conflict",
|
|
195
|
+
"rscc.idempotency.invalidFormat",
|
|
196
|
+
"rscc.idempotency.required",
|
|
197
|
+
"rscc.query.sortField",
|
|
198
|
+
"rscc.query.sortDirection",
|
|
199
|
+
"rscc.security.csrf"
|
|
200
|
+
];
|
|
201
|
+
var FIXED_KEYS = new Set(FIXED_KEY_LIST);
|
|
202
|
+
var FIXED_MESSAGE_KEYS = new Set(FIXED_KEY_LIST);
|
|
203
|
+
var PLACEHOLDER = /\{(\d+)\}/g;
|
|
204
|
+
function formatMessage(template, ...args) {
|
|
205
|
+
if (args.length === 0) return template;
|
|
206
|
+
return template.replace(PLACEHOLDER, (match, digits) => {
|
|
207
|
+
const index = Number(digits);
|
|
208
|
+
return index < args.length ? String(args[index]) : match;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function primarySubtag(tag) {
|
|
212
|
+
if (tag === void 0) return "";
|
|
213
|
+
const trimmed = tag.trim();
|
|
214
|
+
const dash = trimmed.indexOf("-");
|
|
215
|
+
return (dash < 0 ? trimmed : trimmed.slice(0, dash)).toLowerCase();
|
|
216
|
+
}
|
|
217
|
+
function builtinCatalog(language) {
|
|
218
|
+
return language === "ko" || language === "en" ? MESSAGES[language] : void 0;
|
|
219
|
+
}
|
|
220
|
+
function lookupBuiltin(language, key) {
|
|
221
|
+
const catalog = builtinCatalog(language);
|
|
222
|
+
return catalog !== void 0 && Object.prototype.hasOwnProperty.call(catalog, key) ? catalog[key] : void 0;
|
|
223
|
+
}
|
|
224
|
+
function lookupOverride(overrides, key, language) {
|
|
225
|
+
if (overrides === void 0) return void 0;
|
|
226
|
+
const value = typeof overrides === "function" ? overrides(key, language) : Object.prototype.hasOwnProperty.call(overrides, key) ? overrides[key] : void 0;
|
|
227
|
+
return typeof value === "string" && value !== "" && value !== key ? value : void 0;
|
|
228
|
+
}
|
|
229
|
+
function getMessage(key, options = {}) {
|
|
230
|
+
const defaultLanguage = primarySubtag(options.defaultLocale) || "ko";
|
|
231
|
+
const language = primarySubtag(options.locale) || defaultLanguage;
|
|
232
|
+
const args = options.args ?? [];
|
|
233
|
+
const template = (FIXED_KEYS.has(key) && (language === "ko" || language === "en") ? lookupBuiltin(language, key) : void 0) ?? lookupOverride(options.overrides, key, language) ?? lookupBuiltin(language, key) ?? lookupBuiltin(defaultLanguage, key) ?? lookupBuiltin("ko", key) ?? key;
|
|
234
|
+
return formatMessage(template, ...args);
|
|
235
|
+
}
|
|
236
|
+
var LANGUAGE_RANGE = /^(?:\*|[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*)$/;
|
|
237
|
+
var Q_VALUE = /^(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
238
|
+
function negotiateLanguage(acceptLanguage, supported = ["ko", "en"], fallback = "ko") {
|
|
239
|
+
if (acceptLanguage == null || acceptLanguage.trim() === "") return fallback;
|
|
240
|
+
const supportedSet = new Set(supported.map((s) => primarySubtag(s)).filter((s) => s !== ""));
|
|
241
|
+
const ranges = [];
|
|
242
|
+
for (const item of acceptLanguage.split(",")) {
|
|
243
|
+
const [rawTag = "", ...params] = item.split(";");
|
|
244
|
+
const tag = rawTag.trim();
|
|
245
|
+
if (!LANGUAGE_RANGE.test(tag)) continue;
|
|
246
|
+
let q = 1;
|
|
247
|
+
let valid = true;
|
|
248
|
+
for (const param of params) {
|
|
249
|
+
const eq = param.indexOf("=");
|
|
250
|
+
const name = (eq < 0 ? param : param.slice(0, eq)).trim().toLowerCase();
|
|
251
|
+
if (name !== "q") continue;
|
|
252
|
+
const value = eq < 0 ? "" : param.slice(eq + 1).trim();
|
|
253
|
+
const parsed = Number(value);
|
|
254
|
+
if (!Q_VALUE.test(value) || parsed < 0 || parsed > 1) {
|
|
255
|
+
valid = false;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
q = parsed;
|
|
259
|
+
}
|
|
260
|
+
if (!valid || q === 0) continue;
|
|
261
|
+
ranges.push({ tag, q });
|
|
262
|
+
}
|
|
263
|
+
ranges.sort((a, b) => b.q - a.q);
|
|
264
|
+
for (const { tag } of ranges) {
|
|
265
|
+
if (tag === "*") return fallback;
|
|
266
|
+
const language = primarySubtag(tag);
|
|
267
|
+
if (supportedSet.has(language)) return language;
|
|
268
|
+
}
|
|
269
|
+
return fallback;
|
|
270
|
+
}
|
|
271
|
+
|
|
35
272
|
// src/retry.ts
|
|
36
273
|
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
37
274
|
function isRetryableStatus(status) {
|
|
@@ -117,6 +354,7 @@ var ApiError = class extends Error {
|
|
|
117
354
|
super(args.message);
|
|
118
355
|
this.name = "ApiError";
|
|
119
356
|
this.code = args.code;
|
|
357
|
+
this.commonCode = toCommonResultCode(args.code, args.status);
|
|
120
358
|
this.status = args.status;
|
|
121
359
|
this.traceId = args.traceId;
|
|
122
360
|
this.data = args.data;
|
|
@@ -148,6 +386,8 @@ function isCommonResponse(value) {
|
|
|
148
386
|
}
|
|
149
387
|
function createApiClient(config) {
|
|
150
388
|
const traceIdHeader = config.traceIdHeader ?? "X-Trace-Id";
|
|
389
|
+
const clientMessage = (key, ...args) => getMessage(key, { locale: config.locale, args });
|
|
390
|
+
const acceptLanguage = config.sendAcceptLanguage && config.locale && config.locale.trim() !== "" ? config.locale.trim() : null;
|
|
151
391
|
let hookWarned = false;
|
|
152
392
|
const safeHook = (fn) => {
|
|
153
393
|
try {
|
|
@@ -163,6 +403,8 @@ function createApiClient(config) {
|
|
|
163
403
|
async function requestWithMeta(path, init = {}) {
|
|
164
404
|
const sentTraceId = generateTraceId();
|
|
165
405
|
const method = (init.method ?? "GET").toUpperCase();
|
|
406
|
+
const url = joinUrl(config.baseUrl, path);
|
|
407
|
+
const credentials = init.credentials ?? config.credentials;
|
|
166
408
|
const idempotencyHeader = config.idempotency?.header ?? "Idempotency-Key";
|
|
167
409
|
const idempotencyKey = config.idempotency && (config.idempotency.methods ?? DEFAULT_IDEMPOTENCY_METHODS).includes(method) ? generateIdempotencyKey() : null;
|
|
168
410
|
const attemptOnce = async () => {
|
|
@@ -174,12 +416,23 @@ function createApiClient(config) {
|
|
|
174
416
|
if (!headers.has("Content-Type") && typeof init.body === "string") {
|
|
175
417
|
headers.set("Content-Type", "application/json");
|
|
176
418
|
}
|
|
419
|
+
if (acceptLanguage !== null && !headers.has("Accept-Language")) {
|
|
420
|
+
headers.set("Accept-Language", acceptLanguage);
|
|
421
|
+
}
|
|
177
422
|
if (config.getToken && !headers.has("Authorization")) {
|
|
178
423
|
const token = config.getToken();
|
|
179
424
|
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
180
425
|
}
|
|
426
|
+
if (config.csrf) {
|
|
427
|
+
const csrfHeader = csrfHeaderFor(url, method, config.csrf);
|
|
428
|
+
if (csrfHeader && !headers.has(csrfHeader[0])) headers.set(csrfHeader[0], csrfHeader[1]);
|
|
429
|
+
}
|
|
181
430
|
const fetchFn = config.fetchImpl ?? globalThis.fetch;
|
|
182
|
-
const response = await fetchFn(
|
|
431
|
+
const response = await fetchFn(url, {
|
|
432
|
+
...init,
|
|
433
|
+
...credentials !== void 0 ? { credentials } : {},
|
|
434
|
+
headers
|
|
435
|
+
});
|
|
183
436
|
const traceId = response.headers.get(traceIdHeader) ?? sentTraceId;
|
|
184
437
|
let raw = null;
|
|
185
438
|
try {
|
|
@@ -189,7 +442,7 @@ function createApiClient(config) {
|
|
|
189
442
|
if (config.strictJson && response.ok) {
|
|
190
443
|
throw new ApiError({
|
|
191
444
|
code: "INVALID_JSON",
|
|
192
|
-
message: "
|
|
445
|
+
message: clientMessage("rscc.client.unreadableBody"),
|
|
193
446
|
status: response.status,
|
|
194
447
|
traceId,
|
|
195
448
|
retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After"))
|
|
@@ -208,7 +461,7 @@ function createApiClient(config) {
|
|
|
208
461
|
if (config.strictJson && response.ok) {
|
|
209
462
|
throw new ApiError({
|
|
210
463
|
code: "INVALID_JSON",
|
|
211
|
-
message: "
|
|
464
|
+
message: clientMessage("rscc.client.invalidJson"),
|
|
212
465
|
status: response.status,
|
|
213
466
|
traceId,
|
|
214
467
|
retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After")),
|
|
@@ -237,7 +490,7 @@ function createApiClient(config) {
|
|
|
237
490
|
}
|
|
238
491
|
if (!response.ok) {
|
|
239
492
|
const j2 = json;
|
|
240
|
-
const message = typeof j2?.message === "string" && j2.message || typeof j2?.error === "string" && j2.error ||
|
|
493
|
+
const message = typeof j2?.message === "string" && j2.message || typeof j2?.error === "string" && j2.error || clientMessage("rscc.client.httpError", response.status, response.statusText);
|
|
241
494
|
fail(String(response.status), message);
|
|
242
495
|
}
|
|
243
496
|
return { data: json, traceId, status: response.status, response };
|
|
@@ -295,13 +548,232 @@ function createApiClient(config) {
|
|
|
295
548
|
return { request, requestWithMeta };
|
|
296
549
|
}
|
|
297
550
|
|
|
551
|
+
// src/sseEvents.ts
|
|
552
|
+
var RETRY_PATTERN = /^[0-9]+$/;
|
|
553
|
+
function createParser(onEvent, onRetry, haltOnTrue, initialLastEventId) {
|
|
554
|
+
let pending = "";
|
|
555
|
+
let bomChecked = false;
|
|
556
|
+
let skipLeadingLf = false;
|
|
557
|
+
let data = "";
|
|
558
|
+
let eventType = "";
|
|
559
|
+
let idBuffer = initialLastEventId;
|
|
560
|
+
let lastEventId = initialLastEventId;
|
|
561
|
+
let retryMs = null;
|
|
562
|
+
let ended = false;
|
|
563
|
+
let halted = false;
|
|
564
|
+
const lineBreak = /[\r\n]/g;
|
|
565
|
+
const dispatch = () => {
|
|
566
|
+
lastEventId = idBuffer;
|
|
567
|
+
if (data === "") {
|
|
568
|
+
eventType = "";
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const event = {
|
|
572
|
+
event: eventType === "" ? "message" : eventType,
|
|
573
|
+
data: data.endsWith("\n") ? data.slice(0, -1) : data,
|
|
574
|
+
id: lastEventId
|
|
575
|
+
};
|
|
576
|
+
data = "";
|
|
577
|
+
eventType = "";
|
|
578
|
+
if (onEvent(event) === true && haltOnTrue) halted = true;
|
|
579
|
+
};
|
|
580
|
+
const processLine = (line) => {
|
|
581
|
+
if (line === "") {
|
|
582
|
+
dispatch();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (line.charCodeAt(0) === 58) return;
|
|
586
|
+
const colon = line.indexOf(":");
|
|
587
|
+
let field;
|
|
588
|
+
let value;
|
|
589
|
+
if (colon === -1) {
|
|
590
|
+
field = line;
|
|
591
|
+
value = "";
|
|
592
|
+
} else {
|
|
593
|
+
field = line.slice(0, colon);
|
|
594
|
+
value = line.slice(colon + 1);
|
|
595
|
+
if (value.charCodeAt(0) === 32) value = value.slice(1);
|
|
596
|
+
}
|
|
597
|
+
switch (field) {
|
|
598
|
+
case "event":
|
|
599
|
+
eventType = value;
|
|
600
|
+
break;
|
|
601
|
+
case "data":
|
|
602
|
+
data += value + "\n";
|
|
603
|
+
break;
|
|
604
|
+
case "id":
|
|
605
|
+
if (!value.includes("\0")) idBuffer = value;
|
|
606
|
+
break;
|
|
607
|
+
case "retry":
|
|
608
|
+
if (RETRY_PATTERN.test(value)) {
|
|
609
|
+
retryMs = Number(value);
|
|
610
|
+
onRetry?.(retryMs);
|
|
611
|
+
}
|
|
612
|
+
break;
|
|
613
|
+
default:
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
const feed = (chunk) => {
|
|
618
|
+
if (ended) throw new Error("SSE \uD30C\uC11C\uAC00 \uC774\uBBF8 \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4(end \uC774\uD6C4 feed).");
|
|
619
|
+
if (halted || chunk === "") return;
|
|
620
|
+
let text = chunk;
|
|
621
|
+
if (!bomChecked) {
|
|
622
|
+
bomChecked = true;
|
|
623
|
+
if (text.charCodeAt(0) === 65279) text = text.slice(1);
|
|
624
|
+
}
|
|
625
|
+
if (skipLeadingLf) {
|
|
626
|
+
skipLeadingLf = false;
|
|
627
|
+
if (text.charCodeAt(0) === 10) text = text.slice(1);
|
|
628
|
+
}
|
|
629
|
+
let start = 0;
|
|
630
|
+
for (; ; ) {
|
|
631
|
+
lineBreak.lastIndex = start;
|
|
632
|
+
const match = lineBreak.exec(text);
|
|
633
|
+
if (match === null) break;
|
|
634
|
+
const at = match.index;
|
|
635
|
+
const line = pending + text.slice(start, at);
|
|
636
|
+
pending = "";
|
|
637
|
+
let next = at + 1;
|
|
638
|
+
if (text.charCodeAt(at) === 13) {
|
|
639
|
+
if (next < text.length) {
|
|
640
|
+
if (text.charCodeAt(next) === 10) next += 1;
|
|
641
|
+
} else {
|
|
642
|
+
skipLeadingLf = true;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
start = next;
|
|
646
|
+
processLine(line);
|
|
647
|
+
if (halted) return;
|
|
648
|
+
}
|
|
649
|
+
pending += text.slice(start);
|
|
650
|
+
};
|
|
651
|
+
const end = (options) => {
|
|
652
|
+
if (ended) return;
|
|
653
|
+
ended = true;
|
|
654
|
+
if (halted) return;
|
|
655
|
+
if (options?.dispatchIncomplete) {
|
|
656
|
+
if (pending !== "") processLine(pending);
|
|
657
|
+
if (!halted) dispatch();
|
|
658
|
+
}
|
|
659
|
+
pending = "";
|
|
660
|
+
data = "";
|
|
661
|
+
eventType = "";
|
|
662
|
+
};
|
|
663
|
+
return {
|
|
664
|
+
feed,
|
|
665
|
+
end,
|
|
666
|
+
get lastEventId() {
|
|
667
|
+
return lastEventId;
|
|
668
|
+
},
|
|
669
|
+
get retryMs() {
|
|
670
|
+
return retryMs;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function createSseEventParser(onEvent, onRetry, options) {
|
|
675
|
+
return createParser(onEvent, onRetry, false, options?.lastEventId ?? "");
|
|
676
|
+
}
|
|
677
|
+
function parseSseEvents(text) {
|
|
678
|
+
const events = [];
|
|
679
|
+
const parser = createSseEventParser((e) => {
|
|
680
|
+
events.push(e);
|
|
681
|
+
});
|
|
682
|
+
parser.feed(text);
|
|
683
|
+
parser.end();
|
|
684
|
+
return events;
|
|
685
|
+
}
|
|
686
|
+
async function readSseEvents(response, options) {
|
|
687
|
+
if (!response.body) {
|
|
688
|
+
throw new Error("SSE \uC751\uB2F5\uC5D0 body \uC2A4\uD2B8\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
689
|
+
}
|
|
690
|
+
const reader = response.body.getReader();
|
|
691
|
+
const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
692
|
+
let stopped = false;
|
|
693
|
+
const parser = createParser(
|
|
694
|
+
(event) => {
|
|
695
|
+
if (options.onEvent(event) === true) {
|
|
696
|
+
stopped = true;
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
return void 0;
|
|
700
|
+
},
|
|
701
|
+
options.onRetry,
|
|
702
|
+
true,
|
|
703
|
+
options.lastEventId ?? ""
|
|
704
|
+
);
|
|
705
|
+
const result = () => ({
|
|
706
|
+
lastEventId: parser.lastEventId,
|
|
707
|
+
retryMs: parser.retryMs,
|
|
708
|
+
stopped
|
|
709
|
+
});
|
|
710
|
+
try {
|
|
711
|
+
while (true) {
|
|
712
|
+
const { done, value } = await reader.read();
|
|
713
|
+
if (done) break;
|
|
714
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
715
|
+
if (stopped) {
|
|
716
|
+
await reader.cancel().catch(() => void 0);
|
|
717
|
+
return result();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
parser.feed(decoder.decode());
|
|
721
|
+
parser.end({ dispatchIncomplete: options.dispatchIncompleteAtEof === true });
|
|
722
|
+
return result();
|
|
723
|
+
} catch (error) {
|
|
724
|
+
await reader.cancel(error).catch(() => void 0);
|
|
725
|
+
throw error;
|
|
726
|
+
} finally {
|
|
727
|
+
reader.releaseLock();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
var CR_OR_LF = /[\r\n]/;
|
|
731
|
+
function formatSseEvent(data, options = {}) {
|
|
732
|
+
const { event, id, retry: retry2 } = options;
|
|
733
|
+
let out = "";
|
|
734
|
+
if (id !== void 0) {
|
|
735
|
+
if (CR_OR_LF.test(id) || id.includes("\0")) {
|
|
736
|
+
throw new Error("SSE id \uC5D0 CR\xB7LF\xB7NUL \uC744 \uB123\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
737
|
+
}
|
|
738
|
+
out += `id: ${id}
|
|
739
|
+
`;
|
|
740
|
+
}
|
|
741
|
+
if (event !== void 0 && event !== "") {
|
|
742
|
+
if (CR_OR_LF.test(event)) throw new Error("SSE event \uC5D0 CR\xB7LF \uB97C \uB123\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
743
|
+
out += `event: ${event}
|
|
744
|
+
`;
|
|
745
|
+
}
|
|
746
|
+
if (retry2 !== void 0) {
|
|
747
|
+
if (!Number.isSafeInteger(retry2) || retry2 < 0) {
|
|
748
|
+
throw new Error(`SSE retry \uB294 0 \uC774\uC0C1\uC758 \uC815\uC218(ms)\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${retry2}`);
|
|
749
|
+
}
|
|
750
|
+
out += `retry: ${retry2}
|
|
751
|
+
`;
|
|
752
|
+
}
|
|
753
|
+
for (const line of data.split(/\r\n|\r|\n/)) {
|
|
754
|
+
out += `data: ${line}
|
|
755
|
+
`;
|
|
756
|
+
}
|
|
757
|
+
return out + "\n";
|
|
758
|
+
}
|
|
759
|
+
function formatSseComment(text = "") {
|
|
760
|
+
if (CR_OR_LF.test(text)) throw new Error("SSE \uC8FC\uC11D\uC5D0 CR\xB7LF \uB97C \uB123\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
761
|
+
return text === "" ? ":\n\n" : `: ${text}
|
|
762
|
+
|
|
763
|
+
`;
|
|
764
|
+
}
|
|
765
|
+
|
|
298
766
|
// src/sse.ts
|
|
299
767
|
function parseSseFrame(frame) {
|
|
300
768
|
const dataLine = frame.split("\n").find((l) => l.startsWith("data:"))?.replace(/^data:\s*/, "");
|
|
301
|
-
if (dataLine === void 0
|
|
302
|
-
|
|
769
|
+
if (dataLine === void 0) return { type: "skip" };
|
|
770
|
+
return parseChatPayload(dataLine);
|
|
771
|
+
}
|
|
772
|
+
function parseChatPayload(data) {
|
|
773
|
+
if (data === "") return { type: "skip" };
|
|
774
|
+
if (data === "[DONE]") return { type: "done" };
|
|
303
775
|
try {
|
|
304
|
-
const obj = JSON.parse(
|
|
776
|
+
const obj = JSON.parse(data);
|
|
305
777
|
if (typeof obj.conversationId === "number") {
|
|
306
778
|
return { type: "conversationId", id: obj.conversationId };
|
|
307
779
|
}
|
|
@@ -314,16 +786,9 @@ function parseSseFrame(frame) {
|
|
|
314
786
|
}
|
|
315
787
|
return { type: "skip" };
|
|
316
788
|
}
|
|
317
|
-
|
|
318
|
-
if (!response.body) {
|
|
319
|
-
throw new Error("SSE \uC751\uB2F5\uC5D0 body \uC2A4\uD2B8\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
320
|
-
}
|
|
321
|
-
const reader = response.body.getReader();
|
|
322
|
-
const decoder = new TextDecoder();
|
|
323
|
-
let buffer = "";
|
|
789
|
+
function createChatDispatcher(callbacks) {
|
|
324
790
|
let errored = false;
|
|
325
|
-
|
|
326
|
-
const ev = parseSseFrame(frame);
|
|
791
|
+
return (ev) => {
|
|
327
792
|
switch (ev.type) {
|
|
328
793
|
case "conversationId":
|
|
329
794
|
callbacks.onConversationId?.(ev.id);
|
|
@@ -346,6 +811,16 @@ async function readSseStream(response, callbacks) {
|
|
|
346
811
|
}
|
|
347
812
|
return false;
|
|
348
813
|
};
|
|
814
|
+
}
|
|
815
|
+
async function readSseStream(response, callbacks) {
|
|
816
|
+
if (!response.body) {
|
|
817
|
+
throw new Error("SSE \uC751\uB2F5\uC5D0 body \uC2A4\uD2B8\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
818
|
+
}
|
|
819
|
+
const reader = response.body.getReader();
|
|
820
|
+
const decoder = new TextDecoder();
|
|
821
|
+
let buffer = "";
|
|
822
|
+
const deliver = createChatDispatcher(callbacks);
|
|
823
|
+
const dispatch = (frame) => deliver(parseSseFrame(frame));
|
|
349
824
|
try {
|
|
350
825
|
while (true) {
|
|
351
826
|
const { done, value } = await reader.read();
|
|
@@ -367,6 +842,14 @@ async function readSseStream(response, callbacks) {
|
|
|
367
842
|
reader.releaseLock();
|
|
368
843
|
}
|
|
369
844
|
}
|
|
845
|
+
async function readSseChatEvents(response, callbacks) {
|
|
846
|
+
const deliver = createChatDispatcher(callbacks);
|
|
847
|
+
await readSseEvents(response, {
|
|
848
|
+
onEvent: (event) => deliver(parseChatPayload(event.data.trim())),
|
|
849
|
+
// 레거시 readSseStream 의 EOF 잔여 버퍼 처리와 같게 — 미완성 이벤트도 디스패치
|
|
850
|
+
dispatchIncompleteAtEof: true
|
|
851
|
+
});
|
|
852
|
+
}
|
|
370
853
|
|
|
371
854
|
// src/jwt.ts
|
|
372
855
|
function decodeJwtPayload(token) {
|
|
@@ -998,25 +1481,22 @@ function extensionOf(fileName) {
|
|
|
998
1481
|
return fileName.slice(dot + 1).toLowerCase();
|
|
999
1482
|
}
|
|
1000
1483
|
function validateUpload(input) {
|
|
1484
|
+
const message = (key, ...args) => getMessage(key, { locale: input.locale, args, overrides: input.messages });
|
|
1001
1485
|
if (input.size > input.maxSizeBytes) {
|
|
1002
|
-
return { ok: false, reason: "size", message: "
|
|
1486
|
+
return { ok: false, reason: "size", message: message("rscc.upload.size") };
|
|
1003
1487
|
}
|
|
1004
1488
|
const ext = extensionOf(input.fileName);
|
|
1005
1489
|
const allowed = input.allowedExtensions.map(
|
|
1006
1490
|
(e) => (e.startsWith(".") ? e.slice(1) : e).toLowerCase()
|
|
1007
1491
|
);
|
|
1008
1492
|
if (ext === "" || !allowed.includes(ext)) {
|
|
1009
|
-
return { ok: false, reason: "extension", message:
|
|
1493
|
+
return { ok: false, reason: "extension", message: message("rscc.upload.extension", ext) };
|
|
1010
1494
|
}
|
|
1011
1495
|
const extra = input.extraMappings?.[ext];
|
|
1012
1496
|
const kinds = extra !== void 0 ? new Set(extra) : kindsForExtension(ext);
|
|
1013
1497
|
const kind = sniffFile(input.head);
|
|
1014
1498
|
if (kind === null || !kinds.has(kind)) {
|
|
1015
|
-
return {
|
|
1016
|
-
ok: false,
|
|
1017
|
-
reason: "content-mismatch",
|
|
1018
|
-
message: "\uD30C\uC77C \uB0B4\uC6A9\uC774 \uD655\uC7A5\uC790\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
|
|
1019
|
-
};
|
|
1499
|
+
return { ok: false, reason: "content-mismatch", message: message("rscc.upload.contentMismatch") };
|
|
1020
1500
|
}
|
|
1021
1501
|
return { ok: true };
|
|
1022
1502
|
}
|
|
@@ -1686,6 +2166,10 @@ export {
|
|
|
1686
2166
|
ApiError,
|
|
1687
2167
|
BulkheadFullError,
|
|
1688
2168
|
CircuitOpenError,
|
|
2169
|
+
DEFAULT_CSRF_COOKIE_NAME,
|
|
2170
|
+
DEFAULT_CSRF_HEADER_NAME,
|
|
2171
|
+
FIXED_MESSAGE_KEYS,
|
|
2172
|
+
MESSAGES,
|
|
1689
2173
|
ResultCode,
|
|
1690
2174
|
WEBHOOK_SIGNATURE_HEADER,
|
|
1691
2175
|
abbreviateAmount,
|
|
@@ -1703,18 +2187,26 @@ export {
|
|
|
1703
2187
|
createBusinessDays,
|
|
1704
2188
|
createCircuitBreaker,
|
|
1705
2189
|
createFeatureFlags,
|
|
2190
|
+
createSseEventParser,
|
|
1706
2191
|
createTokenBucket,
|
|
1707
2192
|
createTtlCache,
|
|
2193
|
+
csrfHeaderFor,
|
|
1708
2194
|
decodeJwtPayload,
|
|
1709
2195
|
decomposeHangul,
|
|
2196
|
+
formatMessage,
|
|
1710
2197
|
formatPhoneNumber,
|
|
2198
|
+
formatSseComment,
|
|
2199
|
+
formatSseEvent,
|
|
1711
2200
|
generateIdempotencyKey,
|
|
2201
|
+
getMessage,
|
|
1712
2202
|
getTokenExpiry,
|
|
1713
2203
|
isBulkResult,
|
|
1714
2204
|
isChosungQuery,
|
|
2205
|
+
isCommonResultCode,
|
|
1715
2206
|
isForeignerRrn,
|
|
1716
2207
|
isRetryableStatus,
|
|
1717
2208
|
isTokenExpired,
|
|
2209
|
+
isUnsafeMethod,
|
|
1718
2210
|
isValidBusinessNumber,
|
|
1719
2211
|
isValidCorporateNumber,
|
|
1720
2212
|
isValidRrn,
|
|
@@ -1726,15 +2218,22 @@ export {
|
|
|
1726
2218
|
maskPhone,
|
|
1727
2219
|
maskSecret,
|
|
1728
2220
|
matchesHangul,
|
|
2221
|
+
negotiateLanguage,
|
|
1729
2222
|
normalizeBusinessNumber,
|
|
1730
2223
|
normalizePhoneNumber,
|
|
1731
2224
|
normalizeRrn,
|
|
2225
|
+
parseChatPayload,
|
|
1732
2226
|
parseFlag,
|
|
1733
2227
|
parseRetryAfterMs,
|
|
2228
|
+
parseSseEvents,
|
|
1734
2229
|
parseSseFrame,
|
|
1735
2230
|
parseWireDateTime,
|
|
1736
2231
|
pickJosa,
|
|
2232
|
+
readCookie,
|
|
2233
|
+
readSseChatEvents,
|
|
2234
|
+
readSseEvents,
|
|
1737
2235
|
readSseStream,
|
|
2236
|
+
resultCodeForStatus,
|
|
1738
2237
|
retry,
|
|
1739
2238
|
rrnBirthDate,
|
|
1740
2239
|
rrnChecksumOkLegacy,
|
|
@@ -1743,6 +2242,7 @@ export {
|
|
|
1743
2242
|
sniffFile,
|
|
1744
2243
|
stripZone,
|
|
1745
2244
|
toChosung,
|
|
2245
|
+
toCommonResultCode,
|
|
1746
2246
|
toE164,
|
|
1747
2247
|
toFormalNotation,
|
|
1748
2248
|
toKoreanWords,
|