책임: 서버가 클라이언트를 통일 제어하는 타입 커맨드 프로토콜. Python(Pydantic)=단일 진실원 → TS 타입 생성 + 커맨드 발행. 클라 = 제너릭 인터프리터. 해소: "북마크릿 업데이트 지옥" — 로직을 클라에서 제거, 전부 서버 커맨드로.
v1(현
loader.js의payload.scripteval)을 대체하는 목표 모델. 북마크릿은 기본적으로 v1·v2 동일(순수 로더, 불변) — 단 v2엔 인라인 형태(D-csp-script-src)가 추가로 있다. 상세:docs/00_ARCHITECTURE.mdD-csp-script-src.
- 북마크릿 = 순수 로더(불변):
<script src=서버/agent/loader.js?...>만. 영원히 갱신 불필요. - loader.js = 서버 제공 TS 인터프리터(ES2017 컴파일). 서버만 바꾸면 모든 클라 다음 로드 때 최신.
- 커맨드 = 타입 데이터(eval 아님). 드문 커스텀만
scriptescape-hatch. - 단일 소스 = Python Pydantic →
commands.d.ts생성(순수 파이썬 codegen, Node 런타임 無). - 컴파일 타깃 = ES2017(대상 폰 전부 지원, async 네이티브).
tsc/esbuild는 dev/CI 1회 →loader.js체크인.
서버가 큐에 넣고 클라가 처리: window._aaq.push(command). command = {cmd, ...args, v?}.
| cmd | args | 역할 |
|---|---|---|
collect |
{selector, fields[]} |
DOM 추출 |
on-click |
{selector, then: Command[]} |
엘리먼트 클릭 → 후속 커맨드 |
define-button |
{label, on: Command[]} |
UI 버튼 생성(인터랙티브 수집) |
beacon |
{to, data} |
.js/이미지 비콘 전송(CORS-free). to는 서버 오리진만(§8, D-beacon-origin) |
sync |
{cursor} |
서버 허브 date-delta pull |
set |
{key, value} |
localStorage JSON array 누적 |
script |
{code} |
escape-hatch(eval). 기본 차단 — opt-in 필요(§8, D-script-optin) |
class CollectCmd(BaseModel): cmd: Literal["collect"]; selector: str; fields: list[str]
class BeaconCmd(BaseModel): cmd: Literal["beacon"]; to: str; data: dict
Command = Annotated[CollectCmd | BeaconCmd | ..., Field(discriminator="cmd")]
# gen_dts(): Command union → docs/generated/commands.d.ts (순수 파이썬 문자열 생성)- 클라 TS가
commands.d.tsimport. Python 커맨드 변경 → 재생성 → drift 시 클라 TS 컴파일 실패로 적발. - 발행 시 서버는 같은 Pydantic 모델로 검증(잘못된 커맨드 발행 불가).
import type { Command } from "./commands"; // Python 생성
const handlers: { [K in Command["cmd"]]: (c: Command)=>void } = { collect, onClick, beacon, ... };
window._aaq = window._aaq || [];
const orig = window._aaq.push;
window._aaq.push = (c: Command) => { handlers[c.cmd]?.(c); }; // 로드 후 즉시 dispatch
(window._aaq as any).forEach?.((c)=>window._aaq.push(c)); // 버퍼 드레인- 핸들러 레지스트리만 클라에. 무엇을·언제·어떤 값은 서버 커맨드가 지시.
- 새 동작 = loader.js 재컴파일·재배포(서버만). 북마크릿·통합 무변경.
site_id= 불투명 안정 인덱스(surrogate, 예s_001). 호스트네임을 키로 직접 안 씀.sites:site_id PK · name(가변 라벨) · created_at.site_hosts:hostname PK · site_id(N:1 별칭).- 자동 매핑: 첫 호스트네임 등장 →
site_id생성·매핑. 수동: 관리자 발급·별칭 추가·name 변경. - collections는
site_id로 키 → 호스트네임 변경/사이트 재편에도 과거 데이터 불변.
beacon=<script src=...js>(script-src) 또는new Image().src=/api/v1/collect.json?d=(img-src).connect-src/CORS 무관.
- 쿠키 = cursor + 요약(작음). localStorage =
site_id별 JSON array(본체). sync커맨드: cursor(>1h)→서버가 그 사이 delta 반환→localStorage 누적·cursor 갱신. 서버=정본, 클라=캐시. 충돌 없음.
인터프리터의 신뢰 경계는 "컨트롤 서버 응답"이다. 컨트롤 서버 장악/MITM 시 주입된 모든 페이지에서 코드 실행·데이터 유출이 가능하므로, 위험한 두 커맨드에 코드 가드를 둔다.
scriptopt-in (D-script-optin):handleScript는 기본 no-op. 로더 자기src의?allowScript=1(운영자가 북마크릿/로더 URL에 직접 명시)일 때만new Function(code)()실행. 미허용 시console.warn후 무시. → "광범위 eval"을 구조적으로 비활성, 필요 시에만 명시 opt-in.requestClipboardopt-in (실험적,read_clipboard액션용): 로더 자기src에?requestClipboard=1이 명시된 경우에만, 로더 초기화 시점(북마크릿 클릭의 user-activation 윈도 안)에navigator.clipboard.readText()를 1회 호출한다 — 결과는 버리고 오직 브라우저 네이티브 clipboard-read 권한 프롬프트를 사용자 앞에 띄우는 용도. 기본 비활성(대부분 사이트는 클립보드가 불필요 — 뜬금없는 권한 팝업 방지). 사람이 그 프롬프트에서 직접 "허용"을 눌러야 승인되며, 자동화가 대신 누를 수 없다.beacon오리진 제한 (D-beacon-origin):handleBeacon은c.to가SERVER_ORIGIN으로 시작할 때만 전송. 타 오리진은 드롭+경고 → 페이지 데이터의 임의 외부 exfil 채널 차단.- 중복 주입 가드 (D-client-crypto): 같은 페이지에 v2 로더가 두 번 들어와도 자가
commands.js로드·폴링 등 네트워크 부작용은 1회만(__arachneV2Loaded). - 난수: client_id·task_id·JSONP 콜백명은
crypto.getRandomValues우선(예측 차단).
이 가드들은
loader.ts(단일 소스) →bun build→loader.v2.js산출에 박힌다. 빌드 신선도는 GT-interp/GT-build로 보증.
- ❌ 호스트네임을 키로 직접 사용(반드시 site_id 인덱스) ❌ 커맨드/로직을 클라에 하드코딩(서버 발행)
- ❌ 광범위
scripteval(opt-in 게이트 우회) ❌beacon을 타 오리진으로 전송(서버 오리진만) - ❌ Node를 Python 런타임에 의존(codegen은 순수 파이썬, tsc는 dev/CI).
- GT-cmd: 커맨드 Pydantic 검증(유효/무효) + 인터프리터 dispatch(핸들러 호출).
- GT-dts:
gen_dts()가 유효.d.ts생성; 모델 변경 시 클라 TS 컴파일 실패(drift 감지). - GT-siteid: 호스트네임→site_id 자동 매핑; name 변경 후에도 collections의 site_id 불변.