@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/dist/index.cjs CHANGED
@@ -23,6 +23,10 @@ __export(index_exports, {
23
23
  ApiError: () => ApiError,
24
24
  BulkheadFullError: () => BulkheadFullError,
25
25
  CircuitOpenError: () => CircuitOpenError,
26
+ DEFAULT_CSRF_COOKIE_NAME: () => DEFAULT_CSRF_COOKIE_NAME,
27
+ DEFAULT_CSRF_HEADER_NAME: () => DEFAULT_CSRF_HEADER_NAME,
28
+ FIXED_MESSAGE_KEYS: () => FIXED_MESSAGE_KEYS,
29
+ MESSAGES: () => MESSAGES,
26
30
  ResultCode: () => ResultCode,
27
31
  WEBHOOK_SIGNATURE_HEADER: () => WEBHOOK_SIGNATURE_HEADER,
28
32
  abbreviateAmount: () => abbreviateAmount,
@@ -40,18 +44,26 @@ __export(index_exports, {
40
44
  createBusinessDays: () => createBusinessDays,
41
45
  createCircuitBreaker: () => createCircuitBreaker,
42
46
  createFeatureFlags: () => createFeatureFlags,
47
+ createSseEventParser: () => createSseEventParser,
43
48
  createTokenBucket: () => createTokenBucket,
44
49
  createTtlCache: () => createTtlCache,
50
+ csrfHeaderFor: () => csrfHeaderFor,
45
51
  decodeJwtPayload: () => decodeJwtPayload,
46
52
  decomposeHangul: () => decomposeHangul,
53
+ formatMessage: () => formatMessage,
47
54
  formatPhoneNumber: () => formatPhoneNumber,
55
+ formatSseComment: () => formatSseComment,
56
+ formatSseEvent: () => formatSseEvent,
48
57
  generateIdempotencyKey: () => generateIdempotencyKey,
58
+ getMessage: () => getMessage,
49
59
  getTokenExpiry: () => getTokenExpiry,
50
60
  isBulkResult: () => isBulkResult,
51
61
  isChosungQuery: () => isChosungQuery,
62
+ isCommonResultCode: () => isCommonResultCode,
52
63
  isForeignerRrn: () => isForeignerRrn,
53
64
  isRetryableStatus: () => isRetryableStatus,
54
65
  isTokenExpired: () => isTokenExpired,
66
+ isUnsafeMethod: () => isUnsafeMethod,
55
67
  isValidBusinessNumber: () => isValidBusinessNumber,
56
68
  isValidCorporateNumber: () => isValidCorporateNumber,
57
69
  isValidRrn: () => isValidRrn,
@@ -63,15 +75,22 @@ __export(index_exports, {
63
75
  maskPhone: () => maskPhone,
64
76
  maskSecret: () => maskSecret,
65
77
  matchesHangul: () => matchesHangul,
78
+ negotiateLanguage: () => negotiateLanguage,
66
79
  normalizeBusinessNumber: () => normalizeBusinessNumber,
67
80
  normalizePhoneNumber: () => normalizePhoneNumber,
68
81
  normalizeRrn: () => normalizeRrn,
82
+ parseChatPayload: () => parseChatPayload,
69
83
  parseFlag: () => parseFlag,
70
84
  parseRetryAfterMs: () => parseRetryAfterMs,
85
+ parseSseEvents: () => parseSseEvents,
71
86
  parseSseFrame: () => parseSseFrame,
72
87
  parseWireDateTime: () => parseWireDateTime,
73
88
  pickJosa: () => pickJosa,
89
+ readCookie: () => readCookie,
90
+ readSseChatEvents: () => readSseChatEvents,
91
+ readSseEvents: () => readSseEvents,
74
92
  readSseStream: () => readSseStream,
93
+ resultCodeForStatus: () => resultCodeForStatus,
75
94
  retry: () => retry,
76
95
  rrnBirthDate: () => rrnBirthDate,
77
96
  rrnChecksumOkLegacy: () => rrnChecksumOkLegacy,
@@ -80,6 +99,7 @@ __export(index_exports, {
80
99
  sniffFile: () => sniffFile,
81
100
  stripZone: () => stripZone,
82
101
  toChosung: () => toChosung,
102
+ toCommonResultCode: () => toCommonResultCode,
83
103
  toE164: () => toE164,
84
104
  toFormalNotation: () => toFormalNotation,
85
105
  toKoreanWords: () => toKoreanWords,
@@ -112,6 +132,108 @@ var ResultCode = {
112
132
  INTERNAL_SERVER_ERROR: "500"
113
133
  };
114
134
 
135
+ // src/errorCodes.ts
136
+ var COMMON_CODES = new Set(Object.values(ResultCode));
137
+ function isCommonResultCode(code) {
138
+ return COMMON_CODES.has(code);
139
+ }
140
+ function resultCodeForStatus(status) {
141
+ const exact = String(status);
142
+ if (exact !== ResultCode.SUCCESS && COMMON_CODES.has(exact)) return exact;
143
+ if (status >= 400 && status < 500) return ResultCode.BAD_REQUEST;
144
+ return ResultCode.INTERNAL_SERVER_ERROR;
145
+ }
146
+ function toCommonResultCode(code, status) {
147
+ return isCommonResultCode(code) ? code : resultCodeForStatus(status);
148
+ }
149
+
150
+ // src/csrf.ts
151
+ var DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
152
+ var DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
153
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
154
+ var SENTINEL_ORIGIN = "http://rscc-csrf.invalid";
155
+ var ABSOLUTE_URL = /^([a-zA-Z][a-zA-Z\d+.-]*):\/\/([^/?#\\]*)/;
156
+ var HAS_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
157
+ var PROTOCOL_RELATIVE = /^[\\/]{2}/;
158
+ var DEFAULT_PORTS = { http: "80", https: "443", ws: "80", wss: "443" };
159
+ function readCookie(name, cookieString) {
160
+ const source = cookieString ?? documentCookie();
161
+ if (!source) return null;
162
+ for (const part of source.split(";")) {
163
+ const pair = part.trim();
164
+ const eq = pair.indexOf("=");
165
+ if (eq < 0) continue;
166
+ if (pair.slice(0, eq) === name) return pair.slice(eq + 1);
167
+ }
168
+ return null;
169
+ }
170
+ function isUnsafeMethod(method) {
171
+ return !SAFE_METHODS.has((method || "GET").toUpperCase());
172
+ }
173
+ function csrfHeaderFor(url, method, options) {
174
+ if (!isUnsafeMethod(method)) return null;
175
+ const opts = options === true || options === void 0 ? {} : options;
176
+ const cookieString = opts.readCookie ? opts.readCookie() : documentCookie();
177
+ if (!cookieString) return null;
178
+ const token = readCookie(opts.cookieName ?? DEFAULT_CSRF_COOKIE_NAME, cookieString);
179
+ if (!token) return null;
180
+ if (!isAllowedTarget(url, opts.allowedOrigins)) return null;
181
+ return [opts.headerName ?? DEFAULT_CSRF_HEADER_NAME, token];
182
+ }
183
+ function documentCookie() {
184
+ if (typeof document === "undefined") return null;
185
+ try {
186
+ return typeof document.cookie === "string" ? document.cookie : null;
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+ function currentLocation() {
192
+ if (typeof location === "undefined") return null;
193
+ try {
194
+ const { href, origin } = location;
195
+ return typeof href === "string" && typeof origin === "string" ? { href, origin } : null;
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+ function isAllowedTarget(url, allowedOrigins) {
201
+ const loc = currentLocation();
202
+ const base = loc ? loc.href : SENTINEL_ORIGIN;
203
+ const selfOrigin = loc ? loc.origin : SENTINEL_ORIGIN;
204
+ const target = originOf(url, base, selfOrigin);
205
+ if (target === null || target === "null") return false;
206
+ if (selfOrigin !== "null" && target === selfOrigin) return true;
207
+ if (!allowedOrigins || allowedOrigins.length === 0) return false;
208
+ return allowedOrigins.some((entry) => originOf(entry, base, selfOrigin) === target);
209
+ }
210
+ function originOf(url, base, baseOrigin) {
211
+ try {
212
+ if (typeof URL === "function") {
213
+ const origin = new URL(url, base).origin;
214
+ if (typeof origin === "string") return origin;
215
+ }
216
+ } catch {
217
+ }
218
+ return looseOriginOf(url, baseOrigin);
219
+ }
220
+ function looseOriginOf(url, baseOrigin) {
221
+ const trimmed = url.trim();
222
+ const m = ABSOLUTE_URL.exec(trimmed);
223
+ if (m) {
224
+ const scheme = m[1].toLowerCase();
225
+ const authority = m[2];
226
+ const hostPort = authority.slice(authority.lastIndexOf("@") + 1).toLowerCase();
227
+ const hp = /^(.*?)(?::(\d*))?$/.exec(hostPort);
228
+ const host = hp?.[1] ?? "";
229
+ const port = hp?.[2] ?? "";
230
+ if (!host) return null;
231
+ return port === "" || port === DEFAULT_PORTS[scheme] ? `${scheme}://${host}` : `${scheme}://${host}:${port}`;
232
+ }
233
+ if (PROTOCOL_RELATIVE.test(trimmed) || HAS_SCHEME.test(trimmed)) return null;
234
+ return baseOrigin;
235
+ }
236
+
115
237
  // src/idempotency.ts
116
238
  var FALLBACK_GROUPS = [8, 4, 4, 4, 12];
117
239
  function generateIdempotencyKey() {
@@ -124,6 +246,141 @@ function generateIdempotencyKey() {
124
246
  }).join("-");
125
247
  }
126
248
 
249
+ // src/messages.ts
250
+ var KO = Object.freeze({
251
+ "rscc.result.SUCCESS": "\uC694\uCCAD\uC774 \uC131\uACF5\uC801\uC73C\uB85C \uCC98\uB9AC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
252
+ "rscc.result.BAD_REQUEST": "\uC798\uBABB\uB41C \uC694\uCCAD\uC785\uB2C8\uB2E4.",
253
+ "rscc.result.UNAUTHORIZED": "\uC778\uC99D\uB418\uC9C0 \uC54A\uC740 \uC0AC\uC6A9\uC790\uC785\uB2C8\uB2E4.",
254
+ "rscc.result.FORBIDDEN": "\uC811\uADFC \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
255
+ "rscc.result.NOT_FOUND": "\uB9AC\uC18C\uC2A4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
256
+ "rscc.result.METHOD_NOT_ALLOWED": "\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC740 \uC694\uCCAD \uBC29\uC2DD\uC785\uB2C8\uB2E4.",
257
+ "rscc.result.CONFLICT": "\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uB9AC\uC18C\uC2A4\uC785\uB2C8\uB2E4.",
258
+ "rscc.result.UNSUPPORTED_MEDIA_TYPE": "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC694\uCCAD \uD615\uC2DD\uC785\uB2C8\uB2E4.",
259
+ "rscc.result.TOO_MANY_REQUESTS": "\uC694\uCCAD \uD55C\uB3C4\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.",
260
+ "rscc.result.INTERNAL_SERVER_ERROR": "\uC11C\uBC84 \uB0B4\uBD80 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.",
261
+ "rscc.web.missingParameter": "\uD544\uC218 \uC694\uCCAD \uD30C\uB77C\uBBF8\uD130\uAC00 \uB204\uB77D\uB418\uC5C8\uC2B5\uB2C8\uB2E4: {0}",
262
+ "rscc.web.typeMismatch": "\uC694\uCCAD \uD30C\uB77C\uBBF8\uD130 \uD0C0\uC785\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: {0}",
263
+ "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.",
264
+ "rscc.idempotency.conflict": "\uC774\uBBF8 \uCC98\uB9AC\uB418\uC5C8\uAC70\uB098 \uCC98\uB9AC \uC911\uC778 \uC694\uCCAD\uC785\uB2C8\uB2E4.",
265
+ "rscc.idempotency.invalidFormat": "Idempotency-Key \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
266
+ "rscc.idempotency.required": "Idempotency-Key \uD5E4\uB354\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
267
+ "rscc.upload.size": "\uC5C5\uB85C\uB4DC \uAC00\uB2A5\uD55C \uCD5C\uB300 \uD06C\uAE30\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.",
268
+ "rscc.upload.extension": "\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: {0}",
269
+ "rscc.upload.contentMismatch": "\uD30C\uC77C \uB0B4\uC6A9\uC774 \uD655\uC7A5\uC790\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
270
+ "rscc.query.sortField": "\uC815\uB82C\uD560 \uC218 \uC5C6\uB294 \uD544\uB4DC\uC785\uB2C8\uB2E4: {0}",
271
+ "rscc.query.sortDirection": "\uC815\uB82C \uBC29\uD5A5\uC740 asc|desc \uB9CC \uD5C8\uC6A9\uD569\uB2C8\uB2E4: {0}",
272
+ "rscc.security.csrf": "CSRF \uD1A0\uD070\uC774 \uC5C6\uAC70\uB098 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
273
+ "rscc.client.unreadableBody": "\uC751\uB2F5 \uBCF8\uBB38\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
274
+ "rscc.client.invalidJson": "\uC751\uB2F5 \uBCF8\uBB38\uC774 \uC720\uD6A8\uD55C JSON \uC774 \uC544\uB2D9\uB2C8\uB2E4.",
275
+ "rscc.client.httpError": "API \uC624\uB958: {0} {1}"
276
+ });
277
+ var EN = Object.freeze({
278
+ "rscc.result.SUCCESS": "The request was processed successfully.",
279
+ "rscc.result.BAD_REQUEST": "Bad request.",
280
+ "rscc.result.UNAUTHORIZED": "Authentication is required.",
281
+ "rscc.result.FORBIDDEN": "Access is denied.",
282
+ "rscc.result.NOT_FOUND": "Resource not found.",
283
+ "rscc.result.METHOD_NOT_ALLOWED": "Request method is not allowed.",
284
+ "rscc.result.CONFLICT": "The resource already exists.",
285
+ "rscc.result.UNSUPPORTED_MEDIA_TYPE": "Unsupported request content type.",
286
+ "rscc.result.TOO_MANY_REQUESTS": "Too many requests.",
287
+ "rscc.result.INTERNAL_SERVER_ERROR": "An internal server error occurred.",
288
+ "rscc.web.missingParameter": "Required request parameter is missing: {0}",
289
+ "rscc.web.typeMismatch": "Request parameter has an invalid type: {0}",
290
+ "rscc.web.unreadableBody": "The request body could not be read. Check the JSON syntax and field types.",
291
+ "rscc.idempotency.conflict": "The request has already been processed or is in progress.",
292
+ "rscc.idempotency.invalidFormat": "Invalid Idempotency-Key format.",
293
+ "rscc.idempotency.required": "The Idempotency-Key header is required.",
294
+ "rscc.upload.size": "The upload exceeds the maximum allowed size.",
295
+ "rscc.upload.extension": "File type is not allowed: {0}",
296
+ "rscc.upload.contentMismatch": "File content does not match its extension.",
297
+ "rscc.query.sortField": "Field cannot be sorted: {0}",
298
+ "rscc.query.sortDirection": "Sort direction must be asc or desc: {0}",
299
+ "rscc.security.csrf": "The CSRF token is missing or invalid.",
300
+ "rscc.client.unreadableBody": "Failed to read the response body.",
301
+ "rscc.client.invalidJson": "The response body is not valid JSON.",
302
+ "rscc.client.httpError": "API error: {0} {1}"
303
+ });
304
+ var MESSAGES = Object.freeze({ ko: KO, en: EN });
305
+ var FIXED_KEY_LIST = [
306
+ "rscc.idempotency.conflict",
307
+ "rscc.idempotency.invalidFormat",
308
+ "rscc.idempotency.required",
309
+ "rscc.query.sortField",
310
+ "rscc.query.sortDirection",
311
+ "rscc.security.csrf"
312
+ ];
313
+ var FIXED_KEYS = new Set(FIXED_KEY_LIST);
314
+ var FIXED_MESSAGE_KEYS = new Set(FIXED_KEY_LIST);
315
+ var PLACEHOLDER = /\{(\d+)\}/g;
316
+ function formatMessage(template, ...args) {
317
+ if (args.length === 0) return template;
318
+ return template.replace(PLACEHOLDER, (match, digits) => {
319
+ const index = Number(digits);
320
+ return index < args.length ? String(args[index]) : match;
321
+ });
322
+ }
323
+ function primarySubtag(tag) {
324
+ if (tag === void 0) return "";
325
+ const trimmed = tag.trim();
326
+ const dash = trimmed.indexOf("-");
327
+ return (dash < 0 ? trimmed : trimmed.slice(0, dash)).toLowerCase();
328
+ }
329
+ function builtinCatalog(language) {
330
+ return language === "ko" || language === "en" ? MESSAGES[language] : void 0;
331
+ }
332
+ function lookupBuiltin(language, key) {
333
+ const catalog = builtinCatalog(language);
334
+ return catalog !== void 0 && Object.prototype.hasOwnProperty.call(catalog, key) ? catalog[key] : void 0;
335
+ }
336
+ function lookupOverride(overrides, key, language) {
337
+ if (overrides === void 0) return void 0;
338
+ const value = typeof overrides === "function" ? overrides(key, language) : Object.prototype.hasOwnProperty.call(overrides, key) ? overrides[key] : void 0;
339
+ return typeof value === "string" && value !== "" && value !== key ? value : void 0;
340
+ }
341
+ function getMessage(key, options = {}) {
342
+ const defaultLanguage = primarySubtag(options.defaultLocale) || "ko";
343
+ const language = primarySubtag(options.locale) || defaultLanguage;
344
+ const args = options.args ?? [];
345
+ 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;
346
+ return formatMessage(template, ...args);
347
+ }
348
+ var LANGUAGE_RANGE = /^(?:\*|[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*)$/;
349
+ var Q_VALUE = /^(?:\d+(?:\.\d*)?|\.\d+)$/;
350
+ function negotiateLanguage(acceptLanguage, supported = ["ko", "en"], fallback = "ko") {
351
+ if (acceptLanguage == null || acceptLanguage.trim() === "") return fallback;
352
+ const supportedSet = new Set(supported.map((s) => primarySubtag(s)).filter((s) => s !== ""));
353
+ const ranges = [];
354
+ for (const item of acceptLanguage.split(",")) {
355
+ const [rawTag = "", ...params] = item.split(";");
356
+ const tag = rawTag.trim();
357
+ if (!LANGUAGE_RANGE.test(tag)) continue;
358
+ let q = 1;
359
+ let valid = true;
360
+ for (const param of params) {
361
+ const eq = param.indexOf("=");
362
+ const name = (eq < 0 ? param : param.slice(0, eq)).trim().toLowerCase();
363
+ if (name !== "q") continue;
364
+ const value = eq < 0 ? "" : param.slice(eq + 1).trim();
365
+ const parsed = Number(value);
366
+ if (!Q_VALUE.test(value) || parsed < 0 || parsed > 1) {
367
+ valid = false;
368
+ break;
369
+ }
370
+ q = parsed;
371
+ }
372
+ if (!valid || q === 0) continue;
373
+ ranges.push({ tag, q });
374
+ }
375
+ ranges.sort((a, b) => b.q - a.q);
376
+ for (const { tag } of ranges) {
377
+ if (tag === "*") return fallback;
378
+ const language = primarySubtag(tag);
379
+ if (supportedSet.has(language)) return language;
380
+ }
381
+ return fallback;
382
+ }
383
+
127
384
  // src/retry.ts
128
385
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
129
386
  function isRetryableStatus(status) {
@@ -209,6 +466,7 @@ var ApiError = class extends Error {
209
466
  super(args.message);
210
467
  this.name = "ApiError";
211
468
  this.code = args.code;
469
+ this.commonCode = toCommonResultCode(args.code, args.status);
212
470
  this.status = args.status;
213
471
  this.traceId = args.traceId;
214
472
  this.data = args.data;
@@ -240,6 +498,8 @@ function isCommonResponse(value) {
240
498
  }
241
499
  function createApiClient(config) {
242
500
  const traceIdHeader = config.traceIdHeader ?? "X-Trace-Id";
501
+ const clientMessage = (key, ...args) => getMessage(key, { locale: config.locale, args });
502
+ const acceptLanguage = config.sendAcceptLanguage && config.locale && config.locale.trim() !== "" ? config.locale.trim() : null;
243
503
  let hookWarned = false;
244
504
  const safeHook = (fn) => {
245
505
  try {
@@ -255,6 +515,8 @@ function createApiClient(config) {
255
515
  async function requestWithMeta(path, init = {}) {
256
516
  const sentTraceId = generateTraceId();
257
517
  const method = (init.method ?? "GET").toUpperCase();
518
+ const url = joinUrl(config.baseUrl, path);
519
+ const credentials = init.credentials ?? config.credentials;
258
520
  const idempotencyHeader = config.idempotency?.header ?? "Idempotency-Key";
259
521
  const idempotencyKey = config.idempotency && (config.idempotency.methods ?? DEFAULT_IDEMPOTENCY_METHODS).includes(method) ? generateIdempotencyKey() : null;
260
522
  const attemptOnce = async () => {
@@ -266,12 +528,23 @@ function createApiClient(config) {
266
528
  if (!headers.has("Content-Type") && typeof init.body === "string") {
267
529
  headers.set("Content-Type", "application/json");
268
530
  }
531
+ if (acceptLanguage !== null && !headers.has("Accept-Language")) {
532
+ headers.set("Accept-Language", acceptLanguage);
533
+ }
269
534
  if (config.getToken && !headers.has("Authorization")) {
270
535
  const token = config.getToken();
271
536
  if (token) headers.set("Authorization", `Bearer ${token}`);
272
537
  }
538
+ if (config.csrf) {
539
+ const csrfHeader = csrfHeaderFor(url, method, config.csrf);
540
+ if (csrfHeader && !headers.has(csrfHeader[0])) headers.set(csrfHeader[0], csrfHeader[1]);
541
+ }
273
542
  const fetchFn = config.fetchImpl ?? globalThis.fetch;
274
- const response = await fetchFn(joinUrl(config.baseUrl, path), { ...init, headers });
543
+ const response = await fetchFn(url, {
544
+ ...init,
545
+ ...credentials !== void 0 ? { credentials } : {},
546
+ headers
547
+ });
275
548
  const traceId = response.headers.get(traceIdHeader) ?? sentTraceId;
276
549
  let raw = null;
277
550
  try {
@@ -281,7 +554,7 @@ function createApiClient(config) {
281
554
  if (config.strictJson && response.ok) {
282
555
  throw new ApiError({
283
556
  code: "INVALID_JSON",
284
- message: "\uC751\uB2F5 \uBCF8\uBB38\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
557
+ message: clientMessage("rscc.client.unreadableBody"),
285
558
  status: response.status,
286
559
  traceId,
287
560
  retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After"))
@@ -300,7 +573,7 @@ function createApiClient(config) {
300
573
  if (config.strictJson && response.ok) {
301
574
  throw new ApiError({
302
575
  code: "INVALID_JSON",
303
- message: "\uC751\uB2F5 \uBCF8\uBB38\uC774 \uC720\uD6A8\uD55C JSON \uC774 \uC544\uB2D9\uB2C8\uB2E4.",
576
+ message: clientMessage("rscc.client.invalidJson"),
304
577
  status: response.status,
305
578
  traceId,
306
579
  retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After")),
@@ -329,7 +602,7 @@ function createApiClient(config) {
329
602
  }
330
603
  if (!response.ok) {
331
604
  const j2 = json;
332
- const message = typeof j2?.message === "string" && j2.message || typeof j2?.error === "string" && j2.error || `API \uC624\uB958: ${response.status} ${response.statusText}`;
605
+ const message = typeof j2?.message === "string" && j2.message || typeof j2?.error === "string" && j2.error || clientMessage("rscc.client.httpError", response.status, response.statusText);
333
606
  fail(String(response.status), message);
334
607
  }
335
608
  return { data: json, traceId, status: response.status, response };
@@ -387,13 +660,232 @@ function createApiClient(config) {
387
660
  return { request, requestWithMeta };
388
661
  }
389
662
 
663
+ // src/sseEvents.ts
664
+ var RETRY_PATTERN = /^[0-9]+$/;
665
+ function createParser(onEvent, onRetry, haltOnTrue, initialLastEventId) {
666
+ let pending = "";
667
+ let bomChecked = false;
668
+ let skipLeadingLf = false;
669
+ let data = "";
670
+ let eventType = "";
671
+ let idBuffer = initialLastEventId;
672
+ let lastEventId = initialLastEventId;
673
+ let retryMs = null;
674
+ let ended = false;
675
+ let halted = false;
676
+ const lineBreak = /[\r\n]/g;
677
+ const dispatch = () => {
678
+ lastEventId = idBuffer;
679
+ if (data === "") {
680
+ eventType = "";
681
+ return;
682
+ }
683
+ const event = {
684
+ event: eventType === "" ? "message" : eventType,
685
+ data: data.endsWith("\n") ? data.slice(0, -1) : data,
686
+ id: lastEventId
687
+ };
688
+ data = "";
689
+ eventType = "";
690
+ if (onEvent(event) === true && haltOnTrue) halted = true;
691
+ };
692
+ const processLine = (line) => {
693
+ if (line === "") {
694
+ dispatch();
695
+ return;
696
+ }
697
+ if (line.charCodeAt(0) === 58) return;
698
+ const colon = line.indexOf(":");
699
+ let field;
700
+ let value;
701
+ if (colon === -1) {
702
+ field = line;
703
+ value = "";
704
+ } else {
705
+ field = line.slice(0, colon);
706
+ value = line.slice(colon + 1);
707
+ if (value.charCodeAt(0) === 32) value = value.slice(1);
708
+ }
709
+ switch (field) {
710
+ case "event":
711
+ eventType = value;
712
+ break;
713
+ case "data":
714
+ data += value + "\n";
715
+ break;
716
+ case "id":
717
+ if (!value.includes("\0")) idBuffer = value;
718
+ break;
719
+ case "retry":
720
+ if (RETRY_PATTERN.test(value)) {
721
+ retryMs = Number(value);
722
+ onRetry?.(retryMs);
723
+ }
724
+ break;
725
+ default:
726
+ break;
727
+ }
728
+ };
729
+ const feed = (chunk) => {
730
+ if (ended) throw new Error("SSE \uD30C\uC11C\uAC00 \uC774\uBBF8 \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4(end \uC774\uD6C4 feed).");
731
+ if (halted || chunk === "") return;
732
+ let text = chunk;
733
+ if (!bomChecked) {
734
+ bomChecked = true;
735
+ if (text.charCodeAt(0) === 65279) text = text.slice(1);
736
+ }
737
+ if (skipLeadingLf) {
738
+ skipLeadingLf = false;
739
+ if (text.charCodeAt(0) === 10) text = text.slice(1);
740
+ }
741
+ let start = 0;
742
+ for (; ; ) {
743
+ lineBreak.lastIndex = start;
744
+ const match = lineBreak.exec(text);
745
+ if (match === null) break;
746
+ const at = match.index;
747
+ const line = pending + text.slice(start, at);
748
+ pending = "";
749
+ let next = at + 1;
750
+ if (text.charCodeAt(at) === 13) {
751
+ if (next < text.length) {
752
+ if (text.charCodeAt(next) === 10) next += 1;
753
+ } else {
754
+ skipLeadingLf = true;
755
+ }
756
+ }
757
+ start = next;
758
+ processLine(line);
759
+ if (halted) return;
760
+ }
761
+ pending += text.slice(start);
762
+ };
763
+ const end = (options) => {
764
+ if (ended) return;
765
+ ended = true;
766
+ if (halted) return;
767
+ if (options?.dispatchIncomplete) {
768
+ if (pending !== "") processLine(pending);
769
+ if (!halted) dispatch();
770
+ }
771
+ pending = "";
772
+ data = "";
773
+ eventType = "";
774
+ };
775
+ return {
776
+ feed,
777
+ end,
778
+ get lastEventId() {
779
+ return lastEventId;
780
+ },
781
+ get retryMs() {
782
+ return retryMs;
783
+ }
784
+ };
785
+ }
786
+ function createSseEventParser(onEvent, onRetry, options) {
787
+ return createParser(onEvent, onRetry, false, options?.lastEventId ?? "");
788
+ }
789
+ function parseSseEvents(text) {
790
+ const events = [];
791
+ const parser = createSseEventParser((e) => {
792
+ events.push(e);
793
+ });
794
+ parser.feed(text);
795
+ parser.end();
796
+ return events;
797
+ }
798
+ async function readSseEvents(response, options) {
799
+ if (!response.body) {
800
+ throw new Error("SSE \uC751\uB2F5\uC5D0 body \uC2A4\uD2B8\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
801
+ }
802
+ const reader = response.body.getReader();
803
+ const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
804
+ let stopped = false;
805
+ const parser = createParser(
806
+ (event) => {
807
+ if (options.onEvent(event) === true) {
808
+ stopped = true;
809
+ return true;
810
+ }
811
+ return void 0;
812
+ },
813
+ options.onRetry,
814
+ true,
815
+ options.lastEventId ?? ""
816
+ );
817
+ const result = () => ({
818
+ lastEventId: parser.lastEventId,
819
+ retryMs: parser.retryMs,
820
+ stopped
821
+ });
822
+ try {
823
+ while (true) {
824
+ const { done, value } = await reader.read();
825
+ if (done) break;
826
+ parser.feed(decoder.decode(value, { stream: true }));
827
+ if (stopped) {
828
+ await reader.cancel().catch(() => void 0);
829
+ return result();
830
+ }
831
+ }
832
+ parser.feed(decoder.decode());
833
+ parser.end({ dispatchIncomplete: options.dispatchIncompleteAtEof === true });
834
+ return result();
835
+ } catch (error) {
836
+ await reader.cancel(error).catch(() => void 0);
837
+ throw error;
838
+ } finally {
839
+ reader.releaseLock();
840
+ }
841
+ }
842
+ var CR_OR_LF = /[\r\n]/;
843
+ function formatSseEvent(data, options = {}) {
844
+ const { event, id, retry: retry2 } = options;
845
+ let out = "";
846
+ if (id !== void 0) {
847
+ if (CR_OR_LF.test(id) || id.includes("\0")) {
848
+ throw new Error("SSE id \uC5D0 CR\xB7LF\xB7NUL \uC744 \uB123\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
849
+ }
850
+ out += `id: ${id}
851
+ `;
852
+ }
853
+ if (event !== void 0 && event !== "") {
854
+ if (CR_OR_LF.test(event)) throw new Error("SSE event \uC5D0 CR\xB7LF \uB97C \uB123\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
855
+ out += `event: ${event}
856
+ `;
857
+ }
858
+ if (retry2 !== void 0) {
859
+ if (!Number.isSafeInteger(retry2) || retry2 < 0) {
860
+ throw new Error(`SSE retry \uB294 0 \uC774\uC0C1\uC758 \uC815\uC218(ms)\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${retry2}`);
861
+ }
862
+ out += `retry: ${retry2}
863
+ `;
864
+ }
865
+ for (const line of data.split(/\r\n|\r|\n/)) {
866
+ out += `data: ${line}
867
+ `;
868
+ }
869
+ return out + "\n";
870
+ }
871
+ function formatSseComment(text = "") {
872
+ if (CR_OR_LF.test(text)) throw new Error("SSE \uC8FC\uC11D\uC5D0 CR\xB7LF \uB97C \uB123\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
873
+ return text === "" ? ":\n\n" : `: ${text}
874
+
875
+ `;
876
+ }
877
+
390
878
  // src/sse.ts
391
879
  function parseSseFrame(frame) {
392
880
  const dataLine = frame.split("\n").find((l) => l.startsWith("data:"))?.replace(/^data:\s*/, "");
393
- if (dataLine === void 0 || dataLine === "") return { type: "skip" };
394
- if (dataLine === "[DONE]") return { type: "done" };
881
+ if (dataLine === void 0) return { type: "skip" };
882
+ return parseChatPayload(dataLine);
883
+ }
884
+ function parseChatPayload(data) {
885
+ if (data === "") return { type: "skip" };
886
+ if (data === "[DONE]") return { type: "done" };
395
887
  try {
396
- const obj = JSON.parse(dataLine);
888
+ const obj = JSON.parse(data);
397
889
  if (typeof obj.conversationId === "number") {
398
890
  return { type: "conversationId", id: obj.conversationId };
399
891
  }
@@ -406,16 +898,9 @@ function parseSseFrame(frame) {
406
898
  }
407
899
  return { type: "skip" };
408
900
  }
409
- async function readSseStream(response, callbacks) {
410
- if (!response.body) {
411
- throw new Error("SSE \uC751\uB2F5\uC5D0 body \uC2A4\uD2B8\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
412
- }
413
- const reader = response.body.getReader();
414
- const decoder = new TextDecoder();
415
- let buffer = "";
901
+ function createChatDispatcher(callbacks) {
416
902
  let errored = false;
417
- const dispatch = (frame) => {
418
- const ev = parseSseFrame(frame);
903
+ return (ev) => {
419
904
  switch (ev.type) {
420
905
  case "conversationId":
421
906
  callbacks.onConversationId?.(ev.id);
@@ -438,6 +923,16 @@ async function readSseStream(response, callbacks) {
438
923
  }
439
924
  return false;
440
925
  };
926
+ }
927
+ async function readSseStream(response, callbacks) {
928
+ if (!response.body) {
929
+ throw new Error("SSE \uC751\uB2F5\uC5D0 body \uC2A4\uD2B8\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
930
+ }
931
+ const reader = response.body.getReader();
932
+ const decoder = new TextDecoder();
933
+ let buffer = "";
934
+ const deliver = createChatDispatcher(callbacks);
935
+ const dispatch = (frame) => deliver(parseSseFrame(frame));
441
936
  try {
442
937
  while (true) {
443
938
  const { done, value } = await reader.read();
@@ -459,6 +954,14 @@ async function readSseStream(response, callbacks) {
459
954
  reader.releaseLock();
460
955
  }
461
956
  }
957
+ async function readSseChatEvents(response, callbacks) {
958
+ const deliver = createChatDispatcher(callbacks);
959
+ await readSseEvents(response, {
960
+ onEvent: (event) => deliver(parseChatPayload(event.data.trim())),
961
+ // 레거시 readSseStream 의 EOF 잔여 버퍼 처리와 같게 — 미완성 이벤트도 디스패치
962
+ dispatchIncompleteAtEof: true
963
+ });
964
+ }
462
965
 
463
966
  // src/jwt.ts
464
967
  function decodeJwtPayload(token) {
@@ -1090,25 +1593,22 @@ function extensionOf(fileName) {
1090
1593
  return fileName.slice(dot + 1).toLowerCase();
1091
1594
  }
1092
1595
  function validateUpload(input) {
1596
+ const message = (key, ...args) => getMessage(key, { locale: input.locale, args, overrides: input.messages });
1093
1597
  if (input.size > input.maxSizeBytes) {
1094
- return { ok: false, reason: "size", message: "\uC5C5\uB85C\uB4DC \uAC00\uB2A5\uD55C \uCD5C\uB300 \uD06C\uAE30\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4." };
1598
+ return { ok: false, reason: "size", message: message("rscc.upload.size") };
1095
1599
  }
1096
1600
  const ext = extensionOf(input.fileName);
1097
1601
  const allowed = input.allowedExtensions.map(
1098
1602
  (e) => (e.startsWith(".") ? e.slice(1) : e).toLowerCase()
1099
1603
  );
1100
1604
  if (ext === "" || !allowed.includes(ext)) {
1101
- return { ok: false, reason: "extension", message: `\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${ext}` };
1605
+ return { ok: false, reason: "extension", message: message("rscc.upload.extension", ext) };
1102
1606
  }
1103
1607
  const extra = input.extraMappings?.[ext];
1104
1608
  const kinds = extra !== void 0 ? new Set(extra) : kindsForExtension(ext);
1105
1609
  const kind = sniffFile(input.head);
1106
1610
  if (kind === null || !kinds.has(kind)) {
1107
- return {
1108
- ok: false,
1109
- reason: "content-mismatch",
1110
- message: "\uD30C\uC77C \uB0B4\uC6A9\uC774 \uD655\uC7A5\uC790\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
1111
- };
1611
+ return { ok: false, reason: "content-mismatch", message: message("rscc.upload.contentMismatch") };
1112
1612
  }
1113
1613
  return { ok: true };
1114
1614
  }
@@ -1779,6 +2279,10 @@ function matchesHangul(query, target) {
1779
2279
  ApiError,
1780
2280
  BulkheadFullError,
1781
2281
  CircuitOpenError,
2282
+ DEFAULT_CSRF_COOKIE_NAME,
2283
+ DEFAULT_CSRF_HEADER_NAME,
2284
+ FIXED_MESSAGE_KEYS,
2285
+ MESSAGES,
1782
2286
  ResultCode,
1783
2287
  WEBHOOK_SIGNATURE_HEADER,
1784
2288
  abbreviateAmount,
@@ -1796,18 +2300,26 @@ function matchesHangul(query, target) {
1796
2300
  createBusinessDays,
1797
2301
  createCircuitBreaker,
1798
2302
  createFeatureFlags,
2303
+ createSseEventParser,
1799
2304
  createTokenBucket,
1800
2305
  createTtlCache,
2306
+ csrfHeaderFor,
1801
2307
  decodeJwtPayload,
1802
2308
  decomposeHangul,
2309
+ formatMessage,
1803
2310
  formatPhoneNumber,
2311
+ formatSseComment,
2312
+ formatSseEvent,
1804
2313
  generateIdempotencyKey,
2314
+ getMessage,
1805
2315
  getTokenExpiry,
1806
2316
  isBulkResult,
1807
2317
  isChosungQuery,
2318
+ isCommonResultCode,
1808
2319
  isForeignerRrn,
1809
2320
  isRetryableStatus,
1810
2321
  isTokenExpired,
2322
+ isUnsafeMethod,
1811
2323
  isValidBusinessNumber,
1812
2324
  isValidCorporateNumber,
1813
2325
  isValidRrn,
@@ -1819,15 +2331,22 @@ function matchesHangul(query, target) {
1819
2331
  maskPhone,
1820
2332
  maskSecret,
1821
2333
  matchesHangul,
2334
+ negotiateLanguage,
1822
2335
  normalizeBusinessNumber,
1823
2336
  normalizePhoneNumber,
1824
2337
  normalizeRrn,
2338
+ parseChatPayload,
1825
2339
  parseFlag,
1826
2340
  parseRetryAfterMs,
2341
+ parseSseEvents,
1827
2342
  parseSseFrame,
1828
2343
  parseWireDateTime,
1829
2344
  pickJosa,
2345
+ readCookie,
2346
+ readSseChatEvents,
2347
+ readSseEvents,
1830
2348
  readSseStream,
2349
+ resultCodeForStatus,
1831
2350
  retry,
1832
2351
  rrnBirthDate,
1833
2352
  rrnChecksumOkLegacy,
@@ -1836,6 +2355,7 @@ function matchesHangul(query, target) {
1836
2355
  sniffFile,
1837
2356
  stripZone,
1838
2357
  toChosung,
2358
+ toCommonResultCode,
1839
2359
  toE164,
1840
2360
  toFormalNotation,
1841
2361
  toKoreanWords,