@athsra/cli 1.1.3 → 1.1.4
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/package.json +1 -1
- package/src/commands/login.ts +20 -5
- package/src/lib/oidc-flow.ts +80 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@athsra/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "athsra CLI — E2EE secret manager on Cloudflare edge. Doppler-style dev UX + zero-knowledge encryption + soft-delete + version history. MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
package/src/commands/login.ts
CHANGED
|
@@ -16,7 +16,12 @@ import { readPlain } from '../lib/envelope.ts';
|
|
|
16
16
|
import { ensureKeypair } from '../lib/identity-key.ts';
|
|
17
17
|
import { probeKeyring, setDeviceToken, setMasterPw, setToken } from '../lib/keyring.ts';
|
|
18
18
|
import { consumeLegacySession } from '../lib/legacy-session.ts';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
openBrowser,
|
|
21
|
+
resolveOidcEndpoints,
|
|
22
|
+
runOidcPkceFlow,
|
|
23
|
+
SSO_DEFAULTS,
|
|
24
|
+
} from '../lib/oidc-flow.ts';
|
|
20
25
|
import { promptConfirm, promptPassword, promptText } from '../lib/prompt.ts';
|
|
21
26
|
|
|
22
27
|
/**
|
|
@@ -81,9 +86,18 @@ async function ssoLoginCmd(): Promise<number> {
|
|
|
81
86
|
// env override 해석 후 lib/oidc-flow.ts 에 위임. 실패 시 throw → `✗ ...` + exit 1.
|
|
82
87
|
let accessToken: string;
|
|
83
88
|
try {
|
|
89
|
+
// endpoint 는 OIDC discovery 로 해석 (하드코딩 /authorize 금지 — connect 는 /sso/* 로 광고).
|
|
90
|
+
const endpoints = await resolveOidcEndpoints(
|
|
91
|
+
{ discoveryUrl: process.env.ATHSRA_SSO_DISCOVERY_URL ?? SSO_DEFAULTS.discoveryUrl },
|
|
92
|
+
{
|
|
93
|
+
authorizeUrl: process.env.ATHSRA_SSO_AUTHORIZE_URL,
|
|
94
|
+
tokenUrl: process.env.ATHSRA_SSO_TOKEN_URL,
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
console.log(` authorize: ${endpoints.authorizeUrl}`);
|
|
84
98
|
const flow = await runOidcPkceFlow({
|
|
85
|
-
authorizeUrl:
|
|
86
|
-
tokenUrl:
|
|
99
|
+
authorizeUrl: endpoints.authorizeUrl,
|
|
100
|
+
tokenUrl: endpoints.tokenUrl,
|
|
87
101
|
clientId: process.env.ATHSRA_SSO_CLIENT_ID ?? SSO_DEFAULTS.clientId,
|
|
88
102
|
scope: SSO_DEFAULTS.scope,
|
|
89
103
|
callbackTimeoutMs: SSO_DEFAULTS.callbackTimeoutMs,
|
|
@@ -424,8 +438,9 @@ const LOGIN_USAGE = [
|
|
|
424
438
|
' ATHSRA_WORKER_URL worker URL (config 우선)',
|
|
425
439
|
' ATHSRA_MASTER_PW non-interactive master pw',
|
|
426
440
|
' ATHSRA_PAPER_BACKUP_CONFIRMED=1 paper-backup prompt 우회',
|
|
427
|
-
'
|
|
428
|
-
'
|
|
441
|
+
' ATHSRA_SSO_DISCOVERY_URL OIDC discovery URL (default: login.modfolio.io/.well-known/openid-configuration)',
|
|
442
|
+
' ATHSRA_SSO_AUTHORIZE_URL authorize endpoint override (둘 다 설정 시 discovery 생략)',
|
|
443
|
+
' ATHSRA_SSO_TOKEN_URL token endpoint override (둘 다 설정 시 discovery 생략)',
|
|
429
444
|
' ATHSRA_SSO_CLIENT_ID Connect client_id (default: athsra-cli)',
|
|
430
445
|
].join('\n');
|
|
431
446
|
|
package/src/lib/oidc-flow.ts
CHANGED
|
@@ -13,11 +13,11 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
15
15
|
|
|
16
|
-
/** OIDC PKCE 플로우 엔드포인트 + 파라미터 (
|
|
16
|
+
/** OIDC PKCE 플로우 엔드포인트 + 파라미터 (endpoint 는 discovery 로 해석 후 주입). */
|
|
17
17
|
export interface OidcFlowOptions {
|
|
18
|
-
/** Connect authorize endpoint */
|
|
18
|
+
/** Connect authorize endpoint (discovery 로 해석된 값) */
|
|
19
19
|
authorizeUrl: string;
|
|
20
|
-
/** Connect token endpoint */
|
|
20
|
+
/** Connect token endpoint (discovery 로 해석된 값) */
|
|
21
21
|
tokenUrl: string;
|
|
22
22
|
/** Connect 측 CLI client (PKCE public, no secret) */
|
|
23
23
|
clientId: string;
|
|
@@ -27,15 +27,88 @@ export interface OidcFlowOptions {
|
|
|
27
27
|
callbackTimeoutMs: number;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
/**
|
|
31
|
+
* SSO 기본 설정 — endpoint 는 **discovery 로 해석**하므로 하드코딩하지 않는다.
|
|
32
|
+
*
|
|
33
|
+
* connect 는 OIDC 를 `/sso/*` 아래에서만 서빙하고 discovery 문서로 정확히 광고한다.
|
|
34
|
+
* 과거 bare `/authorize` 하드코딩이 404 의 근본 원인이었다 (connect → athsra 메시지 2026-06-15).
|
|
35
|
+
*/
|
|
36
|
+
export interface SsoConfig {
|
|
37
|
+
/** OIDC/OAuth discovery 문서 URL (OIDC Discovery / RFC 8414). connect 가 `/sso/*` 를 광고. */
|
|
38
|
+
discoveryUrl: string;
|
|
39
|
+
/** 기대 issuer (discovery host 와 분리됨 — RFC 8414). connect 의 issuer 식별자. */
|
|
40
|
+
issuer: string;
|
|
41
|
+
/** Connect 측 CLI client (PKCE public, no secret). connect 에 `athsra-cli` 로 등록. */
|
|
42
|
+
clientId: string;
|
|
43
|
+
scope: string;
|
|
44
|
+
callbackTimeoutMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const SSO_DEFAULTS: SsoConfig = {
|
|
48
|
+
discoveryUrl: 'https://login.modfolio.io/.well-known/openid-configuration',
|
|
49
|
+
issuer: 'https://connect.modfolio.io',
|
|
34
50
|
clientId: 'athsra-cli',
|
|
35
51
|
scope: 'openid profile email',
|
|
36
52
|
callbackTimeoutMs: 5 * 60 * 1000,
|
|
37
53
|
};
|
|
38
54
|
|
|
55
|
+
/** discovery 로 해석된 endpoint 쌍. */
|
|
56
|
+
export interface ResolvedEndpoints {
|
|
57
|
+
authorizeUrl: string;
|
|
58
|
+
tokenUrl: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** OIDC discovery 문서 중 우리가 쓰는 필드만의 타입 가드 (외부 입력 → unknown 검증). */
|
|
62
|
+
function hasOidcEndpoints(
|
|
63
|
+
v: unknown,
|
|
64
|
+
): v is { authorization_endpoint: string; token_endpoint: string } {
|
|
65
|
+
if (typeof v !== 'object' || v === null) return false;
|
|
66
|
+
const o = v as Record<string, unknown>;
|
|
67
|
+
return typeof o.authorization_endpoint === 'string' && typeof o.token_endpoint === 'string';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** discovery 실패 시 폴백 — discovery URL 의 origin 에 connect 의 안정 계약(`/sso/*`) 을 붙인다. */
|
|
71
|
+
function fallbackEndpoints(discoveryUrl: string): ResolvedEndpoints {
|
|
72
|
+
const { origin } = new URL(discoveryUrl);
|
|
73
|
+
return { authorizeUrl: `${origin}/sso/authorize`, tokenUrl: `${origin}/sso/token` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* authorize/token endpoint 를 OIDC **discovery 로 해석**한다 (정공법 — 하드코딩 경로 금지).
|
|
78
|
+
*
|
|
79
|
+
* 우선순위: 명시 override(둘 다 설정 시) > discovery 문서 > 폴백(`/sso/*`).
|
|
80
|
+
* 어떤 환경(dev/staging/prod)·향후 경로 변경에도 discovery 가 정답을 광고하므로 자동 적응한다.
|
|
81
|
+
* 부분 override(authorize 또는 token 한쪽)는 그 쪽만 고정하고 나머지는 discovery/폴백에서 채운다.
|
|
82
|
+
*/
|
|
83
|
+
export async function resolveOidcEndpoints(
|
|
84
|
+
cfg: Pick<SsoConfig, 'discoveryUrl'>,
|
|
85
|
+
overrides: { authorizeUrl?: string; tokenUrl?: string } = {},
|
|
86
|
+
): Promise<ResolvedEndpoints> {
|
|
87
|
+
// 명시 override 가 둘 다 있으면 discovery 생략 (오프라인/엣지 E2E).
|
|
88
|
+
if (overrides.authorizeUrl && overrides.tokenUrl) {
|
|
89
|
+
return { authorizeUrl: overrides.authorizeUrl, tokenUrl: overrides.tokenUrl };
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(cfg.discoveryUrl, { headers: { accept: 'application/json' } });
|
|
93
|
+
if (res.ok) {
|
|
94
|
+
const doc: unknown = await res.json();
|
|
95
|
+
if (hasOidcEndpoints(doc)) {
|
|
96
|
+
return {
|
|
97
|
+
authorizeUrl: overrides.authorizeUrl ?? doc.authorization_endpoint,
|
|
98
|
+
tokenUrl: overrides.tokenUrl ?? doc.token_endpoint,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// 네트워크/파싱 실패 → 폴백 (connect 의 `/sso/*` 는 안정 계약).
|
|
104
|
+
}
|
|
105
|
+
const fb = fallbackEndpoints(cfg.discoveryUrl);
|
|
106
|
+
return {
|
|
107
|
+
authorizeUrl: overrides.authorizeUrl ?? fb.authorizeUrl,
|
|
108
|
+
tokenUrl: overrides.tokenUrl ?? fb.tokenUrl,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
39
112
|
interface CallbackResult {
|
|
40
113
|
code: string;
|
|
41
114
|
state: string;
|