@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.js CHANGED
@@ -20,6 +20,21 @@ 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
+
23
38
  // src/csrf.ts
24
39
  var DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
25
40
  var DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
@@ -119,6 +134,141 @@ function generateIdempotencyKey() {
119
134
  }).join("-");
120
135
  }
121
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
+
122
272
  // src/retry.ts
123
273
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
124
274
  function isRetryableStatus(status) {
@@ -204,6 +354,7 @@ var ApiError = class extends Error {
204
354
  super(args.message);
205
355
  this.name = "ApiError";
206
356
  this.code = args.code;
357
+ this.commonCode = toCommonResultCode(args.code, args.status);
207
358
  this.status = args.status;
208
359
  this.traceId = args.traceId;
209
360
  this.data = args.data;
@@ -235,6 +386,8 @@ function isCommonResponse(value) {
235
386
  }
236
387
  function createApiClient(config) {
237
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;
238
391
  let hookWarned = false;
239
392
  const safeHook = (fn) => {
240
393
  try {
@@ -263,6 +416,9 @@ function createApiClient(config) {
263
416
  if (!headers.has("Content-Type") && typeof init.body === "string") {
264
417
  headers.set("Content-Type", "application/json");
265
418
  }
419
+ if (acceptLanguage !== null && !headers.has("Accept-Language")) {
420
+ headers.set("Accept-Language", acceptLanguage);
421
+ }
266
422
  if (config.getToken && !headers.has("Authorization")) {
267
423
  const token = config.getToken();
268
424
  if (token) headers.set("Authorization", `Bearer ${token}`);
@@ -286,7 +442,7 @@ function createApiClient(config) {
286
442
  if (config.strictJson && response.ok) {
287
443
  throw new ApiError({
288
444
  code: "INVALID_JSON",
289
- message: "\uC751\uB2F5 \uBCF8\uBB38\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
445
+ message: clientMessage("rscc.client.unreadableBody"),
290
446
  status: response.status,
291
447
  traceId,
292
448
  retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After"))
@@ -305,7 +461,7 @@ function createApiClient(config) {
305
461
  if (config.strictJson && response.ok) {
306
462
  throw new ApiError({
307
463
  code: "INVALID_JSON",
308
- message: "\uC751\uB2F5 \uBCF8\uBB38\uC774 \uC720\uD6A8\uD55C JSON \uC774 \uC544\uB2D9\uB2C8\uB2E4.",
464
+ message: clientMessage("rscc.client.invalidJson"),
309
465
  status: response.status,
310
466
  traceId,
311
467
  retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After")),
@@ -334,7 +490,7 @@ function createApiClient(config) {
334
490
  }
335
491
  if (!response.ok) {
336
492
  const j2 = json;
337
- const message = typeof j2?.message === "string" && j2.message || typeof j2?.error === "string" && j2.error || `API \uC624\uB958: ${response.status} ${response.statusText}`;
493
+ const message = typeof j2?.message === "string" && j2.message || typeof j2?.error === "string" && j2.error || clientMessage("rscc.client.httpError", response.status, response.statusText);
338
494
  fail(String(response.status), message);
339
495
  }
340
496
  return { data: json, traceId, status: response.status, response };
@@ -392,13 +548,232 @@ function createApiClient(config) {
392
548
  return { request, requestWithMeta };
393
549
  }
394
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
+
395
766
  // src/sse.ts
396
767
  function parseSseFrame(frame) {
397
768
  const dataLine = frame.split("\n").find((l) => l.startsWith("data:"))?.replace(/^data:\s*/, "");
398
- if (dataLine === void 0 || dataLine === "") return { type: "skip" };
399
- if (dataLine === "[DONE]") return { type: "done" };
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" };
400
775
  try {
401
- const obj = JSON.parse(dataLine);
776
+ const obj = JSON.parse(data);
402
777
  if (typeof obj.conversationId === "number") {
403
778
  return { type: "conversationId", id: obj.conversationId };
404
779
  }
@@ -411,16 +786,9 @@ function parseSseFrame(frame) {
411
786
  }
412
787
  return { type: "skip" };
413
788
  }
414
- async function readSseStream(response, callbacks) {
415
- if (!response.body) {
416
- throw new Error("SSE \uC751\uB2F5\uC5D0 body \uC2A4\uD2B8\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
417
- }
418
- const reader = response.body.getReader();
419
- const decoder = new TextDecoder();
420
- let buffer = "";
789
+ function createChatDispatcher(callbacks) {
421
790
  let errored = false;
422
- const dispatch = (frame) => {
423
- const ev = parseSseFrame(frame);
791
+ return (ev) => {
424
792
  switch (ev.type) {
425
793
  case "conversationId":
426
794
  callbacks.onConversationId?.(ev.id);
@@ -443,6 +811,16 @@ async function readSseStream(response, callbacks) {
443
811
  }
444
812
  return false;
445
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));
446
824
  try {
447
825
  while (true) {
448
826
  const { done, value } = await reader.read();
@@ -464,6 +842,14 @@ async function readSseStream(response, callbacks) {
464
842
  reader.releaseLock();
465
843
  }
466
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
+ }
467
853
 
468
854
  // src/jwt.ts
469
855
  function decodeJwtPayload(token) {
@@ -1095,25 +1481,22 @@ function extensionOf(fileName) {
1095
1481
  return fileName.slice(dot + 1).toLowerCase();
1096
1482
  }
1097
1483
  function validateUpload(input) {
1484
+ const message = (key, ...args) => getMessage(key, { locale: input.locale, args, overrides: input.messages });
1098
1485
  if (input.size > input.maxSizeBytes) {
1099
- return { ok: false, reason: "size", message: "\uC5C5\uB85C\uB4DC \uAC00\uB2A5\uD55C \uCD5C\uB300 \uD06C\uAE30\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4." };
1486
+ return { ok: false, reason: "size", message: message("rscc.upload.size") };
1100
1487
  }
1101
1488
  const ext = extensionOf(input.fileName);
1102
1489
  const allowed = input.allowedExtensions.map(
1103
1490
  (e) => (e.startsWith(".") ? e.slice(1) : e).toLowerCase()
1104
1491
  );
1105
1492
  if (ext === "" || !allowed.includes(ext)) {
1106
- return { ok: false, reason: "extension", message: `\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${ext}` };
1493
+ return { ok: false, reason: "extension", message: message("rscc.upload.extension", ext) };
1107
1494
  }
1108
1495
  const extra = input.extraMappings?.[ext];
1109
1496
  const kinds = extra !== void 0 ? new Set(extra) : kindsForExtension(ext);
1110
1497
  const kind = sniffFile(input.head);
1111
1498
  if (kind === null || !kinds.has(kind)) {
1112
- return {
1113
- ok: false,
1114
- reason: "content-mismatch",
1115
- message: "\uD30C\uC77C \uB0B4\uC6A9\uC774 \uD655\uC7A5\uC790\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
1116
- };
1499
+ return { ok: false, reason: "content-mismatch", message: message("rscc.upload.contentMismatch") };
1117
1500
  }
1118
1501
  return { ok: true };
1119
1502
  }
@@ -1785,6 +2168,8 @@ export {
1785
2168
  CircuitOpenError,
1786
2169
  DEFAULT_CSRF_COOKIE_NAME,
1787
2170
  DEFAULT_CSRF_HEADER_NAME,
2171
+ FIXED_MESSAGE_KEYS,
2172
+ MESSAGES,
1788
2173
  ResultCode,
1789
2174
  WEBHOOK_SIGNATURE_HEADER,
1790
2175
  abbreviateAmount,
@@ -1802,16 +2187,22 @@ export {
1802
2187
  createBusinessDays,
1803
2188
  createCircuitBreaker,
1804
2189
  createFeatureFlags,
2190
+ createSseEventParser,
1805
2191
  createTokenBucket,
1806
2192
  createTtlCache,
1807
2193
  csrfHeaderFor,
1808
2194
  decodeJwtPayload,
1809
2195
  decomposeHangul,
2196
+ formatMessage,
1810
2197
  formatPhoneNumber,
2198
+ formatSseComment,
2199
+ formatSseEvent,
1811
2200
  generateIdempotencyKey,
2201
+ getMessage,
1812
2202
  getTokenExpiry,
1813
2203
  isBulkResult,
1814
2204
  isChosungQuery,
2205
+ isCommonResultCode,
1815
2206
  isForeignerRrn,
1816
2207
  isRetryableStatus,
1817
2208
  isTokenExpired,
@@ -1827,16 +2218,22 @@ export {
1827
2218
  maskPhone,
1828
2219
  maskSecret,
1829
2220
  matchesHangul,
2221
+ negotiateLanguage,
1830
2222
  normalizeBusinessNumber,
1831
2223
  normalizePhoneNumber,
1832
2224
  normalizeRrn,
2225
+ parseChatPayload,
1833
2226
  parseFlag,
1834
2227
  parseRetryAfterMs,
2228
+ parseSseEvents,
1835
2229
  parseSseFrame,
1836
2230
  parseWireDateTime,
1837
2231
  pickJosa,
1838
2232
  readCookie,
2233
+ readSseChatEvents,
2234
+ readSseEvents,
1839
2235
  readSseStream,
2236
+ resultCodeForStatus,
1840
2237
  retry,
1841
2238
  rrnBirthDate,
1842
2239
  rrnChecksumOkLegacy,
@@ -1845,6 +2242,7 @@ export {
1845
2242
  sniffFile,
1846
2243
  stripZone,
1847
2244
  toChosung,
2245
+ toCommonResultCode,
1848
2246
  toE164,
1849
2247
  toFormalNotation,
1850
2248
  toKoreanWords,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rscc/common-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "RSCC 공통 코어 — CommonResponse 타입·apiClient(traceId·재시도·멱등성 키), SSE 파서, 회복탄력성(retry·서킷 브레이커·토큰버킷·Bulkhead·TTL 캐시), 보안(마스킹·로그 리댁션·웹훅 서명·JWT 디코드·AES-GCM 서브패스), 와이어 계약 헬퍼, 한국 도메인 유틸. 프레임워크 무관, 런타임 의존성 0.",
5
5
  "keywords": [
6
6
  "rscc",