@rscc/common-core 0.4.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
@@ -25,6 +25,8 @@ __export(index_exports, {
25
25
  CircuitOpenError: () => CircuitOpenError,
26
26
  DEFAULT_CSRF_COOKIE_NAME: () => DEFAULT_CSRF_COOKIE_NAME,
27
27
  DEFAULT_CSRF_HEADER_NAME: () => DEFAULT_CSRF_HEADER_NAME,
28
+ FIXED_MESSAGE_KEYS: () => FIXED_MESSAGE_KEYS,
29
+ MESSAGES: () => MESSAGES,
28
30
  ResultCode: () => ResultCode,
29
31
  WEBHOOK_SIGNATURE_HEADER: () => WEBHOOK_SIGNATURE_HEADER,
30
32
  abbreviateAmount: () => abbreviateAmount,
@@ -42,16 +44,22 @@ __export(index_exports, {
42
44
  createBusinessDays: () => createBusinessDays,
43
45
  createCircuitBreaker: () => createCircuitBreaker,
44
46
  createFeatureFlags: () => createFeatureFlags,
47
+ createSseEventParser: () => createSseEventParser,
45
48
  createTokenBucket: () => createTokenBucket,
46
49
  createTtlCache: () => createTtlCache,
47
50
  csrfHeaderFor: () => csrfHeaderFor,
48
51
  decodeJwtPayload: () => decodeJwtPayload,
49
52
  decomposeHangul: () => decomposeHangul,
53
+ formatMessage: () => formatMessage,
50
54
  formatPhoneNumber: () => formatPhoneNumber,
55
+ formatSseComment: () => formatSseComment,
56
+ formatSseEvent: () => formatSseEvent,
51
57
  generateIdempotencyKey: () => generateIdempotencyKey,
58
+ getMessage: () => getMessage,
52
59
  getTokenExpiry: () => getTokenExpiry,
53
60
  isBulkResult: () => isBulkResult,
54
61
  isChosungQuery: () => isChosungQuery,
62
+ isCommonResultCode: () => isCommonResultCode,
55
63
  isForeignerRrn: () => isForeignerRrn,
56
64
  isRetryableStatus: () => isRetryableStatus,
57
65
  isTokenExpired: () => isTokenExpired,
@@ -67,16 +75,22 @@ __export(index_exports, {
67
75
  maskPhone: () => maskPhone,
68
76
  maskSecret: () => maskSecret,
69
77
  matchesHangul: () => matchesHangul,
78
+ negotiateLanguage: () => negotiateLanguage,
70
79
  normalizeBusinessNumber: () => normalizeBusinessNumber,
71
80
  normalizePhoneNumber: () => normalizePhoneNumber,
72
81
  normalizeRrn: () => normalizeRrn,
82
+ parseChatPayload: () => parseChatPayload,
73
83
  parseFlag: () => parseFlag,
74
84
  parseRetryAfterMs: () => parseRetryAfterMs,
85
+ parseSseEvents: () => parseSseEvents,
75
86
  parseSseFrame: () => parseSseFrame,
76
87
  parseWireDateTime: () => parseWireDateTime,
77
88
  pickJosa: () => pickJosa,
78
89
  readCookie: () => readCookie,
90
+ readSseChatEvents: () => readSseChatEvents,
91
+ readSseEvents: () => readSseEvents,
79
92
  readSseStream: () => readSseStream,
93
+ resultCodeForStatus: () => resultCodeForStatus,
80
94
  retry: () => retry,
81
95
  rrnBirthDate: () => rrnBirthDate,
82
96
  rrnChecksumOkLegacy: () => rrnChecksumOkLegacy,
@@ -85,6 +99,7 @@ __export(index_exports, {
85
99
  sniffFile: () => sniffFile,
86
100
  stripZone: () => stripZone,
87
101
  toChosung: () => toChosung,
102
+ toCommonResultCode: () => toCommonResultCode,
88
103
  toE164: () => toE164,
89
104
  toFormalNotation: () => toFormalNotation,
90
105
  toKoreanWords: () => toKoreanWords,
@@ -117,6 +132,21 @@ var ResultCode = {
117
132
  INTERNAL_SERVER_ERROR: "500"
118
133
  };
119
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
+
120
150
  // src/csrf.ts
121
151
  var DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
122
152
  var DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
@@ -216,6 +246,141 @@ function generateIdempotencyKey() {
216
246
  }).join("-");
217
247
  }
218
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
+
219
384
  // src/retry.ts
220
385
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
221
386
  function isRetryableStatus(status) {
@@ -301,6 +466,7 @@ var ApiError = class extends Error {
301
466
  super(args.message);
302
467
  this.name = "ApiError";
303
468
  this.code = args.code;
469
+ this.commonCode = toCommonResultCode(args.code, args.status);
304
470
  this.status = args.status;
305
471
  this.traceId = args.traceId;
306
472
  this.data = args.data;
@@ -332,6 +498,8 @@ function isCommonResponse(value) {
332
498
  }
333
499
  function createApiClient(config) {
334
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;
335
503
  let hookWarned = false;
336
504
  const safeHook = (fn) => {
337
505
  try {
@@ -360,6 +528,9 @@ function createApiClient(config) {
360
528
  if (!headers.has("Content-Type") && typeof init.body === "string") {
361
529
  headers.set("Content-Type", "application/json");
362
530
  }
531
+ if (acceptLanguage !== null && !headers.has("Accept-Language")) {
532
+ headers.set("Accept-Language", acceptLanguage);
533
+ }
363
534
  if (config.getToken && !headers.has("Authorization")) {
364
535
  const token = config.getToken();
365
536
  if (token) headers.set("Authorization", `Bearer ${token}`);
@@ -383,7 +554,7 @@ function createApiClient(config) {
383
554
  if (config.strictJson && response.ok) {
384
555
  throw new ApiError({
385
556
  code: "INVALID_JSON",
386
- message: "\uC751\uB2F5 \uBCF8\uBB38\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
557
+ message: clientMessage("rscc.client.unreadableBody"),
387
558
  status: response.status,
388
559
  traceId,
389
560
  retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After"))
@@ -402,7 +573,7 @@ function createApiClient(config) {
402
573
  if (config.strictJson && response.ok) {
403
574
  throw new ApiError({
404
575
  code: "INVALID_JSON",
405
- message: "\uC751\uB2F5 \uBCF8\uBB38\uC774 \uC720\uD6A8\uD55C JSON \uC774 \uC544\uB2D9\uB2C8\uB2E4.",
576
+ message: clientMessage("rscc.client.invalidJson"),
406
577
  status: response.status,
407
578
  traceId,
408
579
  retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After")),
@@ -431,7 +602,7 @@ function createApiClient(config) {
431
602
  }
432
603
  if (!response.ok) {
433
604
  const j2 = json;
434
- 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);
435
606
  fail(String(response.status), message);
436
607
  }
437
608
  return { data: json, traceId, status: response.status, response };
@@ -489,13 +660,232 @@ function createApiClient(config) {
489
660
  return { request, requestWithMeta };
490
661
  }
491
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
+
492
878
  // src/sse.ts
493
879
  function parseSseFrame(frame) {
494
880
  const dataLine = frame.split("\n").find((l) => l.startsWith("data:"))?.replace(/^data:\s*/, "");
495
- if (dataLine === void 0 || dataLine === "") return { type: "skip" };
496
- 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" };
497
887
  try {
498
- const obj = JSON.parse(dataLine);
888
+ const obj = JSON.parse(data);
499
889
  if (typeof obj.conversationId === "number") {
500
890
  return { type: "conversationId", id: obj.conversationId };
501
891
  }
@@ -508,16 +898,9 @@ function parseSseFrame(frame) {
508
898
  }
509
899
  return { type: "skip" };
510
900
  }
511
- async function readSseStream(response, callbacks) {
512
- if (!response.body) {
513
- throw new Error("SSE \uC751\uB2F5\uC5D0 body \uC2A4\uD2B8\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
514
- }
515
- const reader = response.body.getReader();
516
- const decoder = new TextDecoder();
517
- let buffer = "";
901
+ function createChatDispatcher(callbacks) {
518
902
  let errored = false;
519
- const dispatch = (frame) => {
520
- const ev = parseSseFrame(frame);
903
+ return (ev) => {
521
904
  switch (ev.type) {
522
905
  case "conversationId":
523
906
  callbacks.onConversationId?.(ev.id);
@@ -540,6 +923,16 @@ async function readSseStream(response, callbacks) {
540
923
  }
541
924
  return false;
542
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));
543
936
  try {
544
937
  while (true) {
545
938
  const { done, value } = await reader.read();
@@ -561,6 +954,14 @@ async function readSseStream(response, callbacks) {
561
954
  reader.releaseLock();
562
955
  }
563
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
+ }
564
965
 
565
966
  // src/jwt.ts
566
967
  function decodeJwtPayload(token) {
@@ -1192,25 +1593,22 @@ function extensionOf(fileName) {
1192
1593
  return fileName.slice(dot + 1).toLowerCase();
1193
1594
  }
1194
1595
  function validateUpload(input) {
1596
+ const message = (key, ...args) => getMessage(key, { locale: input.locale, args, overrides: input.messages });
1195
1597
  if (input.size > input.maxSizeBytes) {
1196
- 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") };
1197
1599
  }
1198
1600
  const ext = extensionOf(input.fileName);
1199
1601
  const allowed = input.allowedExtensions.map(
1200
1602
  (e) => (e.startsWith(".") ? e.slice(1) : e).toLowerCase()
1201
1603
  );
1202
1604
  if (ext === "" || !allowed.includes(ext)) {
1203
- 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) };
1204
1606
  }
1205
1607
  const extra = input.extraMappings?.[ext];
1206
1608
  const kinds = extra !== void 0 ? new Set(extra) : kindsForExtension(ext);
1207
1609
  const kind = sniffFile(input.head);
1208
1610
  if (kind === null || !kinds.has(kind)) {
1209
- return {
1210
- ok: false,
1211
- reason: "content-mismatch",
1212
- message: "\uD30C\uC77C \uB0B4\uC6A9\uC774 \uD655\uC7A5\uC790\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
1213
- };
1611
+ return { ok: false, reason: "content-mismatch", message: message("rscc.upload.contentMismatch") };
1214
1612
  }
1215
1613
  return { ok: true };
1216
1614
  }
@@ -1883,6 +2281,8 @@ function matchesHangul(query, target) {
1883
2281
  CircuitOpenError,
1884
2282
  DEFAULT_CSRF_COOKIE_NAME,
1885
2283
  DEFAULT_CSRF_HEADER_NAME,
2284
+ FIXED_MESSAGE_KEYS,
2285
+ MESSAGES,
1886
2286
  ResultCode,
1887
2287
  WEBHOOK_SIGNATURE_HEADER,
1888
2288
  abbreviateAmount,
@@ -1900,16 +2300,22 @@ function matchesHangul(query, target) {
1900
2300
  createBusinessDays,
1901
2301
  createCircuitBreaker,
1902
2302
  createFeatureFlags,
2303
+ createSseEventParser,
1903
2304
  createTokenBucket,
1904
2305
  createTtlCache,
1905
2306
  csrfHeaderFor,
1906
2307
  decodeJwtPayload,
1907
2308
  decomposeHangul,
2309
+ formatMessage,
1908
2310
  formatPhoneNumber,
2311
+ formatSseComment,
2312
+ formatSseEvent,
1909
2313
  generateIdempotencyKey,
2314
+ getMessage,
1910
2315
  getTokenExpiry,
1911
2316
  isBulkResult,
1912
2317
  isChosungQuery,
2318
+ isCommonResultCode,
1913
2319
  isForeignerRrn,
1914
2320
  isRetryableStatus,
1915
2321
  isTokenExpired,
@@ -1925,16 +2331,22 @@ function matchesHangul(query, target) {
1925
2331
  maskPhone,
1926
2332
  maskSecret,
1927
2333
  matchesHangul,
2334
+ negotiateLanguage,
1928
2335
  normalizeBusinessNumber,
1929
2336
  normalizePhoneNumber,
1930
2337
  normalizeRrn,
2338
+ parseChatPayload,
1931
2339
  parseFlag,
1932
2340
  parseRetryAfterMs,
2341
+ parseSseEvents,
1933
2342
  parseSseFrame,
1934
2343
  parseWireDateTime,
1935
2344
  pickJosa,
1936
2345
  readCookie,
2346
+ readSseChatEvents,
2347
+ readSseEvents,
1937
2348
  readSseStream,
2349
+ resultCodeForStatus,
1938
2350
  retry,
1939
2351
  rrnBirthDate,
1940
2352
  rrnChecksumOkLegacy,
@@ -1943,6 +2355,7 @@ function matchesHangul(query, target) {
1943
2355
  sniffFile,
1944
2356
  stripZone,
1945
2357
  toChosung,
2358
+ toCommonResultCode,
1946
2359
  toE164,
1947
2360
  toFormalNotation,
1948
2361
  toKoreanWords,