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.
@@ -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.loadToken)();
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.loadToken)();
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');
@@ -37,14 +37,17 @@ async function verifyToken(token) {
37
37
  return null;
38
38
  }
39
39
  }
40
- function waitForToken(port) {
41
- return new Promise((resolve, reject) => {
42
- const server = http_1.default.createServer((req, res) => {
43
- const url = new URL(req.url ?? '/', `http://localhost:${port}`);
44
- if (url.pathname === '/callback') {
45
- const token = url.searchParams.get('token');
46
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
47
- res.end(`<!DOCTYPE html>
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 token = opts.token;
101
- if (!token) {
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
- token = await waitForToken(port);
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(token);
116
- (0, config_js_1.saveToken)(token);
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: '로그인 성공',
@@ -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.loadToken)();
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',
@@ -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.loadToken)();
34
+ const token = await (0, config_js_1.getValidToken)();
35
35
  let username;
36
36
  if (token) {
37
37
  username = await resolveUsername(token);
@@ -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.loadToken)();
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 url = `${config_js_1.API_URL}/api/registry/${slug}`;
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 url = `${config_js_1.API_URL}/api/registry/${slug}/versions`;
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 url = `${config_js_1.API_URL}/api/registry/${slug}/install`;
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.loadToken)();
60
+ const token = await (0, config_js_1.getValidToken)();
58
61
  const headers = {
59
62
  'Content-Type': 'application/json',
60
63
  };
@@ -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
  /**
@@ -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 loadToken() {
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
- return fs_1.default.readFileSync(tokenFile, 'utf-8').trim() || undefined;
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
- fs_1.default.writeFileSync(path_1.default.join(GLOBAL_RELAY_DIR, 'token'), token);
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.1.995",
3
+ "version": "0.1.997",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {