opensoma 0.5.1 → 0.6.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 +5 -1
- package/dist/src/agent-browser-launcher.d.ts +43 -0
- package/dist/src/agent-browser-launcher.d.ts.map +1 -0
- package/dist/src/agent-browser-launcher.js +97 -0
- package/dist/src/agent-browser-launcher.js.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +30 -7
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +218 -52
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/agent-browser.d.ts +3 -0
- package/dist/src/commands/agent-browser.d.ts.map +1 -0
- package/dist/src/commands/agent-browser.js +27 -0
- package/dist/src/commands/agent-browser.js.map +1 -0
- package/dist/src/commands/auth.d.ts +1 -1
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +4 -2
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/dashboard.d.ts +13 -0
- package/dist/src/commands/dashboard.d.ts.map +1 -1
- package/dist/src/commands/dashboard.js +10 -18
- package/dist/src/commands/dashboard.js.map +1 -1
- package/dist/src/commands/helpers.d.ts +1 -1
- package/dist/src/commands/helpers.d.ts.map +1 -1
- package/dist/src/commands/helpers.js +2 -2
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/index.d.ts +2 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/mentoring.d.ts.map +1 -1
- package/dist/src/commands/mentoring.js +54 -29
- package/dist/src/commands/mentoring.js.map +1 -1
- package/dist/src/commands/notice.d.ts.map +1 -1
- package/dist/src/commands/notice.js +2 -1
- package/dist/src/commands/notice.js.map +1 -1
- package/dist/src/commands/report.d.ts.map +1 -1
- package/dist/src/commands/report.js +4 -2
- package/dist/src/commands/report.js.map +1 -1
- package/dist/src/commands/room.d.ts.map +1 -1
- package/dist/src/commands/room.js +125 -2
- package/dist/src/commands/room.js.map +1 -1
- package/dist/src/commands/schedule.d.ts +3 -0
- package/dist/src/commands/schedule.d.ts.map +1 -0
- package/dist/src/commands/schedule.js +27 -0
- package/dist/src/commands/schedule.js.map +1 -0
- package/dist/src/commands/team.d.ts.map +1 -1
- package/dist/src/commands/team.js +55 -4
- package/dist/src/commands/team.js.map +1 -1
- package/dist/src/constants.d.ts +5 -5
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +20 -8
- package/dist/src/constants.js.map +1 -1
- package/dist/src/credential-manager.d.ts +9 -0
- package/dist/src/credential-manager.d.ts.map +1 -1
- package/dist/src/credential-manager.js +24 -0
- package/dist/src/credential-manager.js.map +1 -1
- package/dist/src/formatters.d.ts +11 -3
- package/dist/src/formatters.d.ts.map +1 -1
- package/dist/src/formatters.js +281 -52
- package/dist/src/formatters.js.map +1 -1
- package/dist/src/http.d.ts +8 -0
- package/dist/src/http.d.ts.map +1 -1
- package/dist/src/http.js +29 -1
- package/dist/src/http.js.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/shared/utils/swmaestro.d.ts +34 -1
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
- package/dist/src/shared/utils/swmaestro.js +102 -39
- package/dist/src/shared/utils/swmaestro.js.map +1 -1
- package/dist/src/shared/utils/team-action-params.d.ts +3 -0
- package/dist/src/shared/utils/team-action-params.d.ts.map +1 -0
- package/dist/src/shared/utils/team-action-params.js +10 -0
- package/dist/src/shared/utils/team-action-params.js.map +1 -0
- package/dist/src/shared/utils/team-params.d.ts +12 -0
- package/dist/src/shared/utils/team-params.d.ts.map +1 -0
- package/dist/src/shared/utils/team-params.js +38 -0
- package/dist/src/shared/utils/team-params.js.map +1 -0
- package/dist/src/types.d.ts +147 -10
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +74 -6
- package/dist/src/types.js.map +1 -1
- package/package.json +5 -1
- package/src/agent-browser-launcher.test.ts +263 -0
- package/src/agent-browser-launcher.ts +159 -0
- package/src/cli.ts +4 -2
- package/src/client.test.ts +673 -30
- package/src/client.ts +277 -67
- package/src/commands/agent-browser.ts +33 -0
- package/src/commands/auth.test.ts +77 -26
- package/src/commands/auth.ts +5 -3
- package/src/commands/dashboard.test.ts +57 -0
- package/src/commands/dashboard.ts +22 -19
- package/src/commands/helpers.test.ts +72 -25
- package/src/commands/helpers.ts +3 -3
- package/src/commands/index.ts +2 -1
- package/src/commands/mentoring.ts +60 -29
- package/src/commands/notice.ts +2 -1
- package/src/commands/report.ts +4 -2
- package/src/commands/room.ts +160 -1
- package/src/commands/schedule.ts +32 -0
- package/src/commands/team.ts +73 -5
- package/src/constants.ts +20 -8
- package/src/credential-manager.test.ts +44 -0
- package/src/credential-manager.ts +27 -0
- package/src/formatters.test.ts +528 -33
- package/src/formatters.ts +309 -55
- package/src/http.test.ts +71 -2
- package/src/http.ts +41 -2
- package/src/index.ts +10 -1
- package/src/shared/utils/swmaestro.test.ts +245 -9
- package/src/shared/utils/swmaestro.ts +150 -47
- package/src/shared/utils/team-action-params.test.ts +32 -0
- package/src/shared/utils/team-action-params.ts +10 -0
- package/src/shared/utils/team-params.test.ts +141 -0
- package/src/shared/utils/team-params.ts +53 -0
- package/src/types.test.ts +26 -13
- package/src/types.ts +87 -7
- package/dist/src/commands/event.d.ts +0 -3
- package/dist/src/commands/event.d.ts.map +0 -1
- package/dist/src/commands/event.js +0 -58
- package/dist/src/commands/event.js.map +0 -1
- package/src/commands/event.ts +0 -73
|
@@ -57,21 +57,72 @@ describe('resolveExtractedCredentials', () => {
|
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
describe('inspectStoredAuthStatus', () => {
|
|
60
|
-
it('clears
|
|
61
|
-
let
|
|
60
|
+
it('clears session state but preserves saved id/password when both recovery methods fail', async () => {
|
|
61
|
+
let cleared = false
|
|
62
|
+
let postClearCredentials = {
|
|
63
|
+
sessionCookie: '',
|
|
64
|
+
csrfToken: '',
|
|
65
|
+
username: 'mentor@example.com',
|
|
66
|
+
password: 'secret-password',
|
|
67
|
+
}
|
|
62
68
|
|
|
63
69
|
const status = await inspectStoredAuthStatus(
|
|
64
70
|
{
|
|
65
|
-
getCredentials: async () =>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
getCredentials: async () => {
|
|
72
|
+
if (cleared) return postClearCredentials
|
|
73
|
+
return {
|
|
74
|
+
sessionCookie: 'stale-session',
|
|
75
|
+
csrfToken: 'csrf-token',
|
|
76
|
+
username: 'mentor@example.com',
|
|
77
|
+
password: 'secret-password',
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
setCredentials: async () => {
|
|
81
|
+
throw new Error('inspectStoredAuthStatus must not write credentials directly on the failure path')
|
|
82
|
+
},
|
|
83
|
+
clearSessionState: async () => {
|
|
84
|
+
cleared = true
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
() => ({
|
|
88
|
+
checkLogin: async () => null,
|
|
89
|
+
}),
|
|
90
|
+
() => ({
|
|
91
|
+
login: async () => {},
|
|
92
|
+
checkLogin: async () => null,
|
|
93
|
+
getSessionCookie: () => null,
|
|
94
|
+
getCsrfToken: () => null,
|
|
95
|
+
}),
|
|
96
|
+
noBrowserExtraction,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
expect(status).toEqual({
|
|
100
|
+
authenticated: false,
|
|
101
|
+
credentials: null,
|
|
102
|
+
clearedStaleSession: true,
|
|
103
|
+
preservedRecoveryCredentials: true,
|
|
104
|
+
hint: 'Session expired. Run: opensoma auth login or opensoma auth extract',
|
|
105
|
+
})
|
|
106
|
+
expect(cleared).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('reports preservedRecoveryCredentials=false when no recovery material was stored', async () => {
|
|
110
|
+
let cleared = false
|
|
111
|
+
|
|
112
|
+
const status = await inspectStoredAuthStatus(
|
|
113
|
+
{
|
|
114
|
+
getCredentials: async () => {
|
|
115
|
+
if (cleared) return null
|
|
116
|
+
return {
|
|
117
|
+
sessionCookie: 'stale-session',
|
|
118
|
+
csrfToken: 'csrf-token',
|
|
119
|
+
}
|
|
120
|
+
},
|
|
70
121
|
setCredentials: async () => {
|
|
71
|
-
throw new Error('should not
|
|
122
|
+
throw new Error('should not write credentials')
|
|
72
123
|
},
|
|
73
|
-
|
|
74
|
-
|
|
124
|
+
clearSessionState: async () => {
|
|
125
|
+
cleared = true
|
|
75
126
|
},
|
|
76
127
|
},
|
|
77
128
|
() => ({
|
|
@@ -84,28 +135,28 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
84
135
|
expect(status).toEqual({
|
|
85
136
|
authenticated: false,
|
|
86
137
|
credentials: null,
|
|
87
|
-
|
|
138
|
+
clearedStaleSession: true,
|
|
139
|
+
preservedRecoveryCredentials: false,
|
|
88
140
|
hint: 'Session expired. Run: opensoma auth login or opensoma auth extract',
|
|
89
141
|
})
|
|
90
|
-
expect(removed).toBe(true)
|
|
91
142
|
})
|
|
92
143
|
|
|
93
144
|
it('preserves credentials when session verification fails unexpectedly', async () => {
|
|
94
|
-
let
|
|
145
|
+
let cleared = false
|
|
95
146
|
|
|
96
147
|
const status = await inspectStoredAuthStatus(
|
|
97
148
|
{
|
|
98
149
|
getCredentials: async () => ({
|
|
99
150
|
sessionCookie: 'maybe-valid-session',
|
|
100
151
|
csrfToken: 'csrf-token',
|
|
101
|
-
username: '
|
|
152
|
+
username: 'mentor@example.com',
|
|
102
153
|
loggedInAt: '2026-04-13T00:00:00.000Z',
|
|
103
154
|
}),
|
|
104
155
|
setCredentials: async () => {
|
|
105
156
|
throw new Error('should not rewrite credentials when verification fails')
|
|
106
157
|
},
|
|
107
|
-
|
|
108
|
-
|
|
158
|
+
clearSessionState: async () => {
|
|
159
|
+
cleared = true
|
|
109
160
|
},
|
|
110
161
|
},
|
|
111
162
|
() => ({
|
|
@@ -118,11 +169,11 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
118
169
|
expect(status).toEqual({
|
|
119
170
|
authenticated: true,
|
|
120
171
|
valid: false,
|
|
121
|
-
username: '
|
|
172
|
+
username: 'mentor@example.com',
|
|
122
173
|
loggedInAt: '2026-04-13T00:00:00.000Z',
|
|
123
174
|
hint: 'Could not verify session. Try again or run: opensoma auth login or opensoma auth extract',
|
|
124
175
|
})
|
|
125
|
-
expect(
|
|
176
|
+
expect(cleared).toBe(false)
|
|
126
177
|
})
|
|
127
178
|
|
|
128
179
|
it('recovers via browser extraction when no stored password is available', async () => {
|
|
@@ -137,8 +188,8 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
137
188
|
setCredentials: async (credentials: Record<string, unknown>) => {
|
|
138
189
|
savedCredentials = credentials
|
|
139
190
|
},
|
|
140
|
-
|
|
141
|
-
throw new Error('should not
|
|
191
|
+
clearSessionState: async () => {
|
|
192
|
+
throw new Error('should not clear session state when browser extraction succeeds')
|
|
142
193
|
},
|
|
143
194
|
},
|
|
144
195
|
() => ({
|
|
@@ -168,15 +219,15 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
168
219
|
getCredentials: async () => ({
|
|
169
220
|
sessionCookie: 'stale-session',
|
|
170
221
|
csrfToken: 'stale-csrf',
|
|
171
|
-
username: '
|
|
222
|
+
username: 'mentor@example.com',
|
|
172
223
|
password: 'secret',
|
|
173
224
|
loggedInAt: '2026-04-13T00:00:00.000Z',
|
|
174
225
|
}),
|
|
175
226
|
setCredentials: async (credentials: Record<string, string>) => {
|
|
176
227
|
savedCredentials = credentials
|
|
177
228
|
},
|
|
178
|
-
|
|
179
|
-
throw new Error('should not
|
|
229
|
+
clearSessionState: async () => {
|
|
230
|
+
throw new Error('should not clear session state when re-login succeeds')
|
|
180
231
|
},
|
|
181
232
|
},
|
|
182
233
|
() => ({
|
|
@@ -184,7 +235,7 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
184
235
|
}),
|
|
185
236
|
() => ({
|
|
186
237
|
login: async () => {},
|
|
187
|
-
checkLogin: async () => ({ userId: '
|
|
238
|
+
checkLogin: async () => ({ userId: 'mentor@example.com', userNm: 'Mentor One' }),
|
|
188
239
|
getSessionCookie: () => 'fresh-session',
|
|
189
240
|
getCsrfToken: () => 'fresh-csrf',
|
|
190
241
|
}),
|
|
@@ -193,13 +244,13 @@ describe('inspectStoredAuthStatus', () => {
|
|
|
193
244
|
expect(status).toEqual({
|
|
194
245
|
authenticated: true,
|
|
195
246
|
valid: true,
|
|
196
|
-
username: '
|
|
247
|
+
username: 'mentor@example.com',
|
|
197
248
|
loggedInAt: expect.any(String),
|
|
198
249
|
})
|
|
199
250
|
expect(savedCredentials).toMatchObject({
|
|
200
251
|
sessionCookie: 'fresh-session',
|
|
201
252
|
csrfToken: 'fresh-csrf',
|
|
202
|
-
username: '
|
|
253
|
+
username: 'mentor@example.com',
|
|
203
254
|
password: 'secret',
|
|
204
255
|
})
|
|
205
256
|
})
|
package/src/commands/auth.ts
CHANGED
|
@@ -103,7 +103,7 @@ type LoginOptions = { username?: string; password?: string; pretty?: boolean }
|
|
|
103
103
|
type StatusOptions = { pretty?: boolean }
|
|
104
104
|
type ExtractOptions = { debug?: boolean; pretty?: boolean }
|
|
105
105
|
type ExtractedSessionValidator = Pick<SomaHttp, 'checkLogin' | 'extractCsrfToken'>
|
|
106
|
-
type CredentialStore = Pick<CredentialManager, '
|
|
106
|
+
type CredentialStore = Pick<CredentialManager, 'clearSessionState' | 'getCredentials' | 'setCredentials'>
|
|
107
107
|
type StatusValidator = Pick<SomaHttp, 'checkLogin'>
|
|
108
108
|
type ReloginHttp = Pick<SomaHttp, 'checkLogin' | 'getCsrfToken' | 'getSessionCookie' | 'login'>
|
|
109
109
|
type BrowserExtractor = () => Promise<{ csrfToken: string; sessionCookie: string } | null>
|
|
@@ -286,11 +286,13 @@ export async function inspectStoredAuthStatus(
|
|
|
286
286
|
// Browser extraction failed — fall through to credential removal
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
await manager.
|
|
289
|
+
await manager.clearSessionState()
|
|
290
|
+
const post = await manager.getCredentials()
|
|
290
291
|
return {
|
|
291
292
|
authenticated: false,
|
|
292
293
|
credentials: null,
|
|
293
|
-
|
|
294
|
+
clearedStaleSession: true,
|
|
295
|
+
preservedRecoveryCredentials: Boolean(post?.username || post?.password),
|
|
294
296
|
hint: EXPIRED_SESSION_HINT,
|
|
295
297
|
}
|
|
296
298
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import type { SomaHttp } from '../http'
|
|
4
|
+
import type { Dashboard } from '../types'
|
|
5
|
+
import { showDashboard } from './dashboard'
|
|
6
|
+
|
|
7
|
+
describe('showDashboard', () => {
|
|
8
|
+
it('prints the SDK dashboard response', async () => {
|
|
9
|
+
const dashboard: Dashboard = {
|
|
10
|
+
name: '김연수',
|
|
11
|
+
role: '17기 연수생',
|
|
12
|
+
organization: 'OpenSoma',
|
|
13
|
+
position: '',
|
|
14
|
+
mentoringSessions: [
|
|
15
|
+
{
|
|
16
|
+
title: '팀 프로젝트 방향성 피드백',
|
|
17
|
+
url: '/sw/mypage/mentoLec/view.do?qustnrSn=44',
|
|
18
|
+
status: '접수완료',
|
|
19
|
+
date: '2026-04-28',
|
|
20
|
+
time: '22:00',
|
|
21
|
+
timeEnd: '24:00',
|
|
22
|
+
type: '자유 멘토링',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
roomReservations: [],
|
|
26
|
+
}
|
|
27
|
+
const fakeHttp = {} as SomaHttp
|
|
28
|
+
const calls: string[] = []
|
|
29
|
+
const outputs: string[] = []
|
|
30
|
+
|
|
31
|
+
await showDashboard(
|
|
32
|
+
{ pretty: true },
|
|
33
|
+
{
|
|
34
|
+
getHttp: async () => {
|
|
35
|
+
calls.push('getHttp')
|
|
36
|
+
return fakeHttp
|
|
37
|
+
},
|
|
38
|
+
createClient: (http) => {
|
|
39
|
+
expect(http).toBe(fakeHttp)
|
|
40
|
+
calls.push('createClient')
|
|
41
|
+
return {
|
|
42
|
+
dashboard: {
|
|
43
|
+
get: async () => {
|
|
44
|
+
calls.push('dashboard.get')
|
|
45
|
+
return dashboard
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
write: (output) => outputs.push(output),
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
expect(calls).toEqual(['getHttp', 'createClient', 'dashboard.get'])
|
|
55
|
+
expect(outputs).toEqual([JSON.stringify(dashboard, null, 2)])
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { SomaClient } from '../client'
|
|
4
|
+
import type { SomaHttp } from '../http'
|
|
5
5
|
import { handleError } from '../shared/utils/error-handler'
|
|
6
|
-
import { buildMentoringListParams } from '../shared/utils/mentoring-params'
|
|
7
6
|
import { formatOutput } from '../shared/utils/output'
|
|
8
7
|
import { getHttpOrExit } from './helpers'
|
|
9
8
|
|
|
10
|
-
type ShowOptions = { pretty?: boolean }
|
|
9
|
+
export type ShowOptions = { pretty?: boolean }
|
|
10
|
+
|
|
11
|
+
type DashboardClient = Pick<SomaClient, 'dashboard'>
|
|
12
|
+
|
|
13
|
+
interface ShowDashboardDeps {
|
|
14
|
+
getHttp?: () => Promise<SomaHttp>
|
|
15
|
+
createClient?: (http: SomaHttp) => DashboardClient
|
|
16
|
+
write?: (output: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function showDashboard(options: ShowOptions, deps: ShowDashboardDeps = {}): Promise<void> {
|
|
20
|
+
const getHttp = deps.getHttp ?? getHttpOrExit
|
|
21
|
+
const createClient = deps.createClient ?? ((http) => new SomaClient({ http }))
|
|
22
|
+
const write = deps.write ?? ((output) => console.log(output))
|
|
23
|
+
|
|
24
|
+
const http = await getHttp()
|
|
25
|
+
const dashboard = await createClient(http).dashboard.get()
|
|
26
|
+
write(formatOutput(dashboard, options.pretty))
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
async function showAction(options: ShowOptions): Promise<void> {
|
|
13
30
|
try {
|
|
14
|
-
|
|
15
|
-
const user = (await http.checkLogin()) ?? undefined
|
|
16
|
-
const search = { field: 'author' as const, value: '@me', me: true }
|
|
17
|
-
const [dashboardHtml, mentoringHtml] = await Promise.all([
|
|
18
|
-
http.get('/mypage/myMain/dashboard.do', { menuNo: MENU_NO.DASHBOARD }),
|
|
19
|
-
http.get('/mypage/mentoLec/list.do', buildMentoringListParams({ search, user })),
|
|
20
|
-
])
|
|
21
|
-
const dashboard = formatters.parseDashboard(dashboardHtml)
|
|
22
|
-
const myMentoring = formatters.parseMentoringList(mentoringHtml)
|
|
23
|
-
dashboard.mentoringSessions = myMentoring.map((item) => ({
|
|
24
|
-
title: item.title,
|
|
25
|
-
url: `/mypage/mentoLec/view.do?qustnrSn=${item.id}`,
|
|
26
|
-
status: item.status,
|
|
27
|
-
}))
|
|
28
|
-
console.log(formatOutput(dashboard, options.pretty))
|
|
31
|
+
await showDashboard(options)
|
|
29
32
|
} catch (error) {
|
|
30
33
|
handleError(error)
|
|
31
34
|
}
|
|
@@ -8,7 +8,7 @@ describe('createAuthenticatedHttp', () => {
|
|
|
8
8
|
it('throws a login hint when no credentials are stored', async () => {
|
|
9
9
|
const manager = {
|
|
10
10
|
getCredentials: async () => null,
|
|
11
|
-
|
|
11
|
+
clearSessionState: async () => {},
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
await expect(createAuthenticatedHttp(manager)).rejects.toThrow(
|
|
@@ -16,32 +16,79 @@ describe('createAuthenticatedHttp', () => {
|
|
|
16
16
|
)
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
it('clears
|
|
20
|
-
let
|
|
19
|
+
it('clears only session state (not username/password) when both recovery methods fail', async () => {
|
|
20
|
+
let cleared = false
|
|
21
21
|
const manager = {
|
|
22
22
|
getCredentials: async () => ({
|
|
23
23
|
sessionCookie: 'stale-session',
|
|
24
24
|
csrfToken: 'csrf-token',
|
|
25
|
+
username: 'mentor@example.com',
|
|
26
|
+
password: 'secret-password',
|
|
25
27
|
}),
|
|
26
28
|
setCredentials: async () => {
|
|
27
|
-
throw new Error('
|
|
29
|
+
throw new Error('createAuthenticatedHttp must not write credentials directly')
|
|
28
30
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
clearSessionState: async () => {
|
|
32
|
+
cleared = true
|
|
31
33
|
},
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
await expect(
|
|
35
37
|
createAuthenticatedHttp(manager, () => ({ checkLogin: async () => null }), undefined, noBrowserExtraction),
|
|
36
38
|
).rejects.toThrow(
|
|
37
|
-
'Session expired.
|
|
39
|
+
'Session expired. Run: opensoma auth login or opensoma auth extract (saved id/password were preserved)',
|
|
38
40
|
)
|
|
39
|
-
expect(
|
|
41
|
+
expect(cleared).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('preserves stored id/password on disk when session expires and recovery fails', async () => {
|
|
45
|
+
const { CredentialManager } = await import('../credential-manager')
|
|
46
|
+
const { mkdtemp, rm } = await import('node:fs/promises')
|
|
47
|
+
const { tmpdir } = await import('node:os')
|
|
48
|
+
const { join } = await import('node:path')
|
|
49
|
+
|
|
50
|
+
const dir = await mkdtemp(join(tmpdir(), 'opensoma-helpers-'))
|
|
51
|
+
try {
|
|
52
|
+
const manager = new CredentialManager(dir)
|
|
53
|
+
await manager.setCredentials({
|
|
54
|
+
sessionCookie: 'stale-session',
|
|
55
|
+
csrfToken: 'stale-csrf',
|
|
56
|
+
username: 'mentor@example.com',
|
|
57
|
+
password: 'secret-password',
|
|
58
|
+
loggedInAt: '2026-04-09T00:00:00.000Z',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
await expect(
|
|
62
|
+
createAuthenticatedHttp(
|
|
63
|
+
manager,
|
|
64
|
+
() => ({ checkLogin: async () => null }),
|
|
65
|
+
() => ({
|
|
66
|
+
login: async () => {
|
|
67
|
+
throw new Error('upstream rejected re-login')
|
|
68
|
+
},
|
|
69
|
+
checkLogin: async () => null,
|
|
70
|
+
getSessionCookie: () => null,
|
|
71
|
+
getCsrfToken: () => null,
|
|
72
|
+
}),
|
|
73
|
+
noBrowserExtraction,
|
|
74
|
+
),
|
|
75
|
+
).rejects.toThrow('Session expired')
|
|
76
|
+
|
|
77
|
+
const after = await manager.getCredentials()
|
|
78
|
+
expect(after).toEqual({
|
|
79
|
+
sessionCookie: '',
|
|
80
|
+
csrfToken: '',
|
|
81
|
+
username: 'mentor@example.com',
|
|
82
|
+
password: 'secret-password',
|
|
83
|
+
})
|
|
84
|
+
} finally {
|
|
85
|
+
await rm(dir, { force: true, recursive: true })
|
|
86
|
+
}
|
|
40
87
|
})
|
|
41
88
|
|
|
42
89
|
it('returns the authenticated http client when the session is valid', async () => {
|
|
43
90
|
const http = {
|
|
44
|
-
checkLogin: async () => ({ userId: '
|
|
91
|
+
checkLogin: async () => ({ userId: 'mentor@example.com', userNm: 'Mentor One' }),
|
|
45
92
|
get: async () => '',
|
|
46
93
|
}
|
|
47
94
|
const manager = {
|
|
@@ -52,8 +99,8 @@ describe('createAuthenticatedHttp', () => {
|
|
|
52
99
|
setCredentials: async () => {
|
|
53
100
|
throw new Error('should not rewrite valid credentials')
|
|
54
101
|
},
|
|
55
|
-
|
|
56
|
-
throw new Error('should not
|
|
102
|
+
clearSessionState: async () => {
|
|
103
|
+
throw new Error('should not clear session state for valid credentials')
|
|
57
104
|
},
|
|
58
105
|
}
|
|
59
106
|
|
|
@@ -66,18 +113,18 @@ describe('createAuthenticatedHttp', () => {
|
|
|
66
113
|
getCredentials: async () => ({
|
|
67
114
|
sessionCookie: 'stale-session',
|
|
68
115
|
csrfToken: 'stale-csrf',
|
|
69
|
-
username: '
|
|
116
|
+
username: 'mentor@example.com',
|
|
70
117
|
password: 'secret',
|
|
71
118
|
}),
|
|
72
119
|
setCredentials: async (credentials: Record<string, string>) => {
|
|
73
120
|
savedCredentials = credentials
|
|
74
121
|
},
|
|
75
|
-
|
|
76
|
-
throw new Error('should not
|
|
122
|
+
clearSessionState: async () => {
|
|
123
|
+
throw new Error('should not clear session state when re-login succeeds')
|
|
77
124
|
},
|
|
78
125
|
}
|
|
79
126
|
const recoveredHttp = {
|
|
80
|
-
checkLogin: async () => ({ userId: '
|
|
127
|
+
checkLogin: async () => ({ userId: 'mentor@example.com', userNm: 'Mentor One' }),
|
|
81
128
|
get: async () => '',
|
|
82
129
|
}
|
|
83
130
|
|
|
@@ -95,7 +142,7 @@ describe('createAuthenticatedHttp', () => {
|
|
|
95
142
|
},
|
|
96
143
|
() => ({
|
|
97
144
|
login: async () => {},
|
|
98
|
-
checkLogin: async () => ({ userId: '
|
|
145
|
+
checkLogin: async () => ({ userId: 'mentor@example.com', userNm: 'Mentor One' }),
|
|
99
146
|
getSessionCookie: () => 'fresh-session',
|
|
100
147
|
getCsrfToken: () => 'fresh-csrf',
|
|
101
148
|
}),
|
|
@@ -105,7 +152,7 @@ describe('createAuthenticatedHttp', () => {
|
|
|
105
152
|
expect(savedCredentials).toMatchObject({
|
|
106
153
|
sessionCookie: 'fresh-session',
|
|
107
154
|
csrfToken: 'fresh-csrf',
|
|
108
|
-
username: '
|
|
155
|
+
username: 'mentor@example.com',
|
|
109
156
|
password: 'secret',
|
|
110
157
|
})
|
|
111
158
|
})
|
|
@@ -120,12 +167,12 @@ describe('createAuthenticatedHttp', () => {
|
|
|
120
167
|
setCredentials: async (credentials: Record<string, unknown>) => {
|
|
121
168
|
savedCredentials = credentials
|
|
122
169
|
},
|
|
123
|
-
|
|
124
|
-
throw new Error('should not
|
|
170
|
+
clearSessionState: async () => {
|
|
171
|
+
throw new Error('should not clear session state when browser extraction succeeds')
|
|
125
172
|
},
|
|
126
173
|
}
|
|
127
174
|
const recoveredHttp = {
|
|
128
|
-
checkLogin: async () => ({ userId: '
|
|
175
|
+
checkLogin: async () => ({ userId: 'mentor@example.com', userNm: 'Mentor One' }),
|
|
129
176
|
}
|
|
130
177
|
|
|
131
178
|
const result = await createAuthenticatedHttp(
|
|
@@ -152,18 +199,18 @@ describe('createAuthenticatedHttp', () => {
|
|
|
152
199
|
getCredentials: async () => ({
|
|
153
200
|
sessionCookie: 'stale-session',
|
|
154
201
|
csrfToken: 'stale-csrf',
|
|
155
|
-
username: '
|
|
202
|
+
username: 'mentor@example.com',
|
|
156
203
|
password: 'wrong-password',
|
|
157
204
|
}),
|
|
158
205
|
setCredentials: async (credentials: Record<string, unknown>) => {
|
|
159
206
|
savedCredentials = credentials
|
|
160
207
|
},
|
|
161
|
-
|
|
162
|
-
throw new Error('should not
|
|
208
|
+
clearSessionState: async () => {
|
|
209
|
+
throw new Error('should not clear session state when browser extraction succeeds')
|
|
163
210
|
},
|
|
164
211
|
}
|
|
165
212
|
const recoveredHttp = {
|
|
166
|
-
checkLogin: async () => ({ userId: '
|
|
213
|
+
checkLogin: async () => ({ userId: 'mentor@example.com', userNm: 'Mentor One' }),
|
|
167
214
|
}
|
|
168
215
|
|
|
169
216
|
const result = await createAuthenticatedHttp(
|
|
@@ -197,7 +244,7 @@ describe('createAuthenticatedHttp', () => {
|
|
|
197
244
|
setCredentials: async () => {
|
|
198
245
|
throw new Error('should not save')
|
|
199
246
|
},
|
|
200
|
-
|
|
247
|
+
clearSessionState: async () => {},
|
|
201
248
|
}
|
|
202
249
|
|
|
203
250
|
await expect(
|
package/src/commands/helpers.ts
CHANGED
|
@@ -4,14 +4,14 @@ import { recoverSession } from '../session-recovery'
|
|
|
4
4
|
import * as stderr from '../shared/utils/stderr'
|
|
5
5
|
import type { Credentials } from '../types'
|
|
6
6
|
|
|
7
|
-
type CredentialStore = Pick<CredentialManager, '
|
|
7
|
+
type CredentialStore = Pick<CredentialManager, 'clearSessionState' | 'getCredentials' | 'setCredentials'>
|
|
8
8
|
type AuthenticatedHttp = Pick<SomaHttp, 'checkLogin'>
|
|
9
9
|
type ReloginHttp = Pick<SomaHttp, 'checkLogin' | 'getCsrfToken' | 'getSessionCookie' | 'login'>
|
|
10
10
|
type BrowserExtractor = () => Promise<{ csrfToken: string; sessionCookie: string } | null>
|
|
11
11
|
|
|
12
12
|
const NOT_LOGGED_IN_MESSAGE = 'Not logged in. Run: opensoma auth login or opensoma auth extract'
|
|
13
13
|
const STALE_SESSION_MESSAGE =
|
|
14
|
-
'Session expired.
|
|
14
|
+
'Session expired. Run: opensoma auth login or opensoma auth extract (saved id/password were preserved)'
|
|
15
15
|
|
|
16
16
|
function defaultCreateHttp(credentials: Credentials): SomaHttp {
|
|
17
17
|
return new SomaHttp({ sessionCookie: credentials.sessionCookie, csrfToken: credentials.csrfToken })
|
|
@@ -73,7 +73,7 @@ export async function createAuthenticatedHttp<T extends AuthenticatedHttp>(
|
|
|
73
73
|
// Browser extraction also failed
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
await manager.
|
|
76
|
+
await manager.clearSessionState()
|
|
77
77
|
throw new Error(STALE_SESSION_MESSAGE)
|
|
78
78
|
}
|
|
79
79
|
|
package/src/commands/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { agentBrowserCommand } from './agent-browser'
|
|
1
2
|
export { authCommand } from './auth'
|
|
2
3
|
export { mentoringCommand } from './mentoring'
|
|
3
4
|
export { roomCommand } from './room'
|
|
@@ -5,5 +6,5 @@ export { dashboardCommand } from './dashboard'
|
|
|
5
6
|
export { noticeCommand } from './notice'
|
|
6
7
|
export { teamCommand } from './team'
|
|
7
8
|
export { memberCommand } from './member'
|
|
8
|
-
export {
|
|
9
|
+
export { scheduleCommand } from './schedule'
|
|
9
10
|
export { reportCommand } from './report'
|