fix(sync): bind sync engine to its user DB, scope instructions/templates per account, harden navigation

This commit is contained in:
Yun Chan 2026-09-28 00:53:42 +09:00
parent 1b8fe445f3
commit 9aa7302944
30 changed files with 2614 additions and 386 deletions

View file

@ -0,0 +1,59 @@
// packages/core/src/url-policy.ts
// 외부 URL·내비게이션 정책 SSOT. 순수 함수만 둔다 — Electron 어댑터는 데스크톱 main이 맡는다.
//
// - 앱 밖으로 넘기는 URL(OS 기본 핸들러)은 허용 scheme만 연다. file:, UNC, ms-*, search-ms: 같은
// OS 핸들러는 원격 코드 실행 경로가 되므로 막는다.
// - 앱 창은 앱 자신의 오리진 밖으로 이동하지 않는다(원격 페이지가 preload API를 얻지 못하게).
export interface ExternalUrlPolicyOptions {
/** 허용할 scheme(콜론 포함, 소문자). 기본 https:·mailto: */
allowSchemes?: readonly string[]
/** 추가로 허용할 정확한 오리진(예: 개발용 http://localhost:3000). */
allowOrigins?: readonly string[]
}
export const DEFAULT_EXTERNAL_SCHEMES: readonly string[] = ['https:', 'mailto:']
function parse(url: string): URL | null {
if (typeof url !== 'string' || url.trim() === '' || url.length > 8192) return null
// 백슬래시로 시작하는 UNC(\\server\share)는 URL 파서가 상대 경로로 보지 않도록 먼저 거른다.
if (url.startsWith('\\\\') || url.startsWith('//')) return null
try {
return new URL(url)
} catch {
return null
}
}
/** OS 기본 핸들러(브라우저·메일)로 넘겨도 되는 URL인지. */
export function isAllowedExternalUrl(url: string, options: ExternalUrlPolicyOptions = {}): boolean {
const parsed = parse(url)
if (!parsed) return false
const origin = parsed.origin
if (options.allowOrigins?.some((allowed) => allowed === origin)) return true
const schemes = options.allowSchemes ?? DEFAULT_EXTERNAL_SCHEMES
if (!schemes.includes(parsed.protocol.toLowerCase())) return false
if (parsed.protocol === 'https:') {
// 자격 증명이 박힌 URL(https://user@evil)이나 호스트 없는 URL은 열지 않는다.
if (!parsed.hostname || parsed.username || parsed.password) return false
}
return true
}
/**
* URL이 앱 자신의 페이지인지. appOrigins는 'file://' 또는 'http://localhost:5173' 같은 오리진.
* file: 은 오리진이 'null'이라 scheme으로 비교한다.
*/
export function isAppOrigin(url: string, appOrigins: readonly string[]): boolean {
const parsed = parse(url)
if (!parsed) return false
for (const allowed of appOrigins) {
if (allowed === 'file://') {
if (parsed.protocol === 'file:') return true
continue
}
const allowedParsed = parse(allowed)
if (allowedParsed && allowedParsed.origin === parsed.origin) return true
}
return false
}