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/ai.routes.ts',
42
'src/routes/upload.routes.ts',
43
'src/routes/backup.routes.ts',
44
'src/routes/security-dashboard.routes.ts',
45
'src/routes/discord.routes.ts',
46
'src/routes/auth.routes.ts',
47
'src/middleware/security.middleware.ts',
48
'src/middleware/session.middleware.ts',
49
'src/services/file-session-store.service.ts',
50
'src/services/safe-http.service.ts',
51
'src/services/backup.service.ts',
52
'src/services/security-dashboard.service.ts',
53
'src/services/push.service.ts',
54
'view/hinana/ai.ejs',
55
'view/hinana/aiNotes.ejs',
56
'view/hinana/aiView.ejs',
57
'view/hinana/backups.ejs',
58
'view/hinana/securityDashboard.ejs'
59
];
60
const AUTO_FILE_REDACTION_PATTERNS = [
61
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
62
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
63
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
64
/firebase.*admin/i,
65
/service[-_]?account/i,
66
/admin[-_]?sdk/i
67
];
68
const REDACT_LINE_PATTERNS = [
69
/process\.env/i,
70
/\bfs\.(readFile|readFileSync|writeFile|writeFileSync|readdir|readdirSync|existsSync|statSync|statfsSync)\b/i,
71
/\bJSON\.(parse|stringify)\b/,
72
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
73
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
74
/api[_-]?key/i,
75
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
76
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
77
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
78
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
79
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
80
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
81
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
82
/username\s*[!=]={1,2}\s*['"`][^'"`]+['"`]/,
83
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
84
/\b\d{15,22}\b/
85
];
86
87
function toPortalPath(filePath: string): string {
88
return path.relative(ROOT_DIR, filePath).replace(/\\/g, '/');
89
}
90
91
function isExcluded(portalPath: string): boolean {
92
return Array.from(EXCLUDED_DIRS).some((dir) => portalPath === dir || portalPath.startsWith(`${dir}/`));
93
}
94
95
function isAllowedFile(portalPath: string): boolean {
96
if (isExcluded(portalPath)) return false;
97
if (portalPath.includes('/.')) return false;
98
if (portalPath.endsWith('package-lock.json')) return false;
99
const ext = path.extname(portalPath);
100
return ALLOWED_EXTENSIONS.has(ext);
101
}
102
103
function isSensitiveFile(portalPath: string): boolean {
104
if (FULL_FILE_REDACTIONS.includes(portalPath)) return true;
105
if (AUTO_FILE_REDACTION_PATTERNS.some((pattern) => pattern.test(portalPath))) return true;
106
107
const ext = path.extname(portalPath);
108
if (ext === '.json' && !PUBLIC_JSON_FILES.has(portalPath)) return true;
109
110
return false;
111
}
112
113
function walkDirectory(dir: string, entries: PortalFileEntry[]): void {
114
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
115
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
116
117
for (const item of items) {
118
const fullPath = path.join(dir, item.name);
119
const portalPath = toPortalPath(fullPath);
120
if (isExcluded(portalPath)) continue;
121
122
if (item.isDirectory()) {
123
walkDirectory(fullPath, entries);
124
continue;
125
}
126
127
if (!item.isFile() || !isAllowedFile(portalPath)) continue;
128
entries.push({
129
path: portalPath,
130
name: path.basename(portalPath),
131
directory: path.dirname(portalPath).replace(/\\/g, '/'),
132
sensitive: isSensitiveFile(portalPath)
133
});
134
}
135
}
136
137
export function getPortalFiles(): PortalFileEntry[] {
138
const entries: PortalFileEntry[] = [];
139
140
INCLUDE_ROOTS.forEach((root) => walkDirectory(path.join(ROOT_DIR, root), entries));
141
INCLUDE_FILES.forEach((file) => {
142
const fullPath = path.join(ROOT_DIR, file);
143
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
144
entries.push({
145
path: file,
146
name: path.basename(file),
147
directory: path.dirname(file) === '.' ? 'root' : path.dirname(file),
148
sensitive: isSensitiveFile(file)
149
});
150
});
151
152
const unique = new Map(entries.map((entry) => [entry.path, entry]));
153
return Array.from(unique.values()).sort((a, b) => a.path.localeCompare(b.path));
154
}
155
156
export function getPortalFileView(requestedPath: string | undefined): PortalFileView {
157
const files = getPortalFiles();
158
const entry = files.find((item) => item.path === requestedPath) || files[0];
159
160
if (!entry) {
161
return {
162
entry: { path: '', name: '', directory: '', sensitive: false },
163
lines: [],
164
redactedFile: false,
165
notice: '공개 가능한 파일이 없습니다.'
166
};
167
}
168
169
if (entry.sensitive) {
170
return {
171
entry,
172
redactedFile: true,
173
notice: '이 파일은 인증/세션/외부 연동 등 보안상 민감한 구현을 포함하여 공개용 코드 뷰어에서는 내용을 비공개 처리했습니다.',
174
lines: [
175
{ number: 1, text: '/* SECURITY REDACTED */', redacted: true },
176
{ number: 2, text: '파일명과 위치는 공개하지만, 본문 코드는 보안상 표시하지 않습니다.', redacted: true }
177
]
178
};
179
}
180
181
const fullPath = path.join(ROOT_DIR, entry.path);
182
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
183
if (stat.size > 120 * 1024) {
184
return {
185
entry,
186
redactedFile: true,
187
notice: '파일이 커서 공개용 코드 뷰어에서는 요약만 표시합니다.',
188
lines: [{ number: 1, text: `/* Large file omitted: ${entry.path} */`, redacted: true }]
189
};
190
}
191
192
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
193
const lines = raw.split('\n').map((line, index) => {
194
const redacted = REDACT_LINE_PATTERNS.some((pattern) => pattern.test(line));
195
return {
196
number: index + 1,
197
text: redacted ? '[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.' : line,
198
redacted
199
};
200
});
201
202
return {
203
entry,
204
lines,
205
redactedFile: false,
206
notice: null
207
};
208
}
209