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.
- package/dist/package.json +1 -1
- package/dist/src/client.d.ts +7 -1
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +13 -11
- package/dist/src/client.js.map +1 -1
- 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 +94 -52
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/constants.d.ts +40 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +42 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/formatters.d.ts.map +1 -1
- package/dist/src/formatters.js +42 -16
- package/dist/src/formatters.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
- package/dist/src/shared/utils/swmaestro.js +1 -5
- package/dist/src/shared/utils/swmaestro.js.map +1 -1
- package/dist/src/shared/utils/toz.d.ts +23 -0
- package/dist/src/shared/utils/toz.d.ts.map +1 -0
- package/dist/src/shared/utils/toz.js +72 -0
- package/dist/src/shared/utils/toz.js.map +1 -0
- package/dist/src/token-extractor.d.ts +9 -1
- package/dist/src/token-extractor.d.ts.map +1 -1
- package/dist/src/token-extractor.js +54 -10
- package/dist/src/token-extractor.js.map +1 -1
- package/dist/src/toz-formatters.d.ts +9 -0
- package/dist/src/toz-formatters.d.ts.map +1 -0
- package/dist/src/toz-formatters.js +151 -0
- package/dist/src/toz-formatters.js.map +1 -0
- package/dist/src/toz-http.d.ts +27 -0
- package/dist/src/toz-http.d.ts.map +1 -0
- package/dist/src/toz-http.js +154 -0
- package/dist/src/toz-http.js.map +1 -0
- package/dist/src/types.d.ts +52 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +46 -0
- package/dist/src/types.js.map +1 -1
- package/package.json +1 -1
- package/src/__fixtures__/toz/toz_all_branches.json +211 -0
- package/src/__fixtures__/toz/toz_booking.html +2190 -0
- package/src/__fixtures__/toz/toz_boothes.json +59 -0
- package/src/__fixtures__/toz/toz_duration.json +25 -0
- package/src/__fixtures__/toz/toz_mypage_response.html +388 -0
- package/src/__fixtures__/toz/toz_page.html +211 -0
- package/src/client.test.ts +135 -117
- package/src/client.ts +16 -12
- package/src/commands/auth.test.ts +7 -7
- package/src/commands/auth.ts +107 -50
- package/src/commands/helpers.test.ts +8 -8
- package/src/commands/report.test.ts +7 -7
- package/src/constants.ts +50 -0
- package/src/credential-manager.test.ts +5 -5
- package/src/formatters.test.ts +22 -22
- package/src/formatters.ts +44 -16
- package/src/http.test.ts +37 -37
- package/src/index.ts +3 -0
- package/src/shared/utils/mentoring-params.test.ts +16 -16
- package/src/shared/utils/swmaestro.test.ts +87 -8
- package/src/shared/utils/swmaestro.ts +1 -6
- package/src/shared/utils/toz.test.ts +138 -0
- package/src/shared/utils/toz.ts +100 -0
- package/src/token-extractor.test.ts +40 -15
- package/src/token-extractor.ts +65 -13
- package/src/toz-formatters.test.ts +197 -0
- package/src/toz-formatters.ts +211 -0
- package/src/toz-http.test.ts +157 -0
- package/src/toz-http.ts +188 -0
- package/src/types.test.ts +220 -204
- 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
|
+
}
|
package/src/toz-http.ts
ADDED
|
@@ -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
|
+
}
|