@spfn/core 0.3.0-beta.4 → 0.3.0-beta.6

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.
Files changed (45) hide show
  1. package/README.md +183 -4
  2. package/dist/authz/index.js +1 -381
  3. package/dist/authz/index.js.map +1 -1
  4. package/dist/db/index.d.ts +173 -27
  5. package/dist/db/index.js +192 -57
  6. package/dist/db/index.js.map +1 -1
  7. package/dist/env/loader.js +24 -1
  8. package/dist/env/loader.js.map +1 -1
  9. package/dist/errors/index.js +1 -381
  10. package/dist/errors/index.js.map +1 -1
  11. package/dist/logger/index.js +0 -12
  12. package/dist/logger/index.js.map +1 -1
  13. package/dist/middleware/index.js +6 -387
  14. package/dist/middleware/index.js.map +1 -1
  15. package/dist/nextjs/index.d.ts +18 -1
  16. package/dist/nextjs/index.js +40 -1
  17. package/dist/nextjs/index.js.map +1 -1
  18. package/dist/nextjs/server.d.ts +34 -1
  19. package/dist/nextjs/server.js +14 -0
  20. package/dist/nextjs/server.js.map +1 -1
  21. package/dist/ops/index.d.ts +61 -6
  22. package/dist/ops/index.js +330 -30
  23. package/dist/ops/index.js.map +1 -1
  24. package/dist/server/index.js +24 -1
  25. package/dist/server/index.js.map +1 -1
  26. package/docs/file-upload.md +195 -333
  27. package/package.json +6 -5
  28. package/src/cache/README.md +330 -0
  29. package/src/codegen/README.md +516 -0
  30. package/src/config/README.md +326 -0
  31. package/src/contract/README.md +326 -0
  32. package/src/db/README.md +589 -0
  33. package/src/db/manager/README.md +500 -0
  34. package/src/db/schema/README.md +344 -0
  35. package/src/db/transaction/README.md +822 -0
  36. package/src/env/README.md +651 -0
  37. package/src/errors/README.md +429 -0
  38. package/src/event/README.md +736 -0
  39. package/src/job/README.md +514 -0
  40. package/src/logger/README.md +321 -0
  41. package/src/middleware/README.md +634 -0
  42. package/src/nextjs/README.md +608 -0
  43. package/src/route/README.md +738 -0
  44. package/src/security/README.md +100 -0
  45. package/src/server/README.md +704 -0
@@ -26,9 +26,10 @@ function parseEnvFile(filePath) {
26
26
  return parse(readFileSync(filePath, "utf-8"));
27
27
  }
28
28
  function loadEnv(options = {}) {
29
+ const nodeEnvAtEntry = process.env.NODE_ENV;
29
30
  const {
30
31
  cwd = process.cwd(),
31
- nodeEnv = process.env.NODE_ENV || "local",
32
+ nodeEnv = nodeEnvAtEntry || "local",
32
33
  server = true,
33
34
  debug = false,
34
35
  override = false
@@ -58,8 +59,30 @@ function loadEnv(options = {}) {
58
59
  envLogger.debug(`Loaded env files: ${loadedFiles.join(", ")}`);
59
60
  envLogger.debug(`Loaded ${loadedKeys.length} environment variables`);
60
61
  }
62
+ warnIfEnvGuessCostSomething(cwd, nodeEnvAtEntry, options.nodeEnv, nodeEnv);
61
63
  return { loadedFiles, loadedKeys };
62
64
  }
65
+ function warnIfEnvGuessCostSomething(cwd, nodeEnvAtEntry, explicitNodeEnv, resolvedNodeEnv) {
66
+ if (nodeEnvAtEntry || explicitNodeEnv) {
67
+ return;
68
+ }
69
+ const declared = process.env.NODE_ENV;
70
+ if (!declared) {
71
+ envLogger.warn(
72
+ `NODE_ENV is not set anywhere; env files were resolved for "${resolvedNodeEnv}". Set NODE_ENV in the process environment to choose the file set.`
73
+ );
74
+ return;
75
+ }
76
+ if (declared === resolvedNodeEnv) {
77
+ return;
78
+ }
79
+ if (!existsSync(resolve(cwd, `.env.${declared}`))) {
80
+ return;
81
+ }
82
+ envLogger.warn(
83
+ `.env.${declared} was NOT loaded: NODE_ENV was not set when the process started, so env files were resolved for "${resolvedNodeEnv}" before a file declared NODE_ENV=${declared}. Set NODE_ENV in the process environment, not in a .env file.`
84
+ );
85
+ }
63
86
  var isEnvLoaded = false;
64
87
  function loadEnvOnce(options = {}) {
65
88
  if (isEnvLoaded) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/env/loader.ts"],"names":[],"mappings":"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","file":"loader.js","sourcesContent":["/**\n * Environment Variable Loader\n *\n * Next.js 스타일의 환경변수 파일 로딩 (환경별 분리 지원)\n *\n * 로딩 우선순위 (낮음 -> 높음, 나중 파일이 덮어씀):\n * 1. .env - 기본값 (committed)\n * 2. .env.{NODE_ENV} - 환경별 오버라이드 (committed)\n * 3. .env.local - 로컬 오버라이드 (gitignored, test에서 스킵)\n * 4. .env.{NODE_ENV}.local - 환경별 시크릿 (gitignored)\n * 5. .env.server - 서버 전용 (gitignored, Next.js 미로드)\n *\n * @example\n * ```typescript\n * import { loadEnv } from '@spfn/core/env/loader';\n *\n * // 기본 사용 (NODE_ENV 자동 감지)\n * loadEnv();\n *\n * // 특정 환경 지정\n * loadEnv({ nodeEnv: 'production' });\n *\n * // 서버 레이어 제외 (Next.js 클라이언트용)\n * loadEnv({ server: false });\n * ```\n *\n * @module env/loader\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parse } from 'dotenv';\nimport { logger } from '@spfn/core/logger';\n\nconst envLogger = logger.child('@spfn/core:env-loader');\n\n/**\n * loadEnv 옵션\n */\nexport interface LoadEnvOptions\n{\n /**\n * 프로젝트 루트 경로\n * @default process.cwd()\n */\n cwd?: string;\n\n /**\n * NODE_ENV 값 (환경별 .env 파일 결정)\n * @default process.env.NODE_ENV || 'local'\n */\n nodeEnv?: string;\n\n /**\n * 서버 전용 파일 포함 여부 (.env.server)\n * @default true\n */\n server?: boolean;\n\n /**\n * 디버그 모드 (로드된 파일 로깅)\n * @default false\n */\n debug?: boolean;\n\n /**\n * 기존 process.env 값 덮어쓰기 허용\n * @default false\n */\n override?: boolean;\n}\n\n/**\n * 환경변수 로드 결과\n */\nexport interface LoadEnvResult\n{\n /**\n * 로드된 파일 목록\n */\n loadedFiles: string[];\n\n /**\n * 로드된 환경변수 키 목록\n */\n loadedKeys: string[];\n}\n\n/**\n * NODE_ENV에 따른 .env 파일 목록 생성 (우선순위 낮음 -> 높음)\n *\n * 더 구체적인 파일이 승리:\n * - environment > base\n * - server > shared\n * - local > committed\n */\nfunction getEnvFiles(nodeEnv: string, server: boolean): string[]\n{\n const files: string[] = [\n '.env',\n `.env.${nodeEnv}`,\n ];\n\n // test 환경에서는 .env.local 스킵 (테스트 결정론성 보장)\n if (nodeEnv !== 'test')\n {\n files.push('.env.local');\n }\n\n files.push(`.env.${nodeEnv}.local`);\n\n if (server)\n {\n files.push('.env.server');\n }\n\n return files;\n}\n\n/**\n * 단일 .env 파일 파싱\n */\nfunction parseEnvFile(filePath: string): Record<string, string> | null\n{\n if (!existsSync(filePath))\n {\n return null;\n }\n\n return parse(readFileSync(filePath, 'utf-8'));\n}\n\n/**\n * 프로젝트 루트의 환경변수 파일들을 규칙에 따라 로드\n *\n * 모든 파일을 파싱 후 머지한 뒤 process.env에 한번에 적용.\n * 이미 process.env에 존재하는 키는 덮어쓰지 않음 (플랫폼 주입 보호).\n *\n * @param options - 로드 옵션\n * @returns 로드 결과 (로드된 파일, 키 목록)\n */\nexport function loadEnv(options: LoadEnvOptions = {}): LoadEnvResult\n{\n const {\n cwd = process.cwd(),\n nodeEnv = process.env.NODE_ENV || 'local',\n server = true,\n debug = false,\n override = false,\n } = options;\n\n const envFiles = getEnvFiles(nodeEnv, server);\n const loadedFiles: string[] = [];\n\n // 1) 기존 process.env 키 스냅샷 저장\n const existingKeys = new Set(Object.keys(process.env));\n\n // 2) 모든 .env 파일 파싱 후 머지 (나중 파일이 승리)\n const merged: Record<string, string> = {};\n\n for (const fileName of envFiles)\n {\n const filePath = resolve(cwd, fileName);\n const parsed = parseEnvFile(filePath);\n\n if (parsed === null)\n {\n continue;\n }\n\n loadedFiles.push(fileName);\n Object.assign(merged, parsed);\n }\n\n // 3) 머지된 결과를 process.env에 적용\n const loadedKeys: string[] = [];\n\n for (const [key, value] of Object.entries(merged))\n {\n // 기존 process.env에 이미 있는 키는 스킵 (override가 false일 때)\n if (!override && existingKeys.has(key))\n {\n continue;\n }\n\n process.env[key] = value;\n loadedKeys.push(key);\n }\n\n if (debug && loadedFiles.length > 0)\n {\n envLogger.debug(`Loaded env files: ${loadedFiles.join(', ')}`);\n envLogger.debug(`Loaded ${loadedKeys.length} environment variables`);\n }\n\n return { loadedFiles, loadedKeys };\n}\n\n/**\n * 환경변수가 이미 로드되었는지 확인하는 플래그\n */\nlet isEnvLoaded = false;\n\n/**\n * 환경변수를 한 번만 로드 (중복 호출 방지)\n *\n * @param options - 로드 옵션\n * @returns 로드 결과 (이미 로드된 경우 빈 결과)\n */\nexport function loadEnvOnce(options: LoadEnvOptions = {}): LoadEnvResult\n{\n if (isEnvLoaded)\n {\n return { loadedFiles: [], loadedKeys: [] };\n }\n\n isEnvLoaded = true;\n\n return loadEnv(options);\n}\n\n/**\n * 환경변수 로드 상태 리셋 (테스트용)\n */\nexport function resetEnvLoadState(): void\n{\n isEnvLoaded = false;\n}\n"]}
1
+ {"version":3,"sources":["../../src/env/loader.ts"],"names":[],"mappings":"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","file":"loader.js","sourcesContent":["/**\n * Environment Variable Loader\n *\n * Next.js 스타일의 환경변수 파일 로딩 (환경별 분리 지원)\n *\n * 로딩 우선순위 (낮음 -> 높음, 나중 파일이 덮어씀):\n * 1. .env - 기본값 (committed)\n * 2. .env.{NODE_ENV} - 환경별 오버라이드 (committed)\n * 3. .env.local - 로컬 오버라이드 (gitignored, test에서 스킵)\n * 4. .env.{NODE_ENV}.local - 환경별 시크릿 (gitignored)\n * 5. .env.server - 서버 전용 (gitignored, Next.js 미로드)\n *\n * @example\n * ```typescript\n * import { loadEnv } from '@spfn/core/env/loader';\n *\n * // 기본 사용 (NODE_ENV 자동 감지)\n * loadEnv();\n *\n * // 특정 환경 지정\n * loadEnv({ nodeEnv: 'production' });\n *\n * // 서버 레이어 제외 (Next.js 클라이언트용)\n * loadEnv({ server: false });\n * ```\n *\n * @module env/loader\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parse } from 'dotenv';\nimport { logger } from '@spfn/core/logger';\n\nconst envLogger = logger.child('@spfn/core:env-loader');\n\n/**\n * loadEnv 옵션\n */\nexport interface LoadEnvOptions\n{\n /**\n * 프로젝트 루트 경로\n * @default process.cwd()\n */\n cwd?: string;\n\n /**\n * NODE_ENV 값 (환경별 .env 파일 결정)\n * @default process.env.NODE_ENV || 'local'\n */\n nodeEnv?: string;\n\n /**\n * 서버 전용 파일 포함 여부 (.env.server)\n * @default true\n */\n server?: boolean;\n\n /**\n * 디버그 모드 (로드된 파일 로깅)\n * @default false\n */\n debug?: boolean;\n\n /**\n * 기존 process.env 값 덮어쓰기 허용\n * @default false\n */\n override?: boolean;\n}\n\n/**\n * 환경변수 로드 결과\n */\nexport interface LoadEnvResult\n{\n /**\n * 로드된 파일 목록\n */\n loadedFiles: string[];\n\n /**\n * 로드된 환경변수 키 목록\n */\n loadedKeys: string[];\n}\n\n/**\n * NODE_ENV에 따른 .env 파일 목록 생성 (우선순위 낮음 -> 높음)\n *\n * 더 구체적인 파일이 승리:\n * - environment > base\n * - server > shared\n * - local > committed\n */\nfunction getEnvFiles(nodeEnv: string, server: boolean): string[]\n{\n const files: string[] = [\n '.env',\n `.env.${nodeEnv}`,\n ];\n\n // test 환경에서는 .env.local 스킵 (테스트 결정론성 보장)\n if (nodeEnv !== 'test')\n {\n files.push('.env.local');\n }\n\n files.push(`.env.${nodeEnv}.local`);\n\n if (server)\n {\n files.push('.env.server');\n }\n\n return files;\n}\n\n/**\n * 단일 .env 파일 파싱\n */\nfunction parseEnvFile(filePath: string): Record<string, string> | null\n{\n if (!existsSync(filePath))\n {\n return null;\n }\n\n return parse(readFileSync(filePath, 'utf-8'));\n}\n\n/**\n * 프로젝트 루트의 환경변수 파일들을 규칙에 따라 로드\n *\n * 모든 파일을 파싱 후 머지한 뒤 process.env에 한번에 적용.\n * 이미 process.env에 존재하는 키는 덮어쓰지 않음 (플랫폼 주입 보호).\n *\n * @param options - 로드 옵션\n * @returns 로드 결과 (로드된 파일, 키 목록)\n */\nexport function loadEnv(options: LoadEnvOptions = {}): LoadEnvResult\n{\n // Captured before any file is read: NODE_ENV decides WHICH files load, so a\n // value that only arrives from inside a file arrives too late to matter.\n const nodeEnvAtEntry = process.env.NODE_ENV;\n\n const {\n cwd = process.cwd(),\n nodeEnv = nodeEnvAtEntry || 'local',\n server = true,\n debug = false,\n override = false,\n } = options;\n\n const envFiles = getEnvFiles(nodeEnv, server);\n const loadedFiles: string[] = [];\n\n // 1) 기존 process.env 키 스냅샷 저장\n const existingKeys = new Set(Object.keys(process.env));\n\n // 2) 모든 .env 파일 파싱 후 머지 (나중 파일이 승리)\n const merged: Record<string, string> = {};\n\n for (const fileName of envFiles)\n {\n const filePath = resolve(cwd, fileName);\n const parsed = parseEnvFile(filePath);\n\n if (parsed === null)\n {\n continue;\n }\n\n loadedFiles.push(fileName);\n Object.assign(merged, parsed);\n }\n\n // 3) 머지된 결과를 process.env에 적용\n const loadedKeys: string[] = [];\n\n for (const [key, value] of Object.entries(merged))\n {\n // 기존 process.env에 이미 있는 키는 스킵 (override가 false일 때)\n if (!override && existingKeys.has(key))\n {\n continue;\n }\n\n process.env[key] = value;\n loadedKeys.push(key);\n }\n\n if (debug && loadedFiles.length > 0)\n {\n envLogger.debug(`Loaded env files: ${loadedFiles.join(', ')}`);\n envLogger.debug(`Loaded ${loadedKeys.length} environment variables`);\n }\n\n warnIfEnvGuessCostSomething(cwd, nodeEnvAtEntry, options.nodeEnv, nodeEnv);\n\n return { loadedFiles, loadedKeys };\n}\n\n/**\n * Warn when guessing NODE_ENV actually cost the caller something.\n *\n * `getEnvFiles` runs before any file is parsed, so NODE_ENV decides WHICH files\n * load and a value set inside .env.server arrives too late to affect that. The\n * warning therefore reports a missed file, not a missing variable — the previous\n * version announced the variable itself and fired on every boot of a correctly\n * configured app (issue #136).\n *\n * Silent when the guess cost nothing: the guess matched what the files declare,\n * or it did not match but no `.env.<declared>` file exists to have been skipped.\n *\n * @param cwd - project root the files were resolved against\n * @param nodeEnvAtEntry - NODE_ENV as the process was started with\n * @param explicitNodeEnv - nodeEnv passed by the caller, if any\n * @param resolvedNodeEnv - the value the file list was actually built from\n */\nfunction warnIfEnvGuessCostSomething(\n cwd: string,\n nodeEnvAtEntry: string | undefined,\n explicitNodeEnv: string | undefined,\n resolvedNodeEnv: string,\n): void\n{\n // Nothing was guessed — the process or the caller named the environment.\n if (nodeEnvAtEntry || explicitNodeEnv)\n {\n return;\n }\n\n const declared = process.env.NODE_ENV;\n\n if (!declared)\n {\n envLogger.warn(\n `NODE_ENV is not set anywhere; env files were resolved for \"${resolvedNodeEnv}\".`\n + ' Set NODE_ENV in the process environment to choose the file set.',\n );\n\n return;\n }\n\n if (declared === resolvedNodeEnv)\n {\n return;\n }\n\n // The guess was wrong, but a file set only goes missing if the file exists.\n if (!existsSync(resolve(cwd, `.env.${declared}`)))\n {\n return;\n }\n\n envLogger.warn(\n `.env.${declared} was NOT loaded: NODE_ENV was not set when the process started,`\n + ` so env files were resolved for \"${resolvedNodeEnv}\" before a file declared`\n + ` NODE_ENV=${declared}. Set NODE_ENV in the process environment, not in a .env file.`,\n );\n}\n\n/**\n * 환경변수가 이미 로드되었는지 확인하는 플래그\n */\nlet isEnvLoaded = false;\n\n/**\n * 환경변수를 한 번만 로드 (중복 호출 방지)\n *\n * @param options - 로드 옵션\n * @returns 로드 결과 (이미 로드된 경우 빈 결과)\n */\nexport function loadEnvOnce(options: LoadEnvOptions = {}): LoadEnvResult\n{\n if (isEnvLoaded)\n {\n return { loadedFiles: [], loadedKeys: [] };\n }\n\n isEnvLoaded = true;\n\n return loadEnv(options);\n}\n\n/**\n * 환경변수 로드 상태 리셋 (테스트용)\n */\nexport function resetEnvLoadState(): void\n{\n isEnvLoaded = false;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { format } from 'util';
1
+ import { logger } from '@spfn/core/logger';
2
2
 
3
3
  var __defProp = Object.defineProperty;
4
4
  var __export = (target, all) => {
@@ -114,386 +114,6 @@ __export(http_errors_exports, {
114
114
  UnsupportedMediaTypeError: () => UnsupportedMediaTypeError,
115
115
  ValidationError: () => ValidationError
116
116
  });
117
-
118
- // src/logger/types.ts
119
- var LOG_LEVEL_PRIORITY = {
120
- debug: 0,
121
- info: 1,
122
- warn: 2,
123
- error: 3,
124
- fatal: 4
125
- };
126
-
127
- // src/logger/formatters.ts
128
- var SENSITIVE_KEYS = [
129
- "password",
130
- "passwd",
131
- "pwd",
132
- "secret",
133
- "token",
134
- "apikey",
135
- "api_key",
136
- "accesstoken",
137
- "access_token",
138
- "refreshtoken",
139
- "refresh_token",
140
- "authorization",
141
- "auth",
142
- "cookie",
143
- "session",
144
- "sessionid",
145
- "session_id",
146
- "privatekey",
147
- "private_key",
148
- "creditcard",
149
- "credit_card",
150
- "cardnumber",
151
- "card_number",
152
- "cvv",
153
- "ssn",
154
- "pin"
155
- ];
156
- var MASKED_VALUE = "***MASKED***";
157
- function isSensitiveKey(key) {
158
- const lowerKey = key.toLowerCase();
159
- return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
160
- }
161
- function maskSensitiveData(data, seen = /* @__PURE__ */ new WeakSet()) {
162
- if (data === null || data === void 0) {
163
- return data;
164
- }
165
- if (typeof data !== "object") {
166
- return data;
167
- }
168
- if (seen.has(data)) {
169
- return "[Circular]";
170
- }
171
- seen.add(data);
172
- if (Array.isArray(data)) {
173
- return data.map((item) => maskSensitiveData(item, seen));
174
- }
175
- const masked = {};
176
- for (const [key, value] of Object.entries(data)) {
177
- if (isSensitiveKey(key)) {
178
- masked[key] = MASKED_VALUE;
179
- } else if (typeof value === "object" && value !== null) {
180
- masked[key] = maskSensitiveData(value, seen);
181
- } else {
182
- masked[key] = value;
183
- }
184
- }
185
- return masked;
186
- }
187
- var COLORS = {
188
- reset: "\x1B[0m",
189
- bright: "\x1B[1m",
190
- dim: "\x1B[2m",
191
- // 로그 레벨 컬러
192
- debug: "\x1B[36m",
193
- // cyan
194
- info: "\x1B[32m",
195
- // green
196
- warn: "\x1B[33m",
197
- // yellow
198
- error: "\x1B[31m",
199
- // red
200
- fatal: "\x1B[35m",
201
- // magenta
202
- // 추가 컬러
203
- gray: "\x1B[90m"
204
- };
205
- function formatTimestampHuman(date) {
206
- const year = date.getFullYear();
207
- const month = String(date.getMonth() + 1).padStart(2, "0");
208
- const day = String(date.getDate()).padStart(2, "0");
209
- const hours = String(date.getHours()).padStart(2, "0");
210
- const minutes = String(date.getMinutes()).padStart(2, "0");
211
- const seconds = String(date.getSeconds()).padStart(2, "0");
212
- const ms = String(date.getMilliseconds()).padStart(3, "0");
213
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
214
- }
215
- function formatError(error) {
216
- const lines = [];
217
- lines.push(`${error.name}: ${error.message}`);
218
- if (error.stack) {
219
- const stackLines = error.stack.split("\n").slice(1);
220
- lines.push(...stackLines);
221
- }
222
- if (error.cause instanceof Error) {
223
- lines.push(`Caused by: ${formatError(error.cause)}`);
224
- } else if (error.cause !== void 0) {
225
- lines.push(`Caused by: ${String(error.cause)}`);
226
- }
227
- return lines.join("\n");
228
- }
229
- function formatConsole(metadata, colorize = true) {
230
- const parts = [];
231
- const timestamp = formatTimestampHuman(metadata.timestamp);
232
- if (colorize) {
233
- parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
234
- } else {
235
- parts.push(`[${timestamp}]`);
236
- }
237
- const pid = process.pid;
238
- if (colorize) {
239
- parts.push(`${COLORS.dim}[pid=${pid}]${COLORS.reset}`);
240
- } else {
241
- parts.push(`[pid=${pid}]`);
242
- }
243
- if (metadata.module) {
244
- if (colorize) {
245
- parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
246
- } else {
247
- parts.push(`[module=${metadata.module}]`);
248
- }
249
- }
250
- if (metadata.context && Object.keys(metadata.context).length > 0) {
251
- Object.entries(metadata.context).forEach(([key, value]) => {
252
- let valueStr;
253
- if (typeof value === "string") {
254
- valueStr = value;
255
- } else if (typeof value === "object" && value !== null) {
256
- try {
257
- valueStr = JSON.stringify(value);
258
- } catch (error) {
259
- valueStr = "[circular]";
260
- }
261
- } else {
262
- valueStr = String(value);
263
- }
264
- if (colorize) {
265
- parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
266
- } else {
267
- parts.push(`[${key}=${valueStr}]`);
268
- }
269
- });
270
- }
271
- const levelStr = metadata.level.toUpperCase();
272
- if (colorize) {
273
- const color = COLORS[metadata.level];
274
- parts.push(`${color}(${levelStr})${COLORS.reset}:`);
275
- } else {
276
- parts.push(`(${levelStr}):`);
277
- }
278
- if (colorize) {
279
- parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
280
- } else {
281
- parts.push(metadata.message);
282
- }
283
- let output = parts.join(" ");
284
- if (metadata.error) {
285
- output += "\n" + formatError(metadata.error);
286
- }
287
- return output;
288
- }
289
-
290
- // src/logger/logger.ts
291
- var FORMAT_PATTERN = /%[sdifjoOc%]/;
292
- var Logger = class _Logger {
293
- config;
294
- module;
295
- constructor(config) {
296
- this.config = config;
297
- this.module = config.module;
298
- }
299
- /**
300
- * Convert unknown error to Error object
301
- */
302
- toError(error) {
303
- if (error instanceof Error) return error;
304
- if (typeof error === "string") return new Error(error);
305
- if (typeof error === "object" && error !== null) {
306
- return new Error(JSON.stringify(error));
307
- }
308
- return new Error(String(error));
309
- }
310
- /**
311
- * Check if value is a context object (not an error)
312
- */
313
- isContext(value) {
314
- if (typeof value !== "object" || value === null) return false;
315
- if (value instanceof Error) return false;
316
- const hasStack = "stack" in value && typeof value.stack === "string";
317
- if (hasStack) {
318
- return false;
319
- }
320
- return true;
321
- }
322
- /**
323
- * Get current log level
324
- */
325
- get level() {
326
- return this.config.level;
327
- }
328
- /**
329
- * Create child logger (per module)
330
- */
331
- child(module) {
332
- return new _Logger({
333
- ...this.config,
334
- module
335
- });
336
- }
337
- /**
338
- * Common log method with error/context detection
339
- */
340
- logWithLevel(level, message, errorOrContext, context) {
341
- if (errorOrContext !== void 0 && FORMAT_PATTERN.test(message)) {
342
- this.log(level, format(message, errorOrContext), void 0, context);
343
- return;
344
- }
345
- if (errorOrContext instanceof Error) {
346
- this.log(level, message, errorOrContext, context);
347
- } else if (errorOrContext !== void 0 && typeof errorOrContext === "object" && !this.isContext(errorOrContext)) {
348
- this.log(level, message, this.toError(errorOrContext), context);
349
- } else if (typeof errorOrContext === "string" || typeof errorOrContext === "number" || typeof errorOrContext === "boolean") {
350
- this.log(level, message, this.toError(errorOrContext), context);
351
- } else {
352
- this.log(level, message, void 0, errorOrContext);
353
- }
354
- }
355
- debug(message, errorOrContext, context) {
356
- this.logWithLevel("debug", message, errorOrContext, context);
357
- }
358
- info(message, errorOrContext, context) {
359
- this.logWithLevel("info", message, errorOrContext, context);
360
- }
361
- warn(message, errorOrContext, context) {
362
- this.logWithLevel("warn", message, errorOrContext, context);
363
- }
364
- error(message, errorOrContext, context) {
365
- this.logWithLevel("error", message, errorOrContext, context);
366
- }
367
- fatal(message, errorOrContext, context) {
368
- this.logWithLevel("fatal", message, errorOrContext, context);
369
- }
370
- /**
371
- * Log processing (internal)
372
- */
373
- log(level, message, error, context) {
374
- if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
375
- return;
376
- }
377
- const metadata = {
378
- timestamp: /* @__PURE__ */ new Date(),
379
- level,
380
- message,
381
- module: this.module,
382
- error,
383
- // Mask sensitive information in context to prevent credential leaks
384
- context: context ? maskSensitiveData(context) : void 0
385
- };
386
- this.processTransports(metadata);
387
- }
388
- /**
389
- * Process Transports
390
- */
391
- processTransports(metadata) {
392
- const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
393
- Promise.all(promises).catch((error) => {
394
- const errorMessage = error instanceof Error ? error.message : String(error);
395
- process.stderr.write(`[Logger] Transport error: ${errorMessage}
396
- `);
397
- });
398
- }
399
- /**
400
- * Transport log (error-safe)
401
- */
402
- async safeTransportLog(transport, metadata) {
403
- try {
404
- await transport.log(metadata);
405
- } catch (error) {
406
- const errorMessage = error instanceof Error ? error.message : String(error);
407
- process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
408
- `);
409
- }
410
- }
411
- /**
412
- * Close all Transports
413
- */
414
- async close() {
415
- const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
416
- await Promise.all(closePromises);
417
- }
418
- };
419
-
420
- // src/logger/transports/console.ts
421
- var ConsoleTransport = class {
422
- name = "console";
423
- level;
424
- enabled;
425
- colorize;
426
- constructor(config) {
427
- this.level = config.level;
428
- this.enabled = config.enabled;
429
- this.colorize = config.colorize ?? true;
430
- }
431
- async log(metadata) {
432
- if (!this.enabled) {
433
- return;
434
- }
435
- if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
436
- return;
437
- }
438
- const message = formatConsole(metadata, this.colorize);
439
- if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
440
- console.error(message);
441
- } else {
442
- console.log(message);
443
- }
444
- }
445
- };
446
-
447
- // src/logger/config.ts
448
- function getConsoleConfig() {
449
- const isProduction = process.env.NODE_ENV === "production";
450
- return {
451
- level: "debug",
452
- enabled: true,
453
- colorize: !isProduction
454
- // Dev: colored output, Production: plain text
455
- };
456
- }
457
- function validateEnvironment() {
458
- const nodeEnv = process.env.NODE_ENV;
459
- if (!nodeEnv) {
460
- process.stderr.write(
461
- "[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
462
- );
463
- }
464
- }
465
- function validateConfig() {
466
- validateEnvironment();
467
- }
468
-
469
- // src/logger/factory.ts
470
- function initializeTransports() {
471
- const transports = [];
472
- const consoleConfig = getConsoleConfig();
473
- transports.push(new ConsoleTransport(consoleConfig));
474
- return transports;
475
- }
476
- function getLogLevel() {
477
- const envLevel = process.env.SPFN_LOG_LEVEL || process.env.NEXT_PUBLIC_SPFN_LOG_LEVEL || "info";
478
- if (envLevel in LOG_LEVEL_PRIORITY) {
479
- return envLevel;
480
- }
481
- process.stderr.write(
482
- `[Logger] Invalid log level "${envLevel}", defaulting to "info"
483
- `
484
- );
485
- return "info";
486
- }
487
- function initializeLogger() {
488
- validateConfig();
489
- return new Logger({
490
- level: getLogLevel(),
491
- transports: initializeTransports()
492
- });
493
- }
494
- var logger = initializeLogger();
495
-
496
- // src/errors/serializable-error.ts
497
117
  var RESERVED_RESPONSE_KEYS = /* @__PURE__ */ new Set(["__type", "message", "error"]);
498
118
  var AUTHORING_ENVIRONMENTS = /* @__PURE__ */ new Set(["local", "development", "test"]);
499
119
  function refuseReservedKey(className, key) {