opensoma 0.4.0 → 0.5.1

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 (75) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/client.d.ts +7 -1
  3. package/dist/src/client.d.ts.map +1 -1
  4. package/dist/src/client.js +13 -11
  5. package/dist/src/client.js.map +1 -1
  6. package/dist/src/commands/auth.d.ts +1 -1
  7. package/dist/src/commands/auth.d.ts.map +1 -1
  8. package/dist/src/commands/auth.js +94 -52
  9. package/dist/src/commands/auth.js.map +1 -1
  10. package/dist/src/constants.d.ts +40 -0
  11. package/dist/src/constants.d.ts.map +1 -1
  12. package/dist/src/constants.js +42 -0
  13. package/dist/src/constants.js.map +1 -1
  14. package/dist/src/formatters.d.ts.map +1 -1
  15. package/dist/src/formatters.js +42 -16
  16. package/dist/src/formatters.js.map +1 -1
  17. package/dist/src/index.d.ts +3 -0
  18. package/dist/src/index.d.ts.map +1 -1
  19. package/dist/src/index.js +2 -0
  20. package/dist/src/index.js.map +1 -1
  21. package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
  22. package/dist/src/shared/utils/swmaestro.js +1 -5
  23. package/dist/src/shared/utils/swmaestro.js.map +1 -1
  24. package/dist/src/shared/utils/toz.d.ts +23 -0
  25. package/dist/src/shared/utils/toz.d.ts.map +1 -0
  26. package/dist/src/shared/utils/toz.js +72 -0
  27. package/dist/src/shared/utils/toz.js.map +1 -0
  28. package/dist/src/token-extractor.d.ts +9 -1
  29. package/dist/src/token-extractor.d.ts.map +1 -1
  30. package/dist/src/token-extractor.js +54 -10
  31. package/dist/src/token-extractor.js.map +1 -1
  32. package/dist/src/toz-formatters.d.ts +9 -0
  33. package/dist/src/toz-formatters.d.ts.map +1 -0
  34. package/dist/src/toz-formatters.js +151 -0
  35. package/dist/src/toz-formatters.js.map +1 -0
  36. package/dist/src/toz-http.d.ts +27 -0
  37. package/dist/src/toz-http.d.ts.map +1 -0
  38. package/dist/src/toz-http.js +154 -0
  39. package/dist/src/toz-http.js.map +1 -0
  40. package/dist/src/types.d.ts +52 -0
  41. package/dist/src/types.d.ts.map +1 -1
  42. package/dist/src/types.js +46 -0
  43. package/dist/src/types.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/__fixtures__/toz/toz_all_branches.json +211 -0
  46. package/src/__fixtures__/toz/toz_booking.html +2190 -0
  47. package/src/__fixtures__/toz/toz_boothes.json +59 -0
  48. package/src/__fixtures__/toz/toz_duration.json +25 -0
  49. package/src/__fixtures__/toz/toz_mypage_response.html +388 -0
  50. package/src/__fixtures__/toz/toz_page.html +211 -0
  51. package/src/client.test.ts +135 -117
  52. package/src/client.ts +16 -12
  53. package/src/commands/auth.test.ts +7 -7
  54. package/src/commands/auth.ts +107 -50
  55. package/src/commands/helpers.test.ts +8 -8
  56. package/src/commands/report.test.ts +7 -7
  57. package/src/constants.ts +50 -0
  58. package/src/credential-manager.test.ts +5 -5
  59. package/src/formatters.test.ts +22 -22
  60. package/src/formatters.ts +44 -16
  61. package/src/http.test.ts +37 -37
  62. package/src/index.ts +3 -0
  63. package/src/shared/utils/mentoring-params.test.ts +16 -16
  64. package/src/shared/utils/swmaestro.test.ts +87 -8
  65. package/src/shared/utils/swmaestro.ts +1 -6
  66. package/src/shared/utils/toz.test.ts +138 -0
  67. package/src/shared/utils/toz.ts +100 -0
  68. package/src/token-extractor.test.ts +40 -15
  69. package/src/token-extractor.ts +65 -13
  70. package/src/toz-formatters.test.ts +197 -0
  71. package/src/toz-formatters.ts +211 -0
  72. package/src/toz-http.test.ts +157 -0
  73. package/src/toz-http.ts +188 -0
  74. package/src/types.test.ts +220 -204
  75. package/src/types.ts +58 -0
@@ -0,0 +1,157 @@
1
+ import { afterEach, describe, expect, it, mock } from 'bun:test'
2
+
3
+ import { TOZ_BASE_URL } from './constants'
4
+ import { TozHttp } from './toz-http'
5
+
6
+ const originalFetch = globalThis.fetch
7
+
8
+ afterEach(() => {
9
+ globalThis.fetch = originalFetch
10
+ mock.restore()
11
+ })
12
+
13
+ describe('TozHttp', () => {
14
+ it('seeds JSESSIONID by GETting index.htm during bootstrap', async () => {
15
+ const fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
16
+ expect(String(input)).toBe(`${TOZ_BASE_URL}/index.htm`)
17
+ expect(init?.method).toBe('GET')
18
+ return createResponse('ok', ['JSESSIONID=ABC123; Path=/; HttpOnly'])
19
+ })
20
+ globalThis.fetch = fetchMock as typeof fetch
21
+
22
+ const http = new TozHttp()
23
+ await http.bootstrap()
24
+
25
+ expect(http.getSessionCookie()).toBe('ABC123')
26
+ expect(fetchMock).toHaveBeenCalledTimes(1)
27
+ })
28
+
29
+ it('skips bootstrap when JSESSIONID is already set', async () => {
30
+ const fetchMock = mock(async () => createResponse('ok'))
31
+ globalThis.fetch = fetchMock as typeof fetch
32
+
33
+ const http = new TozHttp({ sessionCookie: 'JSESSIONID=EXISTING' })
34
+ await http.bootstrap()
35
+
36
+ expect(fetchMock).not.toHaveBeenCalled()
37
+ expect(http.getSessionCookie()).toBe('EXISTING')
38
+ })
39
+
40
+ it('sends URL-encoded POST bodies with the required headers and cookie', async () => {
41
+ const fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
42
+ expect(String(input)).toBe(`${TOZ_BASE_URL}/ajaxGetEnableBoothes.htm`)
43
+ expect(init?.method).toBe('POST')
44
+
45
+ const headers = init?.headers as Record<string, string>
46
+ expect(headers['Content-Type']).toBe('application/x-www-form-urlencoded; charset=UTF-8')
47
+ expect(headers['X-Requested-With']).toBe('XMLHttpRequest')
48
+ expect(headers.Referer).toBe(`${TOZ_BASE_URL}/booking.htm`)
49
+ expect(headers.cookie).toBe('JSESSIONID=ABC123')
50
+
51
+ expect(init?.body).toBe('basedate=2026-04-21&starttime=1400&durationTime=0200&userCount=2&branchIds=27%2C')
52
+ return createResponse('[]', [], 'application/json')
53
+ })
54
+ globalThis.fetch = fetchMock as typeof fetch
55
+
56
+ const http = new TozHttp({ sessionCookie: 'ABC123' })
57
+ const text = await http.post('/ajaxGetEnableBoothes.htm', {
58
+ basedate: '2026-04-21',
59
+ starttime: '1400',
60
+ durationTime: '0200',
61
+ userCount: '2',
62
+ branchIds: '27,',
63
+ })
64
+
65
+ expect(text).toBe('[]')
66
+ })
67
+
68
+ it('parses the JSON body returned by postJson', async () => {
69
+ globalThis.fetch = mock(async () => createResponse('{"result":"SUCCESS","resultMsg":"abc"}')) as typeof fetch
70
+
71
+ const http = new TozHttp({ sessionCookie: 'ABC' })
72
+ const json = await http.postJson<{ result: string; resultMsg: string }>('/ajaxReservationBooth.htm', {})
73
+
74
+ expect(json).toEqual({ result: 'SUCCESS', resultMsg: 'abc' })
75
+ })
76
+
77
+ it('trims the plain-text body returned by postText', async () => {
78
+ globalThis.fetch = mock(async () => createResponse('SUCCESS\n')) as typeof fetch
79
+
80
+ const http = new TozHttp({ sessionCookie: 'ABC' })
81
+ expect(await http.postText('/ajaxHpVerify.htm', {})).toBe('SUCCESS')
82
+ })
83
+
84
+ it('strips the "JSESSIONID=" prefix when constructed with a full cookie string', async () => {
85
+ const http = new TozHttp({ sessionCookie: 'JSESSIONID=XYZ789' })
86
+ expect(http.getSessionCookie()).toBe('XYZ789')
87
+ expect(http.getCookies()).toEqual({ JSESSIONID: 'XYZ789' })
88
+ })
89
+
90
+ it('preserves every cookie when constructed with a cookies map', async () => {
91
+ const http = new TozHttp({ cookies: { JSESSIONID: 'A', other: 'B' } })
92
+ expect(http.getCookies()).toEqual({ JSESSIONID: 'A', other: 'B' })
93
+ })
94
+
95
+ it('throws on a non-2xx response', async () => {
96
+ globalThis.fetch = mock(async () =>
97
+ createResponse('Server Error', [], 'text/html', { status: 500 }),
98
+ ) as typeof fetch
99
+
100
+ const http = new TozHttp({ sessionCookie: 'ABC' })
101
+ await expect(http.get('/index.htm')).rejects.toThrow(/HTTP 500/)
102
+ })
103
+
104
+ it('refuses to follow cross-origin redirects to prevent cookie leaks', async () => {
105
+ globalThis.fetch = mock(async (input: RequestInfo | URL) => {
106
+ const url = String(input)
107
+ if (url.endsWith('/index.htm')) {
108
+ return redirectResponse('https://evil.example.com/steal')
109
+ }
110
+ throw new Error(`Should not fetch ${url} — cross-origin redirect must be blocked`)
111
+ }) as typeof fetch
112
+
113
+ const http = new TozHttp({ sessionCookie: 'ABC' })
114
+ await expect(http.get('/index.htm')).rejects.toThrow(/cross-origin redirect/)
115
+ })
116
+
117
+ it('follows same-origin redirects normally', async () => {
118
+ let hit = 0
119
+ globalThis.fetch = mock(async (input: RequestInfo | URL) => {
120
+ const url = String(input)
121
+ hit += 1
122
+ if (hit === 1) return redirectResponse(`${TOZ_BASE_URL}/mypage.htm`)
123
+ expect(url).toBe(`${TOZ_BASE_URL}/mypage.htm`)
124
+ return createResponse('mypage content')
125
+ }) as typeof fetch
126
+
127
+ const http = new TozHttp({ sessionCookie: 'ABC' })
128
+ const body = await http.get('/mypage_login_.htm')
129
+ expect(body).toBe('mypage content')
130
+ expect(hit).toBe(2)
131
+ })
132
+ })
133
+
134
+ function redirectResponse(location: string): Response {
135
+ const headers = new Headers({ location })
136
+ const response = new Response(null, { status: 302, headers })
137
+ Object.defineProperty(response.headers, 'getSetCookie', {
138
+ value: () => [],
139
+ configurable: true,
140
+ })
141
+ return response
142
+ }
143
+
144
+ function createResponse(
145
+ body: string,
146
+ cookies: string[] = [],
147
+ contentType = 'text/html',
148
+ options: { status?: number } = {},
149
+ ): Response {
150
+ const headers = new Headers({ 'Content-Type': contentType })
151
+ const response = new Response(body, { headers, status: options.status ?? 200 })
152
+ Object.defineProperty(response.headers, 'getSetCookie', {
153
+ value: () => cookies,
154
+ configurable: true,
155
+ })
156
+ return response
157
+ }
@@ -0,0 +1,188 @@
1
+ import { TOZ_BASE_URL } from './constants'
2
+
3
+ const DEFAULT_USER_AGENT =
4
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36'
5
+ const DEFAULT_ACCEPT_LANGUAGE = 'ko,en-US;q=0.9,en;q=0.8'
6
+
7
+ interface HeadersWithCookieHelpers extends Omit<Headers, 'getSetCookie'> {
8
+ getSetCookie?: () => string[]
9
+ }
10
+
11
+ export interface TozHttpOptions {
12
+ sessionCookie?: string
13
+ cookies?: Record<string, string>
14
+ }
15
+
16
+ export interface TozHttpState {
17
+ cookies: Record<string, string>
18
+ }
19
+
20
+ export class TozHttp {
21
+ private cookies = new Map<string, string>()
22
+
23
+ constructor(options: TozHttpOptions = {}) {
24
+ if (options.cookies) {
25
+ for (const [name, value] of Object.entries(options.cookies)) {
26
+ this.cookies.set(name, value)
27
+ }
28
+ } else if (options.sessionCookie) {
29
+ this.cookies.set('JSESSIONID', stripJsessionPrefix(options.sessionCookie))
30
+ }
31
+ }
32
+
33
+ async bootstrap(): Promise<void> {
34
+ if (this.cookies.has('JSESSIONID')) return
35
+ await this.get('/index.htm')
36
+ }
37
+
38
+ async get(path: string, params?: Record<string, string>): Promise<string> {
39
+ const response = await fetch(this.buildUrl(path, params), {
40
+ method: 'GET',
41
+ headers: this.buildHeaders(),
42
+ redirect: 'manual',
43
+ })
44
+
45
+ this.updateFromResponse(response)
46
+ return this.readFollowingRedirects(response, 'GET')
47
+ }
48
+
49
+ async post(path: string, body: Record<string, string>): Promise<string> {
50
+ const response = await this.rawPost(path, body)
51
+ return this.readFollowingRedirects(response, 'POST')
52
+ }
53
+
54
+ async postJson<T>(path: string, body: Record<string, string>): Promise<T> {
55
+ const text = await this.post(path, body)
56
+ try {
57
+ return JSON.parse(text) as T
58
+ } catch (error) {
59
+ throw new Error(
60
+ `Failed to parse JSON response from ${path}: ${error instanceof Error ? error.message : 'unknown'}`,
61
+ )
62
+ }
63
+ }
64
+
65
+ async postText(path: string, body: Record<string, string>): Promise<string> {
66
+ return (await this.post(path, body)).trim()
67
+ }
68
+
69
+ getCookies(): Record<string, string> {
70
+ return Object.fromEntries(this.cookies)
71
+ }
72
+
73
+ getSessionCookie(): string | undefined {
74
+ return this.cookies.get('JSESSIONID')
75
+ }
76
+
77
+ getState(): TozHttpState {
78
+ return { cookies: this.getCookies() }
79
+ }
80
+
81
+ private async rawPost(path: string, body: Record<string, string>): Promise<Response> {
82
+ const url = this.buildUrl(path)
83
+ const formBody = new URLSearchParams(body).toString()
84
+
85
+ const response = await fetch(url, {
86
+ method: 'POST',
87
+ headers: {
88
+ ...this.buildHeaders(),
89
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
90
+ 'X-Requested-With': 'XMLHttpRequest',
91
+ Referer: this.buildUrl('/booking.htm'),
92
+ },
93
+ body: formBody,
94
+ redirect: 'manual',
95
+ })
96
+
97
+ this.updateFromResponse(response)
98
+ return response
99
+ }
100
+
101
+ private async readFollowingRedirects(initial: Response, method: string): Promise<string> {
102
+ let response = initial
103
+ let hops = 0
104
+
105
+ while (response.status >= 300 && response.status < 400 && hops < 5) {
106
+ const location = response.headers.get('location')
107
+ if (!location) break
108
+ const redirectUrl = location.startsWith('http') ? location : new URL(location, `${TOZ_BASE_URL}/`).toString()
109
+ if (!isSameOrigin(redirectUrl, TOZ_BASE_URL)) {
110
+ throw new Error(`Refusing to follow cross-origin redirect to ${new URL(redirectUrl).origin}`)
111
+ }
112
+ response = await fetch(redirectUrl, {
113
+ method: 'GET',
114
+ headers: this.buildHeaders(),
115
+ redirect: 'manual',
116
+ })
117
+ this.updateFromResponse(response)
118
+ hops += 1
119
+ }
120
+
121
+ if (!response.ok) {
122
+ throw new Error(`Toz ${method} ${initial.url} failed: HTTP ${response.status} ${response.statusText}`)
123
+ }
124
+
125
+ return response.text()
126
+ }
127
+
128
+ private buildUrl(path: string, params?: Record<string, string>): string {
129
+ const normalized = path.startsWith('/') ? path.slice(1) : path
130
+ const url = new URL(normalized, `${TOZ_BASE_URL}/`)
131
+ if (params) {
132
+ for (const [key, value] of Object.entries(params)) {
133
+ url.searchParams.set(key, value)
134
+ }
135
+ }
136
+ return url.toString()
137
+ }
138
+
139
+ private buildHeaders(): Record<string, string> {
140
+ const cookieHeader = this.serializeCookies()
141
+ return {
142
+ 'Accept-Language': DEFAULT_ACCEPT_LANGUAGE,
143
+ 'User-Agent': DEFAULT_USER_AGENT,
144
+ ...(cookieHeader ? { cookie: cookieHeader } : {}),
145
+ }
146
+ }
147
+
148
+ private updateFromResponse(response: Response): void {
149
+ for (const cookie of readSetCookies(response.headers)) {
150
+ this.setCookie(cookie)
151
+ }
152
+ }
153
+
154
+ private setCookie(cookieHeader: string): void {
155
+ const pair = cookieHeader.split(';')[0]?.trim()
156
+ if (!pair) return
157
+ const idx = pair.indexOf('=')
158
+ if (idx === -1) return
159
+ const name = pair.slice(0, idx).trim()
160
+ const value = pair.slice(idx + 1).trim()
161
+ if (name) this.cookies.set(name, value)
162
+ }
163
+
164
+ private serializeCookies(): string {
165
+ return [...this.cookies.entries()].map(([name, value]) => `${name}=${value}`).join('; ')
166
+ }
167
+ }
168
+
169
+ function stripJsessionPrefix(cookie: string): string {
170
+ return cookie.includes('=') ? (cookie.split('=', 2)[1] ?? cookie) : cookie
171
+ }
172
+
173
+ function isSameOrigin(url: string, baseUrl: string): boolean {
174
+ try {
175
+ return new URL(url).origin === new URL(baseUrl).origin
176
+ } catch {
177
+ return false
178
+ }
179
+ }
180
+
181
+ function readSetCookies(headers: Headers): string[] {
182
+ const enhanced = headers as HeadersWithCookieHelpers
183
+ if (typeof enhanced.getSetCookie === 'function') {
184
+ return enhanced.getSetCookie()
185
+ }
186
+ const single = headers.get('set-cookie')
187
+ return single ? [single] : []
188
+ }