Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
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