본문으로 건너뛰기
Public Source Viewer

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

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

Redacted View
src/services/novel-handoff.service.ts
공개 가능
1 import fs from 'fs';
2 import path from 'path';
3 import { createHash, randomBytes } from 'crypto';
4 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
5 import { parseProject } from '../shared/novel-reader';
6 export type HandoffRequest = { callback: string; digest: string; title: string; author: string; chapters: number; characters: number };
7 export type HandoffTicket = HandoffRequest & { expiresAt: number; username: string };
8 const TTL = 5 * 60 * 1000;
9 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
10 export function parseHandoffRequest(value: any): HandoffRequest {
11 if (!value || typeof value.callback !== 'string') throw new Error('Novel 앱에서 게시를 시작해 주세요.');
12 let callback: URL; try { callback = new URL(value.callback); } catch { throw new Error('앱의 반환 주소가 올바르지 않습니다.'); }
13 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
14 if (typeof value.digest !== 'string' || !/^[0-9a-f]{64}$/.test(value.digest)) throw new Error('원고 확인 값이 올바르지 않습니다.');
15 if (typeof value.title !== 'string' || !value.title.trim() || value.title.length > 300 || typeof value.author !== 'string' || value.author.length > 300) throw new Error('작품 정보가 올바르지 않습니다.');
16 const chapters = Number(value.chapters), characters = Number(value.characters);
17 if (!Number.isInteger(chapters) || chapters < 1 || chapters > 500 || !Number.isInteger(characters) || characters < 1 || characters > 28 * 1024 * 1024) throw new Error('작품 분량이 올바르지 않습니다.');
18 return { callback: callback.href, digest: value.digest, title: value.title, author: value.author, chapters, characters };
19 }
20 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
21 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
22 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
23 }
24 function cleanup() {
25 fs.mkdirSync(directory(), { recursive: true });
26 const cutoff = Date.now() - TTL;
27 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
28 if (!/^[a-f0-9]{64}\.(json|used)$/.test(name)) continue;
29 const file = path.join(directory(), name);
30 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
31 }
32 }
33 export function issueHandoff(input: HandoffRequest, username: string) {
34 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
35 const request = parseHandoffRequest(input); cleanup();
36 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
37 const ticket: HandoffTicket = { ...request, username, expiresAt: Date.now() + TTL };
38 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
39 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
40 }
41 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
42 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
43 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
44 if (ticket.expiresAt <= Date.now()) { fs.rmSync(file, { force: true }); throw new Error('게시 승인이 만료되었습니다. 앱에서 다시 시작해 주세요.'); }
45 if (requestOrigin !== new URL(ticket.callback).origin) throw new Error('승인한 앱에서 보낸 요청이 아닙니다.');
46 return ticket;
47 }
48 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
49 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
50 if (typeof raw !== 'string' || Buffer.byteLength(raw) > 28 * 1024 * 1024 || createHash('sha256').update(raw).digest('hex') !== ticket.digest) throw new Error('승인한 원고와 전달된 원고가 일치하지 않습니다.');
51 const p = parseProject(raw);
52 if (p.title !== ticket.title || p.author !== ticket.author || p.chapters.length !== ticket.chapters || p.chapters.reduce((sum, c) => sum + Array.from(c.text).length, 0) !== ticket.characters) throw new Error('브라우저에서 확인한 작품 정보와 원고가 일치하지 않습니다.');
53 // Atomic claim prevents two requests from consuming the same browser approval.
54 try { fs.renameSync(file, file.replace(/\.json$/, '.used')); } catch { throw new Error('이미 처리된 게시 승인입니다.'); }
55 fs.rmSync(file.replace(/\.json$/, '.used'), { force: true });
56 return ticket;
57 }
58