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.
Files changed (46) hide show
  1. package/dist/package.json +1 -3
  2. package/dist/src/client.d.ts.map +1 -1
  3. package/dist/src/client.js +6 -6
  4. package/dist/src/client.js.map +1 -1
  5. package/dist/src/commands/auth.d.ts +1 -11
  6. package/dist/src/commands/auth.d.ts.map +1 -1
  7. package/dist/src/commands/auth.js +4 -81
  8. package/dist/src/commands/auth.js.map +1 -1
  9. package/dist/src/commands/helpers.d.ts +1 -5
  10. package/dist/src/commands/helpers.d.ts.map +1 -1
  11. package/dist/src/commands/helpers.js +4 -32
  12. package/dist/src/commands/helpers.js.map +1 -1
  13. package/dist/src/commands/team.js +1 -1
  14. package/dist/src/commands/team.js.map +1 -1
  15. package/dist/src/credential-manager.js +2 -2
  16. package/dist/src/credential-manager.js.map +1 -1
  17. package/dist/src/errors.d.ts +0 -4
  18. package/dist/src/errors.d.ts.map +1 -1
  19. package/dist/src/errors.js +1 -5
  20. package/dist/src/errors.js.map +1 -1
  21. package/dist/src/shared/utils/config-dir.d.ts +11 -0
  22. package/dist/src/shared/utils/config-dir.d.ts.map +1 -0
  23. package/dist/src/shared/utils/config-dir.js +19 -0
  24. package/dist/src/shared/utils/config-dir.js.map +1 -0
  25. package/dist/src/toz-pending-store.d.ts.map +1 -1
  26. package/dist/src/toz-pending-store.js +2 -2
  27. package/dist/src/toz-pending-store.js.map +1 -1
  28. package/package.json +1 -3
  29. package/src/client.test.ts +55 -4
  30. package/src/client.ts +7 -8
  31. package/src/commands/auth.test.ts +6 -98
  32. package/src/commands/auth.ts +2 -115
  33. package/src/commands/helpers.test.ts +5 -116
  34. package/src/commands/helpers.ts +3 -35
  35. package/src/commands/team.ts +1 -1
  36. package/src/credential-manager.ts +2 -2
  37. package/src/errors.ts +1 -5
  38. package/src/shared/utils/config-dir.test.ts +41 -0
  39. package/src/shared/utils/config-dir.ts +20 -0
  40. package/src/toz-pending-store.ts +3 -2
  41. package/dist/src/token-extractor.d.ts +0 -43
  42. package/dist/src/token-extractor.d.ts.map +0 -1
  43. package/dist/src/token-extractor.js +0 -302
  44. package/dist/src/token-extractor.js.map +0 -1
  45. package/src/token-extractor.test.ts +0 -220
  46. package/src/token-extractor.ts +0 -392
@@ -2,8 +2,6 @@ import { describe, expect, it } from 'bun:test'
2
2
 
3
3
  import { createAuthenticatedHttp } from './helpers'
4
4
 
5
- const noBrowserExtraction = async () => null
6
-
7
5
  describe('createAuthenticatedHttp', () => {
8
6
  it('throws a login hint when no credentials are stored', async () => {
9
7
  const manager = {
@@ -11,12 +9,10 @@ describe('createAuthenticatedHttp', () => {
11
9
  clearSessionState: async () => {},
12
10
  }
13
11
 
14
- await expect(createAuthenticatedHttp(manager)).rejects.toThrow(
15
- 'Not logged in. Run: opensoma auth login or opensoma auth extract',
16
- )
12
+ await expect(createAuthenticatedHttp(manager)).rejects.toThrow('Not logged in. Run: opensoma auth login')
17
13
  })
18
14
 
19
- it('clears only session state (not username/password) when both recovery methods fail', async () => {
15
+ it('clears only session state (not username/password) when re-login fails', async () => {
20
16
  let cleared = false
21
17
  const manager = {
22
18
  getCredentials: async () => ({
@@ -33,15 +29,13 @@ describe('createAuthenticatedHttp', () => {
33
29
  },
34
30
  }
35
31
 
36
- await expect(
37
- createAuthenticatedHttp(manager, () => ({ checkLogin: async () => null }), undefined, noBrowserExtraction),
38
- ).rejects.toThrow(
39
- 'Session expired. Run: opensoma auth login or opensoma auth extract (saved id/password were preserved)',
32
+ await expect(createAuthenticatedHttp(manager, () => ({ checkLogin: async () => null }))).rejects.toThrow(
33
+ 'Session expired. Run: opensoma auth login (saved id/password were preserved)',
40
34
  )
41
35
  expect(cleared).toBe(true)
42
36
  })
43
37
 
44
- it('preserves stored id/password on disk when session expires and recovery fails', async () => {
38
+ it('preserves stored id/password on disk when session expires and re-login fails', async () => {
45
39
  const { CredentialManager } = await import('../credential-manager')
46
40
  const { mkdtemp, rm } = await import('node:fs/promises')
47
41
  const { tmpdir } = await import('node:os')
@@ -70,7 +64,6 @@ describe('createAuthenticatedHttp', () => {
70
64
  getSessionCookie: () => null,
71
65
  getCsrfToken: () => null,
72
66
  }),
73
- noBrowserExtraction,
74
67
  ),
75
68
  ).rejects.toThrow('Session expired')
76
69
 
@@ -160,108 +153,4 @@ describe('createAuthenticatedHttp', () => {
160
153
  tozPhone: '010-1234-5678',
161
154
  })
162
155
  })
163
-
164
- it('recovers via browser extraction when no stored password is available', async () => {
165
- let savedCredentials: Record<string, unknown> | null = null
166
- const manager = {
167
- getCredentials: async () => ({
168
- sessionCookie: 'stale-session',
169
- csrfToken: 'stale-csrf',
170
- }),
171
- setCredentials: async (credentials: Record<string, unknown>) => {
172
- savedCredentials = credentials
173
- },
174
- clearSessionState: async () => {
175
- throw new Error('should not clear session state when browser extraction succeeds')
176
- },
177
- }
178
- const recoveredHttp = {
179
- checkLogin: async () => ({ userId: 'mentor@example.com', userNm: 'Mentor One' }),
180
- }
181
-
182
- const result = await createAuthenticatedHttp(
183
- manager,
184
- (credentials) => {
185
- if (credentials.sessionCookie === 'browser-session') return recoveredHttp
186
- return { checkLogin: async () => null }
187
- },
188
- undefined,
189
- async () => ({ sessionCookie: 'browser-session', csrfToken: 'browser-csrf' }),
190
- )
191
-
192
- expect(result).toBe(recoveredHttp)
193
- expect(savedCredentials).toMatchObject({
194
- sessionCookie: 'browser-session',
195
- csrfToken: 'browser-csrf',
196
- })
197
- expect(savedCredentials).toHaveProperty('loggedInAt')
198
- })
199
-
200
- it('falls back to browser extraction when password re-login fails', async () => {
201
- let savedCredentials: Record<string, unknown> | null = null
202
- const manager = {
203
- getCredentials: async () => ({
204
- sessionCookie: 'stale-session',
205
- csrfToken: 'stale-csrf',
206
- username: 'mentor@example.com',
207
- password: 'wrong-password',
208
- }),
209
- setCredentials: async (credentials: Record<string, unknown>) => {
210
- savedCredentials = credentials
211
- },
212
- clearSessionState: async () => {
213
- throw new Error('should not clear session state when browser extraction succeeds')
214
- },
215
- }
216
- const recoveredHttp = {
217
- checkLogin: async () => ({ userId: 'mentor@example.com', userNm: 'Mentor One' }),
218
- }
219
-
220
- const result = await createAuthenticatedHttp(
221
- manager,
222
- (credentials) => {
223
- if (credentials.sessionCookie === 'browser-session') return recoveredHttp
224
- return { checkLogin: async () => null }
225
- },
226
- () => ({
227
- login: async () => {
228
- throw new Error('wrong password')
229
- },
230
- checkLogin: async () => null,
231
- getSessionCookie: () => null,
232
- getCsrfToken: () => null,
233
- }),
234
- async () => ({ sessionCookie: 'browser-session', csrfToken: 'browser-csrf' }),
235
- )
236
-
237
- expect(result).toBe(recoveredHttp)
238
- expect(savedCredentials).toMatchObject({
239
- sessionCookie: 'browser-session',
240
- csrfToken: 'browser-csrf',
241
- })
242
- })
243
-
244
- it('skips browser extraction when no credentials exist', async () => {
245
- let browserExtractionCalled = false
246
- const manager = {
247
- getCredentials: async () => null,
248
- setCredentials: async () => {
249
- throw new Error('should not save')
250
- },
251
- clearSessionState: async () => {},
252
- }
253
-
254
- await expect(
255
- createAuthenticatedHttp(
256
- manager,
257
- () => ({ checkLogin: async () => null }),
258
- undefined,
259
- async () => {
260
- browserExtractionCalled = true
261
- return { sessionCookie: 's', csrfToken: 'c' }
262
- },
263
- ),
264
- ).rejects.toThrow('Not logged in')
265
- expect(browserExtractionCalled).toBe(false)
266
- })
267
156
  })
@@ -1,42 +1,29 @@
1
1
  import { CredentialManager } from '../credential-manager'
2
2
  import { SomaHttp } from '../http'
3
3
  import { recoverSession } from '../session-recovery'
4
- import * as stderr from '../shared/utils/stderr'
5
4
  import type { Credentials } from '../types'
6
5
 
7
6
  type CredentialStore = Pick<CredentialManager, 'clearSessionState' | 'getCredentials' | 'setCredentials'>
8
7
  type AuthenticatedHttp = Pick<SomaHttp, 'checkLogin'>
9
8
  type ReloginHttp = Pick<SomaHttp, 'checkLogin' | 'getCsrfToken' | 'getSessionCookie' | 'login'>
10
- type BrowserExtractor = () => Promise<{ csrfToken: string; sessionCookie: string } | null>
11
9
 
12
- const NOT_LOGGED_IN_MESSAGE = 'Not logged in. Run: opensoma auth login or opensoma auth extract'
13
- const STALE_SESSION_MESSAGE =
14
- 'Session expired. Run: opensoma auth login or opensoma auth extract (saved id/password were preserved)'
10
+ const NOT_LOGGED_IN_MESSAGE = 'Not logged in. Run: opensoma auth login'
11
+ const STALE_SESSION_MESSAGE = 'Session expired. Run: opensoma auth login (saved id/password were preserved)'
15
12
 
16
13
  function defaultCreateHttp(credentials: Credentials): SomaHttp {
17
14
  return new SomaHttp({ sessionCookie: credentials.sessionCookie, csrfToken: credentials.csrfToken })
18
15
  }
19
16
 
20
- async function defaultExtractBrowserCredentials(): Promise<{ csrfToken: string; sessionCookie: string } | null> {
21
- const { TokenExtractor } = await import('../token-extractor')
22
- const { resolveExtractedCredentials } = await import('./auth')
23
- const candidates = await new TokenExtractor().extractCandidates()
24
- if (candidates.length === 0) return null
25
- return resolveExtractedCredentials(candidates)
26
- }
27
-
28
17
  export function createAuthenticatedHttp(): Promise<SomaHttp>
29
18
  export function createAuthenticatedHttp<T extends AuthenticatedHttp>(
30
19
  manager: CredentialStore,
31
20
  createHttp: (credentials: Credentials) => T,
32
21
  createReloginHttp?: () => ReloginHttp,
33
- recoverViaBrowser?: BrowserExtractor,
34
22
  ): Promise<T>
35
23
  export async function createAuthenticatedHttp<T extends AuthenticatedHttp>(
36
24
  manager: CredentialStore = new CredentialManager(),
37
25
  createHttp?: (credentials: Credentials) => T,
38
26
  createReloginHttp: () => ReloginHttp = () => new SomaHttp(),
39
- recoverViaBrowser: BrowserExtractor = defaultExtractBrowserCredentials,
40
27
  ): Promise<SomaHttp | T> {
41
28
  const creds = await manager.getCredentials()
42
29
  if (!creds) {
@@ -52,26 +39,7 @@ export async function createAuthenticatedHttp<T extends AuthenticatedHttp>(
52
39
  if (refreshedCredentials) {
53
40
  return createHttp ? createHttp(refreshedCredentials) : defaultCreateHttp(refreshedCredentials)
54
41
  }
55
- } catch {
56
- // Password recovery failed — try browser extraction next
57
- }
58
-
59
- try {
60
- stderr.info('Session expired. Attempting browser token extraction...')
61
- const extracted = await recoverViaBrowser()
62
- if (extracted) {
63
- const browserCredentials: Credentials = {
64
- sessionCookie: extracted.sessionCookie,
65
- csrfToken: extracted.csrfToken,
66
- loggedInAt: new Date().toISOString(),
67
- }
68
- await manager.setCredentials(browserCredentials)
69
- stderr.info('Browser token extraction successful.')
70
- return createHttp ? createHttp(browserCredentials) : defaultCreateHttp(browserCredentials)
71
- }
72
- } catch {
73
- // Browser extraction also failed
74
- }
42
+ } catch {}
75
43
 
76
44
  await manager.clearSessionState()
77
45
  throw new Error(STALE_SESSION_MESSAGE)
@@ -49,7 +49,7 @@ async function runTeamAction(params: {
49
49
  try {
50
50
  const http = await getHttpOrExit()
51
51
  const user = await http.checkLogin()
52
- if (!user) throw new Error('Not logged in. Run: opensoma auth login or opensoma auth extract')
52
+ if (!user) throw new Error('Not logged in. Run: opensoma auth login')
53
53
  if (!user.userNo) throw new Error('현재 사용자의 userNo를 확인할 수 없습니다.')
54
54
 
55
55
  const response = await http.postJson<{ resultCode?: string }>(
@@ -1,9 +1,9 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'
2
2
  import { existsSync } from 'node:fs'
3
3
  import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
4
- import { homedir } from 'node:os'
5
4
  import { join } from 'node:path'
6
5
 
6
+ import { getConfigDir } from './shared/utils/config-dir'
7
7
  import type { Credentials } from './types'
8
8
 
9
9
  interface EncryptedSecret {
@@ -26,7 +26,7 @@ export class CredentialManager {
26
26
  private encryptionKeyPath: string
27
27
 
28
28
  constructor(configDir?: string) {
29
- this.configDir = configDir ?? join(homedir(), '.config', 'opensoma')
29
+ this.configDir = configDir ?? getConfigDir()
30
30
  this.credentialsPath = join(this.configDir, 'credentials.json')
31
31
  this.encryptionKeyPath = join(this.configDir, 'credentials.key')
32
32
  }
package/src/errors.ts CHANGED
@@ -1,9 +1,5 @@
1
- /**
2
- * Error thrown when authentication is required but not valid.
3
- * Provides a clear message indicating the need to authenticate.
4
- */
5
1
  export class AuthenticationError extends Error {
6
- constructor(message = 'Authentication required. Please login with: opensoma auth login or opensoma auth extract') {
2
+ constructor(message = 'Authentication required. Please login with: opensoma auth login') {
7
3
  super(message)
8
4
  this.name = 'AuthenticationError'
9
5
  }
@@ -0,0 +1,41 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ import { CONFIG_DIR_ENV_VAR, getConfigDir } from './config-dir'
6
+
7
+ let originalValue: string | undefined
8
+
9
+ beforeEach(() => {
10
+ originalValue = process.env[CONFIG_DIR_ENV_VAR]
11
+ delete process.env[CONFIG_DIR_ENV_VAR]
12
+ })
13
+
14
+ afterEach(() => {
15
+ if (originalValue === undefined) {
16
+ delete process.env[CONFIG_DIR_ENV_VAR]
17
+ } else {
18
+ process.env[CONFIG_DIR_ENV_VAR] = originalValue
19
+ }
20
+ })
21
+
22
+ describe('getConfigDir', () => {
23
+ it('falls back to ~/.config/opensoma when env var is unset', () => {
24
+ expect(getConfigDir()).toBe(join(homedir(), '.config', 'opensoma'))
25
+ })
26
+
27
+ it('returns OPENSOMA_CONFIG_DIR when set', () => {
28
+ process.env[CONFIG_DIR_ENV_VAR] = '/custom/config/path'
29
+ expect(getConfigDir()).toBe('/custom/config/path')
30
+ })
31
+
32
+ it('falls back to default when env var is empty string', () => {
33
+ process.env[CONFIG_DIR_ENV_VAR] = ''
34
+ expect(getConfigDir()).toBe(join(homedir(), '.config', 'opensoma'))
35
+ })
36
+
37
+ it('preserves relative paths verbatim', () => {
38
+ process.env[CONFIG_DIR_ENV_VAR] = './local-config'
39
+ expect(getConfigDir()).toBe('./local-config')
40
+ })
41
+ })
@@ -0,0 +1,20 @@
1
+ import { homedir } from 'node:os'
2
+ import { join } from 'node:path'
3
+
4
+ export const CONFIG_DIR_ENV_VAR = 'OPENSOMA_CONFIG_DIR'
5
+
6
+ /**
7
+ * Resolves the directory used to persist opensoma state (credentials, pending
8
+ * reservations, etc.).
9
+ *
10
+ * Resolution order:
11
+ * 1. `OPENSOMA_CONFIG_DIR` environment variable (if set and non-empty)
12
+ * 2. `~/.config/opensoma`
13
+ */
14
+ export function getConfigDir(): string {
15
+ const fromEnv = process.env[CONFIG_DIR_ENV_VAR]
16
+ if (fromEnv && fromEnv.length > 0) {
17
+ return fromEnv
18
+ }
19
+ return join(homedir(), '.config', 'opensoma')
20
+ }
@@ -1,8 +1,9 @@
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'
5
4
 
5
+ import { getConfigDir } from './shared/utils/config-dir'
6
+
6
7
  export interface TozPendingReservation {
7
8
  reservationId: string
8
9
  cookies: Record<string, string>
@@ -30,7 +31,7 @@ export class TozPendingStore {
30
31
  private readonly path: string
31
32
 
32
33
  constructor(configDir?: string) {
33
- const dir = configDir ?? join(homedir(), '.config', 'opensoma')
34
+ const dir = configDir ?? getConfigDir()
34
35
  this.path = join(dir, 'toz-pending.json')
35
36
  }
36
37
 
@@ -1,43 +0,0 @@
1
- type BrowserConfig = {
2
- name: string;
3
- macPath: string;
4
- linuxPath: string;
5
- keychainService: string;
6
- keychainAccount: string;
7
- };
8
- export interface ExtractedSessionCandidate {
9
- browser: string;
10
- lastAccessUtc: number;
11
- profile: string;
12
- sessionCookie: string;
13
- }
14
- export declare const BROWSERS: BrowserConfig[];
15
- export type TokenExtractorOptions = {
16
- platform?: NodeJS.Platform;
17
- homeDirectory?: string;
18
- debug?: boolean;
19
- };
20
- export declare class TokenExtractor {
21
- private readonly platform;
22
- private readonly homeDirectory;
23
- private readonly debugEnabled;
24
- constructor(options?: TokenExtractorOptions);
25
- constructor(platform?: NodeJS.Platform, homeDirectory?: string, debug?: boolean);
26
- private log;
27
- extract(): Promise<{
28
- sessionCookie: string;
29
- } | null>;
30
- extractCandidates(): Promise<ExtractedSessionCandidate[]>;
31
- findCookieDatabases(): string[];
32
- private decryptCookie;
33
- private getMacOSEncryptionKey;
34
- private decryptChromiumValue;
35
- private getBrowserByPath;
36
- private addCandidate;
37
- private findBrowserCookieDatabases;
38
- private getBrowserRoot;
39
- private isSupportedProfileDirectory;
40
- private normalizeLastAccessUtc;
41
- }
42
- export {};
43
- //# sourceMappingURL=token-extractor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"token-extractor.d.ts","sourceRoot":"","sources":["../../src/token-extractor.ts"],"names":[],"mappings":"AAeA,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;CACxB,CAAA;AAQD,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,eAAO,MAAM,QAAQ,EAAE,aAAa,EA2CnC,CAAA;AA+CD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAA;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,OAAO,CAAC,EAAE,qBAAqB;gBAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;IAa/E,OAAO,CAAC,GAAG;IAML,OAAO,IAAI,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAWpD,iBAAiB,IAAI,OAAO,CAAC,yBAAyB,EAAE,CAAC;IAsE/D,mBAAmB,IAAI,MAAM,EAAE;YAQjB,aAAa;YAqBb,qBAAqB;IAanC,OAAO,CAAC,oBAAoB;IAkB5B,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,0BAA0B;IAmBlC,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,2BAA2B;IAInC,OAAO,CAAC,sBAAsB;CAO/B"}