Public Source Viewer

비나래아카이브 개발자 포털

실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.

Redacted View
src/services/developer-portal.service.ts
공개 가능
1 import fs from 'fs';
2 import path from 'path';
3 import { ROOT_DIR } from '../utils/paths';
4
5 export type PortalFileEntry = {
6 path: string;
7 name: string;
8 directory: string;
9 sensitive: boolean;
10 };
11
12 export type PortalFileView = {
13 entry: PortalFileEntry;
14 lines: { number: number; text: string; redacted: boolean }[];
15 redactedFile: boolean;
16 notice: string | null;
17 };
18
19 const INCLUDE_ROOTS = ['src', 'view/hinana', 'public/js'];
20 const INCLUDE_FILES = ['package.json', 'Dockerfile', 'docker-compose.yml', 'tsconfig.json', 'README', 'DEPLOYMENT.md', 'public/sw.js'];
21 const EXCLUDED_DIRS = new Set([
22 '.git',
23 '.github',
24 '.claude',
25 'archive',
26 'config',
27 'data',
28 'dist',
29 'node_modules',
30 'public/uploads',
31 'public/image',
32 'public/sound',
33 'view/image',
34 'view/css'
35 ]);
36 const ALLOWED_EXTENSIONS = new Set(['.ts', '.js', '.ejs', '.css', '.json', '.md', '.yml', '.yaml', '.html', '']);
37 const PUBLIC_JSON_FILES = new Set(['package.json', 'tsconfig.json']);
38 const FULL_FILE_REDACTIONS = [
39 'src/config/constants.ts',
40 'src/bot/discord-bot.ts',
41 'src/routes/discord.routes.ts',
42 'src/routes/auth.routes.ts',
43 'src/middleware/session.middleware.ts',
44 'src/services/file-session-store.service.ts',
45 'src/services/push.service.ts'
46 ];
47 const AUTO_FILE_REDACTION_PATTERNS = [
48 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
49 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
50 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
51 /firebase.*admin/i,
52 /service[-_]?account/i,
53 /admin[-_]?sdk/i
54 ];
55 const REDACT_LINE_PATTERNS = [
56 /process\.env/i,
57 /\bfs\.(readFile|readFileSync|writeFile|writeFileSync|readdir|readdirSync|existsSync|statSync|statfsSync)\b/i,
58 /\bJSON\.(parse|stringify)\b/,
59 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
60 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
61 /api[_-]?key/i,
62 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
63 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
64 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
65 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
66 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
67 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
68 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
69 /username\s*[!=]={1,2}\s*['"`][^'"`]+['"`]/,
70 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
71 /\b\d{15,22}\b/
72 ];
73
74 function toPortalPath(filePath: string): string {
75 return path.relative(ROOT_DIR, filePath).replace(/\\/g, '/');
76 }
77
78 function isExcluded(portalPath: string): boolean {
79 return Array.from(EXCLUDED_DIRS).some((dir) => portalPath === dir || portalPath.startsWith(`${dir}/`));
80 }
81
82 function isAllowedFile(portalPath: string): boolean {
83 if (isExcluded(portalPath)) return false;
84 if (portalPath.includes('/.')) return false;
85 if (portalPath.endsWith('package-lock.json')) return false;
86 const ext = path.extname(portalPath);
87 return ALLOWED_EXTENSIONS.has(ext);
88 }
89
90 function isSensitiveFile(portalPath: string): boolean {
91 if (FULL_FILE_REDACTIONS.includes(portalPath)) return true;
92 if (AUTO_FILE_REDACTION_PATTERNS.some((pattern) => pattern.test(portalPath))) return true;
93
94 const ext = path.extname(portalPath);
95 if (ext === '.json' && !PUBLIC_JSON_FILES.has(portalPath)) return true;
96
97 return false;
98 }
99
100 function walkDirectory(dir: string, entries: PortalFileEntry[]): void {
101 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
102 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
103
104 for (const item of items) {
105 const fullPath = path.join(dir, item.name);
106 const portalPath = toPortalPath(fullPath);
107 if (isExcluded(portalPath)) continue;
108
109 if (item.isDirectory()) {
110 walkDirectory(fullPath, entries);
111 continue;
112 }
113
114 if (!item.isFile() || !isAllowedFile(portalPath)) continue;
115 entries.push({
116 path: portalPath,
117 name: path.basename(portalPath),
118 directory: path.dirname(portalPath).replace(/\\/g, '/'),
119 sensitive: isSensitiveFile(portalPath)
120 });
121 }
122 }
123
124 export function getPortalFiles(): PortalFileEntry[] {
125 const entries: PortalFileEntry[] = [];
126
127 INCLUDE_ROOTS.forEach((root) => walkDirectory(path.join(ROOT_DIR, root), entries));
128 INCLUDE_FILES.forEach((file) => {
129 const fullPath = path.join(ROOT_DIR, file);
130 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
131 entries.push({
132 path: file,
133 name: path.basename(file),
134 directory: path.dirname(file) === '.' ? 'root' : path.dirname(file),
135 sensitive: isSensitiveFile(file)
136 });
137 });
138
139 const unique = new Map(entries.map((entry) => [entry.path, entry]));
140 return Array.from(unique.values()).sort((a, b) => a.path.localeCompare(b.path));
141 }
142
143 export function getPortalFileView(requestedPath: string | undefined): PortalFileView {
144 const files = getPortalFiles();
145 const entry = files.find((item) => item.path === requestedPath) || files[0];
146
147 if (!entry) {
148 return {
149 entry: { path: '', name: '', directory: '', sensitive: false },
150 lines: [],
151 redactedFile: false,
152 notice: '공개 가능한 파일이 없습니다.'
153 };
154 }
155
156 if (entry.sensitive) {
157 return {
158 entry,
159 redactedFile: true,
160 notice: '이 파일은 인증/세션/외부 연동 등 보안상 민감한 구현을 포함하여 공개용 코드 뷰어에서는 내용을 비공개 처리했습니다.',
161 lines: [
162 { number: 1, text: '/* SECURITY REDACTED */', redacted: true },
163 { number: 2, text: '파일명과 위치는 공개하지만, 본문 코드는 보안상 표시하지 않습니다.', redacted: true }
164 ]
165 };
166 }
167
168 const fullPath = path.join(ROOT_DIR, entry.path);
169 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
170 if (stat.size > 120 * 1024) {
171 return {
172 entry,
173 redactedFile: true,
174 notice: '파일이 커서 공개용 코드 뷰어에서는 요약만 표시합니다.',
175 lines: [{ number: 1, text: `/* Large file omitted: ${entry.path} */`, redacted: true }]
176 };
177 }
178
179 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
180 const lines = raw.split('\n').map((line, index) => {
181 const redacted = REDACT_LINE_PATTERNS.some((pattern) => pattern.test(line));
182 return {
183 number: index + 1,
184 text: redacted ? '[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.' : line,
185 redacted
186 };
187 });
188
189 return {
190 entry,
191 lines,
192 redactedFile: false,
193 notice: null
194 };
195 }
196