Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/sync-worker-secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
OY_RELAY_TOKEN: ${{ secrets.OY_RELAY_TOKEN }}
OY_ACCESS_CLIENT_ID: ${{ secrets.OY_ACCESS_CLIENT_ID }}
OY_ACCESS_CLIENT_SECRET: ${{ secrets.OY_ACCESS_CLIENT_SECRET }}
DTRYX_RELAY_URL: ${{ secrets.DTRYX_RELAY_URL }}
DTRYX_RELAY_TOKEN: ${{ secrets.DTRYX_RELAY_TOKEN }}
DTRYX_ACCESS_CLIENT_ID: ${{ secrets.DTRYX_ACCESS_CLIENT_ID }}
DTRYX_ACCESS_CLIENT_SECRET: ${{ secrets.DTRYX_ACCESS_CLIENT_SECRET }}
GS25_API_KEY: ${{ secrets.GS25_API_KEY }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
NAVER_CLIENT_ID: ${{ secrets.NAVER_CLIENT_ID }}
Expand All @@ -57,6 +61,10 @@ jobs:
put_secret_if_set OY_RELAY_TOKEN "$OY_RELAY_TOKEN"
put_secret_if_set OY_ACCESS_CLIENT_ID "$OY_ACCESS_CLIENT_ID"
put_secret_if_set OY_ACCESS_CLIENT_SECRET "$OY_ACCESS_CLIENT_SECRET"
put_secret_if_set DTRYX_RELAY_URL "$DTRYX_RELAY_URL"
put_secret_if_set DTRYX_RELAY_TOKEN "$DTRYX_RELAY_TOKEN"
put_secret_if_set DTRYX_ACCESS_CLIENT_ID "$DTRYX_ACCESS_CLIENT_ID"
put_secret_if_set DTRYX_ACCESS_CLIENT_SECRET "$DTRYX_ACCESS_CLIENT_SECRET"
put_secret_if_set GS25_API_KEY "$GS25_API_KEY"
put_secret_if_set GOOGLE_MAPS_API_KEY "$GOOGLE_MAPS_API_KEY"
put_secret_if_set NAVER_CLIENT_ID "$NAVER_CLIENT_ID"
Expand Down
55 changes: 55 additions & 0 deletions docs/dtryx-relay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 디트릭스 Mac HTTPS 중계

## 구성
Cloudflare에서 디트릭스로 나가는 연결이 실패할 때 Mac의 일반 HTTPS 요청을 사용한다. 브라우저·탭·Zyte는 사용하지 않는다. 원본 네트워크 장애를 고치는 것이 아니라 검증된 Mac 출발 경로를 이용하는 운영 구성이다.

```text
MCP / REST → Cloudflare Worker
→ Cloudflare Access + Tunnel
→ 127.0.0.1:4320 디트릭스 프로세스
→ https://api.dtryx.com (공개 조회 GET)
```

Worker 설정이 없으면 기존 직접 요청을 유지한다. 설정이 있으면 중계만 사용하며 실패 뒤 원본 재시도나 유료 폴백은 하지 않는다.

## Worker 설정
- `DTRYX_RELAY_URL`: 중계 기본 HTTPS URL. 로컬 시험만 localhost/127.0.0.1 HTTP 허용.
- `DTRYX_RELAY_TOKEN`: 디트릭스 전용 Bearer 토큰.
- `DTRYX_ACCESS_CLIENT_ID`, `DTRYX_ACCESS_CLIENT_SECRET`: Access 서비스 토큰 쌍.

Access 서비스 자격증명은 중계에만 보내고 원본 디트릭스에는 전달하지 않는다. 리다이렉트를 따라가지 않는다. `/health`의 `config.dtryxRelay`로 비밀값 없이 설정을 확인한다. GitHub 시크릿도 같은 이름으로 보관하고 Sync Worker Secrets 워크플로로 복구할 수 있다.

## Mac 실행
Node24와 프로젝트 의존성이 필요하다. 환경 파일은0600, 상태 디렉터리는0700으로 제한한다.

```sh
node --max-old-space-size=128 --env-file=/absolute/path/dtryx.env \
--import tsx scripts/relay/dtryx-start.ts
```

`dtryx.env`에는 `DTRYX_RELAY_TOKEN`, `DTRYX_RELAY_STATE_DIR` 절대 경로를 넣는다. `DTRYX_RELAY_PORT`는 생략 시4320이다. 설정 파일의 토큰을 버전 관리에 넣지 않는다. 올리브영과 다른 상태 디렉터리를 사용한다.

현재 Mac 설치는 `~/Library/Application Support/DaisoRelay/dtryx-runtime`, `dtryx.env`, `dtryx-state`를 사용하고 `page.aka.daiso-dtryx` LaunchAgent로 관리한다. GUI 사용자 로그인 세션이 필요하다. Node 힙128MiB 제한은 전체 프로세스 RSS128MiB 보장이 아니다.

## Tunnel 경로
기존 `oy-relay.aka.page` Access 정책을 적용한 상태에서 기존 올리브영 규칙 앞에 다음 ingress를 배치한다.

```json
{"hostname":"oy-relay.aka.page","path":"^/v1/dtryx/.*$","service":"http://127.0.0.1:4320"}
```

기존4319 올리브영 ingress 및 마지막404 규칙을 유지한다. 디트릭스 프로세스는 loopback에만 바인딩한다. Access 외에 핸들러 자체도 모든 경로에서 디트릭스 Bearer 토큰을 검증한다.

## 허용 계약과 상한
- `POST /v1/dtryx/timetable`: `brandCode`, `cinemaCode`, 유효한 `playDate`(YYYYMMDD).
- `POST /v1/dtryx/play-dates`, `POST /v1/dtryx/movies`: `brandCode`, `cinemaCode`.
- `GET /v1/dtryx/health`: 인증된 상태 조회.
- 임의URL·헤더·추가필드·알 수 없는 경로는 거절한다.
- 동시4개, 대기 포함32개, 전체15초 제한. 요청 본문16KiB, 원본 응답2MiB.
- 별도 파일 원장으로 분당30회·UTC일3000회를 제한한다. 재시작해도 유지되며 원장 장애는 차단한다.
- 성공은 원본의 `RetCode=success` 및 유효한 `Recordset`을 확인한 뒤 반환한다. 실패나 미확인 좌석을 정상 데이터로 만들지 않는다.

## 점검과 복구
먼저 로컬 인증 health와 세 API를 확인한 뒤 Access를 통한 같은 요청, 운영 REST/MCP를 확인한다. 올리브영의 health·실제 조회·탭 수를 함께 확인한다. 강제 종료 후 LaunchAgent 재시작 및 사용량 원장 보존을 확인한다.

중계를 중단할 때는 Worker의 디트릭스 설정을 제거하면 직접 요청으로 돌아가지만, 원본 연결 장애가 재현될 수 있다. 토큰을 Worker/GitHub/로컬에서 함께 교체한다. 올리브영 토큰이나 상태 파일을 덮어쓰지 않는다.
24 changes: 24 additions & 0 deletions docs/superpowers/plans/2026-09-28-dtryx-relay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 디트릭스 전용 HTTPS 중계 구현 계획

## 승인된 목표와 설계
사용자가 Mac 출발 고정 API 중계를 승인했다. Cloudflare 원본 연결 실패를 피하기 위해 디트릭스만 별도 브라우저 없는 프로세스127.0.0.1:4320으로 중계한다. 기존 Tunnel/Access를 공유하며 토큰·프로세스·큐·파일 사용량 원장은 분리한다. 직접 CLI는 설정이 없으면 기존 원본 조회를 유지하고 Worker REST/MCP는 명시적 DTRYX 설정을 주입한다. 운영 변경 전 회귀·보안·수명주기 검증을 완료한다.

## 경로 계약
- POST /v1/dtryx/timetable: {brandCode, cinemaCode, playDate(YYYYMMDD)}
- POST /v1/dtryx/play-dates 및 /v1/dtryx/movies: {brandCode, cinemaCode}
- GET /v1/dtryx/health: 인증 상태 확인
- 성공 응답은 기존 원본 RetCode/Recordset 그대로. 임의URL/헤더/추가필드/리다이렉트 불허.
- brandCode는 영문소문자로 시작하는 영문소문자/숫자/_/- 최대32자, cinemaCode는6자리 숫자, 날짜는 유효한 YYYYMMDD.
- 인증 Bearer 비교, 본문16KiB/응답2MiB/전체15초, 동시4·대기포함32개, 원장30회/분·3000회/UTC일. 취소·실패 뒤 슬롯 회수.
- DTRYX_RELAY_URL/TOKEN 및 DTRYX_ACCESS_CLIENT_ID/SECRET. 운영URL은 기존oy-relay.aka.page, 별도 Bearer토큰과 기존 Access서비스 자격증명 사용.

## 실행 순서
- [x] TDD로 Mac 핸들러/고정HTTPS 호출·상한·취소·별도 시작점 구현.
- [x] TDD로 client/options·REST·MCP·설정진단·시크릿동기화 연결. 원본 경로와 오류 의미 보존.
- [x] 독립 검토, 전체check/coverage100/build/audit/pack.
- [x] Mac LaunchAgent 설치·실제조회·인증거부·재시작 검증 후 Tunnel경로 추가.
- [ ] Worker/GitHub 시크릿 설정, PR CI/병합/배포/npm 릴리스.
- [ ] 운영 REST/MCP 반복 조회, OY 회귀·브라우저수 불변 확인. 결과와 한계 기록.

## 운영 경계
Zyte 미사용. OY브라우저코드/큐 변경 없음. 디트릭스 source3개 공개GET API만 호출. max-old-space-size128MiB, 요청/응답 상한, 외부신호종료시 HTTP연결 정리. 사용량원장은 재시작에도 유지하며 오류시failclosed. 원본 네트워크 문제 자체가 해결됐다고 주장하지 않는다.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "daiso",
"version": "1.2.4",
"version": "1.2.5",
"description": "Korean retail and cinema MCP server, CLI, and Codex Skill for Daiso, convenience stores, marts, Olive Young, and theaters",
"main": "dist/index.js",
"bin": {
Expand Down
65 changes: 65 additions & 0 deletions scripts/relay/dtryx-start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/** 브라우저 없이 별도 프로세스로 디트릭스 중계를 실행합니다. */
import { createServer } from 'node:http';
import { Readable } from 'node:stream';
import { mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { createFileQuota } from './quota.js';
import { createDtryxRelay } from './dtryx.js';

async function main() {
const token = process.env.DTRYX_RELAY_TOKEN;
const stateDir = process.env.DTRYX_RELAY_STATE_DIR;
const port = Number(process.env.DTRYX_RELAY_PORT || 4320);
if (!token?.trim() || !stateDir || !Number.isInteger(port) || port < 1024 || port > 65535) {
throw new Error('Invalid relay configuration');
}
await mkdir(stateDir, { recursive: true, mode: 0o700 });
const handler = createDtryxRelay(token, {
takeQuota: await createFileQuota(join(stateDir, 'quota.json')),
});
const server = createServer(async (req, res) => {
const controller = new AbortController();
res.once('close', () => controller.abort());
try {
const response = await handler(
new Request(`http://127.0.0.1:${port}${req.url}`, {
method: req.method,
headers: req.headers as Record<string, string>,
signal: controller.signal,
body: req.method === 'GET' || req.method === 'HEAD' ? undefined : Readable.toWeb(req),
duplex: 'half',
} as RequestInit & { duplex: 'half' }),
);
res.writeHead(response.status, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
Connection: 'close',
});
res.end(await response.text());
} catch {
res.writeHead(500, { Connection: 'close' });
res.end('{"error":"Relay failure"}');
}
});
server.requestTimeout = 15000;
server.headersTimeout = 10000;
server.setTimeout(15000, (socket) => socket.destroy());
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(port, '127.0.0.1', resolve);
});
console.log(`Dtryx relay ready on 127.0.0.1:${port}`);
const close = () => {
server.close();
server.closeAllConnections();
};
process.once('SIGTERM', close);
process.once('SIGINT', close);
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(() => {
console.error('디트릭스 릴레이 시작 실패: 토큰, 포트 및 상태 경로를 확인하세요.');
process.exitCode = 1;
});
}
Loading
Loading