@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.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(joinUrl(config.baseUrl, path), { ...init, headers });
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: "\uC751\uB2F5 \uBCF8\uBB38\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
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: "\uC751\uB2F5 \uBCF8\uBB38\uC774 \uC720\uD6A8\uD55C JSON \uC774 \uC544\uB2D9\uB2C8\uB2E4.",
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 || `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);
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 || dataLine === "") return { type: "skip" };
302
- 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" };
303
775
  try {
304
- const obj = JSON.parse(dataLine);
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
- async function readSseStream(response, callbacks) {
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
- const dispatch = (frame) => {
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: "\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") };
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: `\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) };
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,