opensoma 0.7.0 → 0.9.0
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/package.json +1 -3
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +6 -6
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/auth.d.ts +1 -11
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +4 -81
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/helpers.d.ts +1 -5
- package/dist/src/commands/helpers.d.ts.map +1 -1
- package/dist/src/commands/helpers.js +4 -32
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/team.js +1 -1
- package/dist/src/commands/team.js.map +1 -1
- package/dist/src/credential-manager.js +2 -2
- package/dist/src/credential-manager.js.map +1 -1
- package/dist/src/errors.d.ts +0 -4
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +1 -5
- package/dist/src/errors.js.map +1 -1
- package/dist/src/shared/utils/config-dir.d.ts +11 -0
- package/dist/src/shared/utils/config-dir.d.ts.map +1 -0
- package/dist/src/shared/utils/config-dir.js +19 -0
- package/dist/src/shared/utils/config-dir.js.map +1 -0
- package/dist/src/toz-pending-store.d.ts.map +1 -1
- package/dist/src/toz-pending-store.js +2 -2
- package/dist/src/toz-pending-store.js.map +1 -1
- package/package.json +1 -3
- package/src/client.test.ts +55 -4
- package/src/client.ts +7 -8
- package/src/commands/auth.test.ts +6 -98
- package/src/commands/auth.ts +2 -115
- package/src/commands/helpers.test.ts +5 -116
- package/src/commands/helpers.ts +3 -35
- package/src/commands/team.ts +1 -1
- package/src/credential-manager.ts +2 -2
- package/src/errors.ts +1 -5
- package/src/shared/utils/config-dir.test.ts +41 -0
- package/src/shared/utils/config-dir.ts +20 -0
- package/src/toz-pending-store.ts +3 -2
- package/dist/src/token-extractor.d.ts +0 -43
- package/dist/src/token-extractor.d.ts.map +0 -1
- package/dist/src/token-extractor.js +0 -302
- package/dist/src/token-extractor.js.map +0 -1
- package/src/token-extractor.test.ts +0 -220
- package/src/token-extractor.ts +0 -392
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const CONFIG_DIR_ENV_VAR = "OPENSOMA_CONFIG_DIR";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the directory used to persist opensoma state (credentials, pending
|
|
4
|
+
* reservations, etc.).
|
|
5
|
+
*
|
|
6
|
+
* Resolution order:
|
|
7
|
+
* 1. `OPENSOMA_CONFIG_DIR` environment variable (if set and non-empty)
|
|
8
|
+
* 2. `~/.config/opensoma`
|
|
9
|
+
*/
|
|
10
|
+
export declare function getConfigDir(): string;
|
|
11
|
+
//# sourceMappingURL=config-dir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-dir.d.ts","sourceRoot":"","sources":["../../../../src/shared/utils/config-dir.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,kBAAkB,wBAAwB,CAAA;AAEvD;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAMrC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const CONFIG_DIR_ENV_VAR = 'OPENSOMA_CONFIG_DIR';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the directory used to persist opensoma state (credentials, pending
|
|
6
|
+
* reservations, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Resolution order:
|
|
9
|
+
* 1. `OPENSOMA_CONFIG_DIR` environment variable (if set and non-empty)
|
|
10
|
+
* 2. `~/.config/opensoma`
|
|
11
|
+
*/
|
|
12
|
+
export function getConfigDir() {
|
|
13
|
+
const fromEnv = process.env[CONFIG_DIR_ENV_VAR];
|
|
14
|
+
if (fromEnv && fromEnv.length > 0) {
|
|
15
|
+
return fromEnv;
|
|
16
|
+
}
|
|
17
|
+
return join(homedir(), '.config', 'opensoma');
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=config-dir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-dir.js","sourceRoot":"","sources":["../../../../src/shared/utils/config-dir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,MAAM,CAAC,MAAM,kBAAkB,GAAG,qBAAqB,CAAA;AAEvD;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;IAC/C,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClC,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,CAAC,CAAA;AAC/C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toz-pending-store.d.ts","sourceRoot":"","sources":["../../src/toz-pending-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"toz-pending-store.d.ts","sourceRoot":"","sources":["../../src/toz-pending-store.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,OAAO,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;gBAEjB,SAAS,CAAC,EAAE,MAAM;IAKxB,GAAG,IAAI,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAW5C,GAAG,CAAC,WAAW,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWtD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { dirname, join } from 'node:path';
|
|
4
|
+
import { getConfigDir } from './shared/utils/config-dir.js';
|
|
5
5
|
export class TozPendingStore {
|
|
6
6
|
path;
|
|
7
7
|
constructor(configDir) {
|
|
8
|
-
const dir = configDir ??
|
|
8
|
+
const dir = configDir ?? getConfigDir();
|
|
9
9
|
this.path = join(dir, 'toz-pending.json');
|
|
10
10
|
}
|
|
11
11
|
async get() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toz-pending-store.js","sourceRoot":"","sources":["../../src/toz-pending-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"toz-pending-store.js","sourceRoot":"","sources":["../../src/toz-pending-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACzE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAyBxD,MAAM,OAAO,eAAe;IACT,IAAI,CAAQ;IAE7B,YAAY,SAAkB;QAC5B,MAAM,GAAG,GAAG,SAAS,IAAI,YAAY,EAAE,CAAA;QACvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAA;IAC3C,CAAC;IAED,KAAK,CAAC,GAAG;QACP,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAA;QAEvC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YAC7C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAA0B,CAAA;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,WAAkC;QAC1C,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9B,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACrC,yEAAyE;QACzE,uEAAuE;QACvE,+EAA+E;QAC/E,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,QAAQ,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;QAC3D,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QAC3E,MAAM,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACtC,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opensoma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "SWMaestro MyPage CLI & SDK for AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -59,13 +59,11 @@
|
|
|
59
59
|
"postpublish": "git checkout package.json"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"better-sqlite3": "latest",
|
|
63
62
|
"commander": "latest",
|
|
64
63
|
"node-html-parser": "latest",
|
|
65
64
|
"zod": "latest"
|
|
66
65
|
},
|
|
67
66
|
"devDependencies": {
|
|
68
|
-
"@types/better-sqlite3": "latest",
|
|
69
67
|
"@types/bun": "latest",
|
|
70
68
|
"@types/node": "latest",
|
|
71
69
|
"oxfmt": "latest",
|
package/src/client.test.ts
CHANGED
|
@@ -654,10 +654,10 @@ describe('SomaClient', () => {
|
|
|
654
654
|
const today = new Date().toISOString().slice(0, 10)
|
|
655
655
|
const todayDotted = today.replaceAll('-', '.')
|
|
656
656
|
const { http, calls } = createFakeHttp({
|
|
657
|
-
identity: { userId: 'trainee@example.com', userNm: '
|
|
657
|
+
identity: { userId: 'trainee@example.com', userNm: 'Trainee One', userNo: 'trainee-1', userGb: UserGb.Trainee },
|
|
658
658
|
getBody: (path, data) => {
|
|
659
659
|
if (path === '/mypage/myMain/dashboard.do') {
|
|
660
|
-
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> OpenSoma</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-blue label"><span>17기 연수생</span></span><div class="welcome"><strong
|
|
660
|
+
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> OpenSoma</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-blue label"><span>17기 연수생</span></span><div class="welcome"><strong>Trainee One</strong>님 안녕하세요.</div></div></div></li></ul><ul class="bbs-dash_w"><li>멘토링 · 멘토특강<li><a href="/sw/mypage/mentoLec/view.do?qustnrSn=999">네이티브 항목 접수중</a></li></li></ul>'
|
|
661
661
|
}
|
|
662
662
|
if (path === '/mypage/userAnswer/history.do') {
|
|
663
663
|
const page = Number(data?.pageIndex ?? '1')
|
|
@@ -710,6 +710,58 @@ describe('SomaClient', () => {
|
|
|
710
710
|
expect(calls.some((c) => c.path === '/mypage/mentoLec/list.do')).toBe(false)
|
|
711
711
|
})
|
|
712
712
|
|
|
713
|
+
it('uses checkLogin userGb instead of dashboard role to choose the trainee dashboard source', async () => {
|
|
714
|
+
const { http, calls } = createFakeHttp({
|
|
715
|
+
identity: { userId: 'trainee@example.com', userNm: 'Trainee One', userNo: 'trainee-1', userGb: UserGb.Trainee },
|
|
716
|
+
getBody: (path) => {
|
|
717
|
+
if (path === '/mypage/myMain/dashboard.do') {
|
|
718
|
+
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> Org</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-orange label"><span>멘토</span></span><div class="welcome"><strong>Trainee One</strong>님 안녕하세요.</div></div></div></li></ul>'
|
|
719
|
+
}
|
|
720
|
+
if (path === '/mypage/userAnswer/history.do') {
|
|
721
|
+
return '<table><tbody><tr><td>1</td><td>멘토특강</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=39">미래 특강</a></td><td>Mentor One</td><td>2099.01.01(목) 10:00:00 ~ 12:00:00</td><td>2026-04-20 22:41</td><td>[접수완료]</td><td>[OK]</td><td>-</td><td>-</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>'
|
|
722
|
+
}
|
|
723
|
+
if (path === '/mypage/myTeam/team.do') {
|
|
724
|
+
return '<ul class="bbs-team"></ul><p class="ico-team">현재 참여중인 방은 <strong class="color-blue">0</strong>/100팀 입니다</p>'
|
|
725
|
+
}
|
|
726
|
+
return ''
|
|
727
|
+
},
|
|
728
|
+
})
|
|
729
|
+
const client = new SomaClient({ http })
|
|
730
|
+
|
|
731
|
+
const dashboard = await client.dashboard.get()
|
|
732
|
+
|
|
733
|
+
expect(dashboard.role).toBe('멘토')
|
|
734
|
+
expect(dashboard.mentoringSessions.map((item) => item.url)).toEqual(['/sw/mypage/mentoLec/view.do?qustnrSn=39'])
|
|
735
|
+
expect(calls.some((c) => c.path === '/mypage/userAnswer/history.do')).toBe(true)
|
|
736
|
+
expect(calls.some((c) => c.path === '/mypage/mentoLec/list.do')).toBe(false)
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
it('excludes deleted application history rows from trainee dashboard mentoring sessions', async () => {
|
|
740
|
+
const { http } = createFakeHttp({
|
|
741
|
+
identity: { userId: 'trainee@example.com', userNm: 'Trainee One', userNo: 'trainee-1', userGb: UserGb.Trainee },
|
|
742
|
+
getBody: (path) => {
|
|
743
|
+
if (path === '/mypage/myMain/dashboard.do') {
|
|
744
|
+
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> Org</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-blue label"><span>17기 연수생</span></span><div class="welcome"><strong>Trainee One</strong>님 안녕하세요.</div></div></div></li></ul>'
|
|
745
|
+
}
|
|
746
|
+
if (path === '/mypage/userAnswer/history.do') {
|
|
747
|
+
return `<table><tbody>
|
|
748
|
+
<tr><td>2</td><td>멘토특강</td><td>Deleted Lecture</td><td>Mentor One</td><td>2099.01.01(목) 19:00:00 ~ 22:00:00</td><td>2026-04-20 22:41</td><td>[접수완료]</td><td>[OK]</td><td>삭제</td><td>-</td></tr>
|
|
749
|
+
<tr><td>1</td><td>멘토특강</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=39">Active Lecture</a></td><td>Mentor Two</td><td>2099.01.02(금) 10:00:00 ~ 12:00:00</td><td>2026-04-20 22:41</td><td>[접수완료]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
750
|
+
</tbody></table><ul class="bbs-total"><li>Total : 2</li><li>1/1 Page</li></ul>`
|
|
751
|
+
}
|
|
752
|
+
if (path === '/mypage/myTeam/team.do') {
|
|
753
|
+
return '<ul class="bbs-team"></ul><p class="ico-team">현재 참여중인 방은 <strong class="color-blue">0</strong>/100팀 입니다</p>'
|
|
754
|
+
}
|
|
755
|
+
return ''
|
|
756
|
+
},
|
|
757
|
+
})
|
|
758
|
+
const client = new SomaClient({ http })
|
|
759
|
+
|
|
760
|
+
const dashboard = await client.dashboard.get()
|
|
761
|
+
|
|
762
|
+
expect(dashboard.mentoringSessions.map((item) => item.title)).toEqual(['Active Lecture'])
|
|
763
|
+
})
|
|
764
|
+
|
|
713
765
|
it('enriches dashboard with every team the mentor belongs to', async () => {
|
|
714
766
|
const teamCard = (name: string, ownerId: string, teamId: string) =>
|
|
715
767
|
`<li><div class="top"><strong class="t"><a href="javascript:void(0);" onclick="teamPageGo('${name}','${ownerId}','${teamId}');">${name}</a></strong></div><p>팀원 3명</p><button type="button">참여중</button></li>`
|
|
@@ -748,7 +800,7 @@ describe('SomaClient', () => {
|
|
|
748
800
|
|
|
749
801
|
it('enriches trainee dashboard with every team the trainee belongs to', async () => {
|
|
750
802
|
const { http, calls } = createFakeHttp({
|
|
751
|
-
identity: { userId: 'trainee@example.com', userNm: 'Trainee One' },
|
|
803
|
+
identity: { userId: 'trainee@example.com', userNm: 'Trainee One', userNo: 'trainee-1', userGb: UserGb.Trainee },
|
|
752
804
|
getBody: (path) => {
|
|
753
805
|
if (path === '/mypage/myMain/dashboard.do') {
|
|
754
806
|
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> Org</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-blue label"><span>17기 연수생</span></span><div class="welcome"><strong>Trainee One</strong>님 안녕하세요.</div></div></div></li></ul>'
|
|
@@ -1057,7 +1109,6 @@ describe('SomaClient', () => {
|
|
|
1057
1109
|
} catch (error) {
|
|
1058
1110
|
expect(error).toBeInstanceOf(AuthenticationError)
|
|
1059
1111
|
expect((error as Error).message).toContain('opensoma auth login')
|
|
1060
|
-
expect((error as Error).message).toContain('opensoma auth extract')
|
|
1061
1112
|
}
|
|
1062
1113
|
})
|
|
1063
1114
|
|
package/src/client.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { MENU_NO } from './constants'
|
|
|
4
4
|
import { CredentialManager } from './credential-manager'
|
|
5
5
|
import { AuthenticationError } from './errors'
|
|
6
6
|
import * as formatters from './formatters'
|
|
7
|
-
import { SomaHttp, type UserIdentity } from './http'
|
|
7
|
+
import { SomaHttp, UserGb, type UserIdentity } from './http'
|
|
8
8
|
import { buildMentoringListParams, type MentoringSearchQuery } from './shared/utils/mentoring-params'
|
|
9
9
|
import {
|
|
10
10
|
buildApplicationPayload,
|
|
@@ -357,11 +357,11 @@ export class SomaClient {
|
|
|
357
357
|
|
|
358
358
|
this.dashboard = {
|
|
359
359
|
get: async () => {
|
|
360
|
-
await this.requireAuth()
|
|
360
|
+
const identity = await this.requireAuth()
|
|
361
361
|
const dashboard = formatters.parseDashboard(
|
|
362
362
|
await this.http.get('/mypage/myMain/dashboard.do', { menuNo: MENU_NO.DASHBOARD }),
|
|
363
363
|
)
|
|
364
|
-
const trainee =
|
|
364
|
+
const trainee = identity.userGb === UserGb.Trainee
|
|
365
365
|
const teamSearchField = trainee ? ('member' as const) : ('mentor' as const)
|
|
366
366
|
if (trainee) {
|
|
367
367
|
const [firstPage, teams] = await Promise.all([
|
|
@@ -598,7 +598,7 @@ export class SomaClient {
|
|
|
598
598
|
}
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
-
private async requireAuth(): Promise<
|
|
601
|
+
private async requireAuth(): Promise<UserIdentity> {
|
|
602
602
|
let identity = await this.http.checkLogin()
|
|
603
603
|
if (!identity && this.loginCredentials) {
|
|
604
604
|
await this.relogin()
|
|
@@ -608,6 +608,8 @@ export class SomaClient {
|
|
|
608
608
|
if (!identity) {
|
|
609
609
|
throw new AuthenticationError()
|
|
610
610
|
}
|
|
611
|
+
|
|
612
|
+
return identity
|
|
611
613
|
}
|
|
612
614
|
|
|
613
615
|
private async relogin(): Promise<void> {
|
|
@@ -730,14 +732,11 @@ export class SomaClient {
|
|
|
730
732
|
}
|
|
731
733
|
}
|
|
732
734
|
|
|
733
|
-
function isTraineeRole(role: string): boolean {
|
|
734
|
-
return role.includes('연수생')
|
|
735
|
-
}
|
|
736
|
-
|
|
737
735
|
function applicationHistoryToDashboardItem(
|
|
738
736
|
item: ApplicationHistoryItem,
|
|
739
737
|
): Dashboard['mentoringSessions'][number] | null {
|
|
740
738
|
if (item.applicationStatus.includes('취소')) return null
|
|
739
|
+
if (item.applicationDetail.includes('삭제')) return null
|
|
741
740
|
|
|
742
741
|
const type = applicationCategoryToMentoringType(item.category)
|
|
743
742
|
if (!type) return null
|
|
@@ -1,65 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import { inspectStoredAuthStatus
|
|
4
|
-
|
|
5
|
-
const noBrowserExtraction = async () => null
|
|
6
|
-
|
|
7
|
-
describe('resolveExtractedCredentials', () => {
|
|
8
|
-
it('returns the first candidate that validates successfully', async () => {
|
|
9
|
-
const calls: string[] = []
|
|
10
|
-
|
|
11
|
-
const credentials = await resolveExtractedCredentials(
|
|
12
|
-
[
|
|
13
|
-
{ browser: 'Chrome', lastAccessUtc: 30, profile: 'Default', sessionCookie: 'stale-session' },
|
|
14
|
-
{ browser: 'Chrome', lastAccessUtc: 20, profile: 'Profile 1', sessionCookie: 'valid-session' },
|
|
15
|
-
],
|
|
16
|
-
(sessionCookie) => ({
|
|
17
|
-
checkLogin: async () => {
|
|
18
|
-
calls.push(`check:${sessionCookie}`)
|
|
19
|
-
return sessionCookie === 'valid-session' ? { userId: 'neo', userNm: 'Neo' } : null
|
|
20
|
-
},
|
|
21
|
-
extractCsrfToken: async () => {
|
|
22
|
-
calls.push(`csrf:${sessionCookie}`)
|
|
23
|
-
return `${sessionCookie}-csrf`
|
|
24
|
-
},
|
|
25
|
-
}),
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
expect(credentials).toEqual({
|
|
29
|
-
sessionCookie: 'valid-session',
|
|
30
|
-
csrfToken: 'valid-session-csrf',
|
|
31
|
-
})
|
|
32
|
-
expect(calls).toEqual(['check:stale-session', 'check:valid-session', 'csrf:valid-session'])
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('returns null when every candidate is invalid or throws', async () => {
|
|
36
|
-
const credentials = await resolveExtractedCredentials(
|
|
37
|
-
[
|
|
38
|
-
{ browser: 'Chrome', lastAccessUtc: 30, profile: 'Default', sessionCookie: 'stale-session' },
|
|
39
|
-
{ browser: 'Edge', lastAccessUtc: 20, profile: 'Profile 1', sessionCookie: 'broken-session' },
|
|
40
|
-
],
|
|
41
|
-
(sessionCookie) => ({
|
|
42
|
-
checkLogin: async () => {
|
|
43
|
-
if (sessionCookie === 'broken-session') {
|
|
44
|
-
throw new Error('network error')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return null
|
|
48
|
-
},
|
|
49
|
-
extractCsrfToken: async () => {
|
|
50
|
-
throw new Error('should not be called')
|
|
51
|
-
},
|
|
52
|
-
}),
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
expect(credentials).toBeNull()
|
|
56
|
-
})
|
|
57
|
-
})
|
|
3
|
+
import { inspectStoredAuthStatus } from './auth'
|
|
58
4
|
|
|
59
5
|
describe('inspectStoredAuthStatus', () => {
|
|
60
|
-
it('clears session state but preserves saved id/password when
|
|
6
|
+
it('clears session state but preserves saved id/password when re-login fails', async () => {
|
|
61
7
|
let cleared = false
|
|
62
|
-
|
|
8
|
+
const postClearCredentials = {
|
|
63
9
|
sessionCookie: '',
|
|
64
10
|
csrfToken: '',
|
|
65
11
|
username: 'mentor@example.com',
|
|
@@ -93,7 +39,6 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
93
39
|
getSessionCookie: () => null,
|
|
94
40
|
getCsrfToken: () => null,
|
|
95
41
|
}),
|
|
96
|
-
noBrowserExtraction,
|
|
97
42
|
)
|
|
98
43
|
|
|
99
44
|
expect(status).toEqual({
|
|
@@ -101,7 +46,7 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
101
46
|
credentials: null,
|
|
102
47
|
clearedStaleSession: true,
|
|
103
48
|
preservedRecoveryCredentials: true,
|
|
104
|
-
hint: 'Session expired. Run: opensoma auth login
|
|
49
|
+
hint: 'Session expired. Run: opensoma auth login',
|
|
105
50
|
})
|
|
106
51
|
expect(cleared).toBe(true)
|
|
107
52
|
})
|
|
@@ -128,8 +73,6 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
128
73
|
() => ({
|
|
129
74
|
checkLogin: async () => null,
|
|
130
75
|
}),
|
|
131
|
-
undefined,
|
|
132
|
-
noBrowserExtraction,
|
|
133
76
|
)
|
|
134
77
|
|
|
135
78
|
expect(status).toEqual({
|
|
@@ -137,7 +80,7 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
137
80
|
credentials: null,
|
|
138
81
|
clearedStaleSession: true,
|
|
139
82
|
preservedRecoveryCredentials: false,
|
|
140
|
-
hint: 'Session expired. Run: opensoma auth login
|
|
83
|
+
hint: 'Session expired. Run: opensoma auth login',
|
|
141
84
|
})
|
|
142
85
|
})
|
|
143
86
|
|
|
@@ -171,46 +114,11 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
171
114
|
valid: false,
|
|
172
115
|
username: 'mentor@example.com',
|
|
173
116
|
loggedInAt: '2026-04-13T00:00:00.000Z',
|
|
174
|
-
hint: 'Could not verify session. Try again or run: opensoma auth login
|
|
117
|
+
hint: 'Could not verify session. Try again or run: opensoma auth login',
|
|
175
118
|
})
|
|
176
119
|
expect(cleared).toBe(false)
|
|
177
120
|
})
|
|
178
121
|
|
|
179
|
-
it('recovers via browser extraction when no stored password is available', async () => {
|
|
180
|
-
let savedCredentials: Record<string, unknown> | null = null
|
|
181
|
-
|
|
182
|
-
const status = await inspectStoredAuthStatus(
|
|
183
|
-
{
|
|
184
|
-
getCredentials: async () => ({
|
|
185
|
-
sessionCookie: 'stale-session',
|
|
186
|
-
csrfToken: 'stale-csrf',
|
|
187
|
-
}),
|
|
188
|
-
setCredentials: async (credentials: Record<string, unknown>) => {
|
|
189
|
-
savedCredentials = credentials
|
|
190
|
-
},
|
|
191
|
-
clearSessionState: async () => {
|
|
192
|
-
throw new Error('should not clear session state when browser extraction succeeds')
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
() => ({
|
|
196
|
-
checkLogin: async () => null,
|
|
197
|
-
}),
|
|
198
|
-
undefined,
|
|
199
|
-
async () => ({ sessionCookie: 'browser-session', csrfToken: 'browser-csrf' }),
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
expect(status).toMatchObject({
|
|
203
|
-
authenticated: true,
|
|
204
|
-
valid: true,
|
|
205
|
-
username: null,
|
|
206
|
-
})
|
|
207
|
-
expect(savedCredentials).toMatchObject({
|
|
208
|
-
sessionCookie: 'browser-session',
|
|
209
|
-
csrfToken: 'browser-csrf',
|
|
210
|
-
})
|
|
211
|
-
expect(savedCredentials).toHaveProperty('loggedInAt')
|
|
212
|
-
})
|
|
213
|
-
|
|
214
122
|
it('refreshes the session automatically when stored username and password are available', async () => {
|
|
215
123
|
let savedCredentials: Record<string, string> | null = null
|
|
216
124
|
|
package/src/commands/auth.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { SomaHttp } from '../http'
|
|
|
7
7
|
import { recoverSession } from '../session-recovery'
|
|
8
8
|
import { handleError } from '../shared/utils/error-handler'
|
|
9
9
|
import { formatOutput } from '../shared/utils/output'
|
|
10
|
-
import type { ExtractedSessionCandidate } from '../token-extractor'
|
|
11
10
|
|
|
12
11
|
function ask(rl: ReadlineInterface, message: string): Promise<string> {
|
|
13
12
|
return new Promise((resolve) => {
|
|
@@ -101,62 +100,12 @@ async function promptCredentials(
|
|
|
101
100
|
|
|
102
101
|
type LoginOptions = { username?: string; password?: string; pretty?: boolean }
|
|
103
102
|
type StatusOptions = { pretty?: boolean }
|
|
104
|
-
type ExtractOptions = { debug?: boolean; pretty?: boolean }
|
|
105
|
-
type ExtractedSessionValidator = Pick<SomaHttp, 'checkLogin' | 'extractCsrfToken'>
|
|
106
103
|
type CredentialStore = Pick<CredentialManager, 'clearSessionState' | 'getCredentials' | 'setCredentials'>
|
|
107
104
|
type StatusValidator = Pick<SomaHttp, 'checkLogin'>
|
|
108
105
|
type ReloginHttp = Pick<SomaHttp, 'checkLogin' | 'getCsrfToken' | 'getSessionCookie' | 'login'>
|
|
109
|
-
type BrowserExtractor = () => Promise<{ csrfToken: string; sessionCookie: string } | null>
|
|
110
106
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
TokenExtractor: new () => { extractCandidates: () => Promise<ExtractedSessionCandidate[]> }
|
|
114
|
-
}
|
|
115
|
-
const candidates = await new TokenExtractor().extractCandidates()
|
|
116
|
-
if (candidates.length === 0) return null
|
|
117
|
-
return resolveExtractedCredentials(candidates)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const EXPIRED_SESSION_HINT = 'Session expired. Run: opensoma auth login or opensoma auth extract'
|
|
121
|
-
const UNVERIFIED_SESSION_HINT =
|
|
122
|
-
'Could not verify session. Try again or run: opensoma auth login or opensoma auth extract'
|
|
123
|
-
|
|
124
|
-
export async function resolveExtractedCredentials(
|
|
125
|
-
candidates: ExtractedSessionCandidate[],
|
|
126
|
-
createValidator: (sessionCookie: string) => ExtractedSessionValidator = (sessionCookie) =>
|
|
127
|
-
new SomaHttp({ sessionCookie }),
|
|
128
|
-
debug?: (message: string) => void,
|
|
129
|
-
): Promise<{ csrfToken: string; sessionCookie: string } | null> {
|
|
130
|
-
debug?.(`Validating ${candidates.length} candidate(s) against server...`)
|
|
131
|
-
|
|
132
|
-
for (const candidate of candidates) {
|
|
133
|
-
const http = createValidator(candidate.sessionCookie)
|
|
134
|
-
debug?.(` ${candidate.browser} / ${candidate.profile}: checkLogin...`)
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const valid = Boolean(await http.checkLogin())
|
|
138
|
-
if (!valid) {
|
|
139
|
-
debug?.(` ${candidate.browser} / ${candidate.profile}: session invalid`)
|
|
140
|
-
continue
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
debug?.(` ${candidate.browser} / ${candidate.profile}: valid! Extracting CSRF token...`)
|
|
144
|
-
const csrfToken = await http.extractCsrfToken()
|
|
145
|
-
debug?.(` CSRF token obtained (${csrfToken.length} chars)`)
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
sessionCookie: candidate.sessionCookie,
|
|
149
|
-
csrfToken,
|
|
150
|
-
}
|
|
151
|
-
} catch (error) {
|
|
152
|
-
debug?.(
|
|
153
|
-
` ${candidate.browser} / ${candidate.profile}: error: ${error instanceof Error ? error.message : String(error)}`,
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return null
|
|
159
|
-
}
|
|
107
|
+
const EXPIRED_SESSION_HINT = 'Session expired. Run: opensoma auth login'
|
|
108
|
+
const UNVERIFIED_SESSION_HINT = 'Could not verify session. Try again or run: opensoma auth login'
|
|
160
109
|
|
|
161
110
|
async function loginAction(options: LoginOptions): Promise<void> {
|
|
162
111
|
try {
|
|
@@ -230,7 +179,6 @@ export async function inspectStoredAuthStatus(
|
|
|
230
179
|
createValidator: (credentials: { sessionCookie: string; csrfToken: string }) => StatusValidator = (credentials) =>
|
|
231
180
|
new SomaHttp({ sessionCookie: credentials.sessionCookie, csrfToken: credentials.csrfToken }),
|
|
232
181
|
createReloginHttp: () => ReloginHttp = () => new SomaHttp(),
|
|
233
|
-
recoverViaBrowser: BrowserExtractor = defaultExtractBrowserCredentials,
|
|
234
182
|
): Promise<Record<string, boolean | null | string>> {
|
|
235
183
|
const creds = await manager.getCredentials()
|
|
236
184
|
if (!creds) {
|
|
@@ -271,21 +219,6 @@ export async function inspectStoredAuthStatus(
|
|
|
271
219
|
}
|
|
272
220
|
}
|
|
273
221
|
|
|
274
|
-
try {
|
|
275
|
-
const extracted = await recoverViaBrowser()
|
|
276
|
-
if (extracted) {
|
|
277
|
-
const loggedInAt = new Date().toISOString()
|
|
278
|
-
await manager.setCredentials({
|
|
279
|
-
sessionCookie: extracted.sessionCookie,
|
|
280
|
-
csrfToken: extracted.csrfToken,
|
|
281
|
-
loggedInAt,
|
|
282
|
-
})
|
|
283
|
-
return { authenticated: true, valid: true, username: null, loggedInAt }
|
|
284
|
-
}
|
|
285
|
-
} catch {
|
|
286
|
-
// Browser extraction failed — fall through to credential removal
|
|
287
|
-
}
|
|
288
|
-
|
|
289
222
|
await manager.clearSessionState()
|
|
290
223
|
const post = await manager.getCredentials()
|
|
291
224
|
return {
|
|
@@ -313,45 +246,6 @@ async function statusAction(options: StatusOptions): Promise<void> {
|
|
|
313
246
|
}
|
|
314
247
|
}
|
|
315
248
|
|
|
316
|
-
async function extractAction(options: ExtractOptions): Promise<void> {
|
|
317
|
-
const log = options.debug ? (message: string) => process.stderr.write(`[extract] ${message}\n`) : undefined
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
const { TokenExtractor } = (await import('../token-extractor')) as {
|
|
321
|
-
TokenExtractor: new (options?: { debug?: boolean }) => {
|
|
322
|
-
extractCandidates: () => Promise<ExtractedSessionCandidate[]>
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
const extractor = new TokenExtractor({ debug: options.debug })
|
|
326
|
-
const candidates = await extractor.extractCandidates()
|
|
327
|
-
if (candidates.length === 0) {
|
|
328
|
-
throw new Error(
|
|
329
|
-
'No SWMaestro session found in any browser. Login to swmaestro.ai or opensoma.dev in a supported Chromium browser (Chrome, Edge, Brave, Arc, Vivaldi) first.',
|
|
330
|
-
)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
log?.(
|
|
334
|
-
`Extracted ${candidates.length} candidate(s): ${candidates.map((c) => `${c.browser}/${c.profile}`).join(', ')}`,
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
const credentials = await resolveExtractedCredentials(candidates, undefined, log)
|
|
338
|
-
if (!credentials) {
|
|
339
|
-
throw new Error(
|
|
340
|
-
'Found SWMaestro session cookies in supported browsers, but none are valid. Refresh your swmaestro.ai or opensoma.dev login in a supported Chromium browser and try again.',
|
|
341
|
-
)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
await new CredentialManager().setCredentials({
|
|
345
|
-
sessionCookie: credentials.sessionCookie,
|
|
346
|
-
csrfToken: credentials.csrfToken,
|
|
347
|
-
loggedInAt: new Date().toISOString(),
|
|
348
|
-
})
|
|
349
|
-
console.log(formatOutput({ ok: true, extracted: true, valid: true }, options.pretty))
|
|
350
|
-
} catch (error) {
|
|
351
|
-
handleError(error)
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
249
|
export const authCommand = new Command('auth')
|
|
356
250
|
.description('Manage authentication')
|
|
357
251
|
.addCommand(
|
|
@@ -374,10 +268,3 @@ export const authCommand = new Command('auth')
|
|
|
374
268
|
.option('--pretty', 'Pretty print JSON output')
|
|
375
269
|
.action(statusAction),
|
|
376
270
|
)
|
|
377
|
-
.addCommand(
|
|
378
|
-
new Command('extract')
|
|
379
|
-
.description('Extract browser credentials')
|
|
380
|
-
.option('--debug', 'Show debug output')
|
|
381
|
-
.option('--pretty', 'Pretty print JSON output')
|
|
382
|
-
.action(extractAction),
|
|
383
|
-
)
|