relayax-cli 0.1.995 → 0.1.997
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/install.js +2 -2
- package/dist/commands/login.js +47 -15
- package/dist/commands/publish.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/lib/api.js +7 -4
- package/dist/lib/config.d.ts +15 -0
- package/dist/lib/config.js +57 -3
- package/package.json +1 -1
package/dist/commands/install.js
CHANGED
|
@@ -28,7 +28,7 @@ function registerInstall(program) {
|
|
|
28
28
|
// 2. Visibility check
|
|
29
29
|
const visibility = team.visibility ?? 'public';
|
|
30
30
|
if (visibility === 'login-only' || visibility === 'invite-only') {
|
|
31
|
-
const token = (0, config_js_1.
|
|
31
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
32
32
|
if (!token) {
|
|
33
33
|
console.error(JSON.stringify({
|
|
34
34
|
error: 'LOGIN_REQUIRED',
|
|
@@ -123,7 +123,7 @@ function registerInstall(program) {
|
|
|
123
123
|
console.log(` \x1b[90m└${'─'.repeat(44)}┘\x1b[0m`);
|
|
124
124
|
}
|
|
125
125
|
// Follow prompt (only when logged in)
|
|
126
|
-
const token = (0, config_js_1.
|
|
126
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
127
127
|
if (authorUsername && token) {
|
|
128
128
|
try {
|
|
129
129
|
const { confirm } = await import('@inquirer/prompts');
|
package/dist/commands/login.js
CHANGED
|
@@ -37,14 +37,17 @@ async function verifyToken(token) {
|
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
function
|
|
41
|
-
return new
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
function parseFormBody(body) {
|
|
41
|
+
return new URLSearchParams(body);
|
|
42
|
+
}
|
|
43
|
+
function collectBody(req) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
47
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
48
51
|
<html><head><title>RelayAX</title></head>
|
|
49
52
|
<body style="font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f6f5f2;color:#111318">
|
|
50
53
|
<div style="text-align:center">
|
|
@@ -52,10 +55,30 @@ function waitForToken(port) {
|
|
|
52
55
|
<p>터미널로 돌아가세요. 이 창은 닫아도 됩니다.</p>
|
|
53
56
|
<script>setTimeout(()=>window.close(),2000)</script>
|
|
54
57
|
</div>
|
|
55
|
-
</body></html
|
|
58
|
+
</body></html>`;
|
|
59
|
+
function waitForToken(port) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const server = http_1.default.createServer(async (req, res) => {
|
|
62
|
+
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
63
|
+
if (url.pathname === '/callback') {
|
|
64
|
+
// POST body (secure) 또는 GET query params (하위 호환) 모두 지원
|
|
65
|
+
let params;
|
|
66
|
+
if (req.method === 'POST') {
|
|
67
|
+
const body = await collectBody(req);
|
|
68
|
+
params = parseFormBody(body);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
params = url.searchParams;
|
|
72
|
+
}
|
|
73
|
+
const token = params.get('token');
|
|
74
|
+
const refresh_token = params.get('refresh_token') ?? undefined;
|
|
75
|
+
const expires_at_raw = params.get('expires_at');
|
|
76
|
+
const expires_at = expires_at_raw ? Number(expires_at_raw) : undefined;
|
|
77
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
78
|
+
res.end(SUCCESS_HTML);
|
|
56
79
|
server.close();
|
|
57
80
|
if (token) {
|
|
58
|
-
resolve(token);
|
|
81
|
+
resolve({ token, refresh_token, expires_at });
|
|
59
82
|
}
|
|
60
83
|
else {
|
|
61
84
|
reject(new Error('토큰이 전달되지 않았습니다'));
|
|
@@ -97,14 +120,19 @@ function registerLogin(program) {
|
|
|
97
120
|
.action(async (opts) => {
|
|
98
121
|
const json = program.opts().json ?? false;
|
|
99
122
|
(0, config_js_1.ensureGlobalRelayDir)();
|
|
100
|
-
let
|
|
101
|
-
|
|
123
|
+
let accessToken = opts.token;
|
|
124
|
+
let refreshToken;
|
|
125
|
+
let expiresAt;
|
|
126
|
+
if (!accessToken) {
|
|
102
127
|
try {
|
|
103
128
|
const port = await findAvailablePort();
|
|
104
129
|
const loginUrl = `${config_js_1.API_URL}/auth/cli-login?port=${port}`;
|
|
105
130
|
console.error('브라우저에서 로그인을 진행합니다...');
|
|
106
131
|
openBrowser(loginUrl);
|
|
107
|
-
|
|
132
|
+
const loginResult = await waitForToken(port);
|
|
133
|
+
accessToken = loginResult.token;
|
|
134
|
+
refreshToken = loginResult.refresh_token;
|
|
135
|
+
expiresAt = loginResult.expires_at;
|
|
108
136
|
}
|
|
109
137
|
catch (err) {
|
|
110
138
|
const msg = err instanceof Error ? err.message : '로그인 실패';
|
|
@@ -112,8 +140,12 @@ function registerLogin(program) {
|
|
|
112
140
|
process.exit(1);
|
|
113
141
|
}
|
|
114
142
|
}
|
|
115
|
-
const user = await verifyToken(
|
|
116
|
-
(0, config_js_1.
|
|
143
|
+
const user = await verifyToken(accessToken);
|
|
144
|
+
(0, config_js_1.saveTokenData)({
|
|
145
|
+
access_token: accessToken,
|
|
146
|
+
...(refreshToken ? { refresh_token: refreshToken } : {}),
|
|
147
|
+
...(expiresAt ? { expires_at: expiresAt } : {}),
|
|
148
|
+
});
|
|
117
149
|
const result = {
|
|
118
150
|
status: 'ok',
|
|
119
151
|
message: '로그인 성공',
|
package/dist/commands/publish.js
CHANGED
|
@@ -379,7 +379,7 @@ function registerPublish(program) {
|
|
|
379
379
|
process.exit(1);
|
|
380
380
|
}
|
|
381
381
|
// Get token (checked before tarball creation)
|
|
382
|
-
const token = opts.token ?? process.env.RELAY_TOKEN ?? (0, config_js_1.
|
|
382
|
+
const token = opts.token ?? process.env.RELAY_TOKEN ?? await (0, config_js_1.getValidToken)();
|
|
383
383
|
if (!token) {
|
|
384
384
|
console.error(JSON.stringify({
|
|
385
385
|
error: 'NO_TOKEN',
|
package/dist/commands/status.js
CHANGED
|
@@ -31,7 +31,7 @@ function registerStatus(program) {
|
|
|
31
31
|
const json = program.opts().json ?? false;
|
|
32
32
|
const projectPath = process.cwd();
|
|
33
33
|
// 1. 로그인 상태
|
|
34
|
-
const token = (0, config_js_1.
|
|
34
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
35
35
|
let username;
|
|
36
36
|
if (token) {
|
|
37
37
|
username = await resolveUsername(token);
|
package/dist/commands/update.js
CHANGED
|
@@ -51,7 +51,7 @@ function registerUpdate(program) {
|
|
|
51
51
|
// Visibility check
|
|
52
52
|
const visibility = team.visibility ?? 'public';
|
|
53
53
|
if (visibility === 'login-only') {
|
|
54
|
-
const token = (0, config_js_1.
|
|
54
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
55
55
|
if (!token) {
|
|
56
56
|
console.error('이 팀은 로그인이 필요합니다. `relay login`을 먼저 실행하세요.');
|
|
57
57
|
process.exit(1);
|
package/dist/lib/api.js
CHANGED
|
@@ -8,7 +8,8 @@ exports.resolveSlugFromServer = resolveSlugFromServer;
|
|
|
8
8
|
exports.followBuilder = followBuilder;
|
|
9
9
|
const config_js_1 = require("./config.js");
|
|
10
10
|
async function fetchTeamInfo(slug) {
|
|
11
|
-
const
|
|
11
|
+
const registrySlug = slug.startsWith('@') ? slug.slice(1) : slug;
|
|
12
|
+
const url = `${config_js_1.API_URL}/api/registry/${registrySlug}`;
|
|
12
13
|
const res = await fetch(url);
|
|
13
14
|
if (!res.ok) {
|
|
14
15
|
const body = await res.text();
|
|
@@ -30,7 +31,8 @@ async function searchTeams(query, tag) {
|
|
|
30
31
|
return data.results;
|
|
31
32
|
}
|
|
32
33
|
async function fetchTeamVersions(slug) {
|
|
33
|
-
const
|
|
34
|
+
const registrySlug = slug.startsWith('@') ? slug.slice(1) : slug;
|
|
35
|
+
const url = `${config_js_1.API_URL}/api/registry/${registrySlug}/versions`;
|
|
34
36
|
const res = await fetch(url);
|
|
35
37
|
if (!res.ok) {
|
|
36
38
|
const body = await res.text();
|
|
@@ -39,7 +41,8 @@ async function fetchTeamVersions(slug) {
|
|
|
39
41
|
return res.json();
|
|
40
42
|
}
|
|
41
43
|
async function reportInstall(slug) {
|
|
42
|
-
const
|
|
44
|
+
const registrySlug = slug.startsWith('@') ? slug.slice(1) : slug;
|
|
45
|
+
const url = `${config_js_1.API_URL}/api/registry/${registrySlug}/install`;
|
|
43
46
|
await fetch(url, { method: 'POST' }).catch(() => {
|
|
44
47
|
// non-critical: ignore errors
|
|
45
48
|
});
|
|
@@ -54,7 +57,7 @@ async function resolveSlugFromServer(name) {
|
|
|
54
57
|
return data.results;
|
|
55
58
|
}
|
|
56
59
|
async function followBuilder(username) {
|
|
57
|
-
const token = (0, config_js_1.
|
|
60
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
58
61
|
const headers = {
|
|
59
62
|
'Content-Type': 'application/json',
|
|
60
63
|
};
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -11,8 +11,23 @@ export declare function getInstallPath(override?: string): string;
|
|
|
11
11
|
export declare function ensureGlobalRelayDir(): void;
|
|
12
12
|
/** cwd/.relay/ — 프로젝트 로컬 (installed.json, teams/) */
|
|
13
13
|
export declare function ensureProjectRelayDir(): void;
|
|
14
|
+
export interface TokenData {
|
|
15
|
+
access_token: string;
|
|
16
|
+
refresh_token?: string;
|
|
17
|
+
expires_at?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function loadTokenData(): TokenData | undefined;
|
|
14
20
|
export declare function loadToken(): string | undefined;
|
|
21
|
+
export declare function saveTokenData(data: TokenData): void;
|
|
15
22
|
export declare function saveToken(token: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* 유효한 access_token을 반환한다.
|
|
25
|
+
* 1. 저장된 토큰이 없으면 undefined
|
|
26
|
+
* 2. expires_at이 아직 유효하면 access_token 반환
|
|
27
|
+
* 3. 만료되었으면 refresh_token으로 갱신 시도
|
|
28
|
+
* 4. 갱신 실패 시 undefined (재로그인 필요)
|
|
29
|
+
*/
|
|
30
|
+
export declare function getValidToken(): Promise<string | undefined>;
|
|
16
31
|
/** 프로젝트 로컬 installed.json 읽기 (unscoped 키 자동 마이그레이션) */
|
|
17
32
|
export declare function loadInstalled(): InstalledRegistry;
|
|
18
33
|
/**
|
package/dist/lib/config.js
CHANGED
|
@@ -7,8 +7,11 @@ exports.API_URL = void 0;
|
|
|
7
7
|
exports.getInstallPath = getInstallPath;
|
|
8
8
|
exports.ensureGlobalRelayDir = ensureGlobalRelayDir;
|
|
9
9
|
exports.ensureProjectRelayDir = ensureProjectRelayDir;
|
|
10
|
+
exports.loadTokenData = loadTokenData;
|
|
10
11
|
exports.loadToken = loadToken;
|
|
12
|
+
exports.saveTokenData = saveTokenData;
|
|
11
13
|
exports.saveToken = saveToken;
|
|
14
|
+
exports.getValidToken = getValidToken;
|
|
12
15
|
exports.loadInstalled = loadInstalled;
|
|
13
16
|
exports.migrateInstalled = migrateInstalled;
|
|
14
17
|
exports.saveInstalled = saveInstalled;
|
|
@@ -52,20 +55,71 @@ function ensureProjectRelayDir() {
|
|
|
52
55
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
|
-
function
|
|
58
|
+
function loadTokenData() {
|
|
56
59
|
const tokenFile = path_1.default.join(GLOBAL_RELAY_DIR, 'token');
|
|
57
60
|
if (!fs_1.default.existsSync(tokenFile))
|
|
58
61
|
return undefined;
|
|
59
62
|
try {
|
|
60
|
-
|
|
63
|
+
const raw = fs_1.default.readFileSync(tokenFile, 'utf-8').trim();
|
|
64
|
+
if (!raw)
|
|
65
|
+
return undefined;
|
|
66
|
+
// JSON 형식 (새 포맷)
|
|
67
|
+
if (raw.startsWith('{')) {
|
|
68
|
+
return JSON.parse(raw);
|
|
69
|
+
}
|
|
70
|
+
// plain text (기존 포맷) — 호환성 유지
|
|
71
|
+
return { access_token: raw };
|
|
61
72
|
}
|
|
62
73
|
catch {
|
|
63
74
|
return undefined;
|
|
64
75
|
}
|
|
65
76
|
}
|
|
77
|
+
function loadToken() {
|
|
78
|
+
return loadTokenData()?.access_token;
|
|
79
|
+
}
|
|
80
|
+
function saveTokenData(data) {
|
|
81
|
+
ensureGlobalRelayDir();
|
|
82
|
+
const tokenFile = path_1.default.join(GLOBAL_RELAY_DIR, 'token');
|
|
83
|
+
fs_1.default.writeFileSync(tokenFile, JSON.stringify(data), { mode: 0o600 });
|
|
84
|
+
}
|
|
66
85
|
function saveToken(token) {
|
|
67
86
|
ensureGlobalRelayDir();
|
|
68
|
-
|
|
87
|
+
const tokenFile = path_1.default.join(GLOBAL_RELAY_DIR, 'token');
|
|
88
|
+
fs_1.default.writeFileSync(tokenFile, JSON.stringify({ access_token: token }), { mode: 0o600 });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 유효한 access_token을 반환한다.
|
|
92
|
+
* 1. 저장된 토큰이 없으면 undefined
|
|
93
|
+
* 2. expires_at이 아직 유효하면 access_token 반환
|
|
94
|
+
* 3. 만료되었으면 refresh_token으로 갱신 시도
|
|
95
|
+
* 4. 갱신 실패 시 undefined (재로그인 필요)
|
|
96
|
+
*/
|
|
97
|
+
async function getValidToken() {
|
|
98
|
+
const data = loadTokenData();
|
|
99
|
+
if (!data)
|
|
100
|
+
return undefined;
|
|
101
|
+
// expires_at이 없거나 아직 유효하면 (30초 여유) 그대로 사용
|
|
102
|
+
if (!data.expires_at || data.expires_at > Date.now() / 1000 + 30) {
|
|
103
|
+
return data.access_token;
|
|
104
|
+
}
|
|
105
|
+
// 만료됨 — refresh 시도
|
|
106
|
+
if (!data.refresh_token)
|
|
107
|
+
return undefined;
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(`${exports.API_URL}/api/auth/refresh`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'Content-Type': 'application/json' },
|
|
112
|
+
body: JSON.stringify({ refresh_token: data.refresh_token }),
|
|
113
|
+
});
|
|
114
|
+
if (!res.ok)
|
|
115
|
+
return undefined;
|
|
116
|
+
const refreshed = (await res.json());
|
|
117
|
+
saveTokenData(refreshed);
|
|
118
|
+
return refreshed.access_token;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
69
123
|
}
|
|
70
124
|
/** 프로젝트 로컬 installed.json 읽기 (unscoped 키 자동 마이그레이션) */
|
|
71
125
|
function loadInstalled() {
|