opensoma 0.1.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/.claude-plugin/README.md +145 -0
- package/.claude-plugin/plugin.json +23 -0
- package/.github/workflows/release.yml +86 -0
- package/.oxfmtrc.json +9 -0
- package/.oxlintrc.json +4 -0
- package/AGENTS.md +78 -0
- package/README.md +249 -0
- package/bun.lock +297 -0
- package/bunfig.toml +2 -0
- package/dist/package.json +56 -0
- package/dist/src/cli.d.ts +5 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +39 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/client.d.ts +98 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +141 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/commands/auth.d.ts +3 -0
- package/dist/src/commands/auth.d.ts.map +1 -0
- package/dist/src/commands/auth.js +125 -0
- package/dist/src/commands/auth.js.map +1 -0
- package/dist/src/commands/dashboard.d.ts +3 -0
- package/dist/src/commands/dashboard.d.ts.map +1 -0
- package/dist/src/commands/dashboard.js +33 -0
- package/dist/src/commands/dashboard.js.map +1 -0
- package/dist/src/commands/event.d.ts +3 -0
- package/dist/src/commands/event.d.ts.map +1 -0
- package/dist/src/commands/event.js +58 -0
- package/dist/src/commands/event.js.map +1 -0
- package/dist/src/commands/helpers.d.ts +3 -0
- package/dist/src/commands/helpers.d.ts.map +1 -0
- package/dist/src/commands/helpers.js +12 -0
- package/dist/src/commands/helpers.js.map +1 -0
- package/dist/src/commands/index.d.ts +9 -0
- package/dist/src/commands/index.d.ts.map +1 -0
- package/dist/src/commands/index.js +9 -0
- package/dist/src/commands/index.js.map +1 -0
- package/dist/src/commands/member.d.ts +3 -0
- package/dist/src/commands/member.d.ts.map +1 -0
- package/dist/src/commands/member.js +23 -0
- package/dist/src/commands/member.js.map +1 -0
- package/dist/src/commands/mentoring.d.ts +3 -0
- package/dist/src/commands/mentoring.d.ts.map +1 -0
- package/dist/src/commands/mentoring.js +154 -0
- package/dist/src/commands/mentoring.js.map +1 -0
- package/dist/src/commands/notice.d.ts +3 -0
- package/dist/src/commands/notice.d.ts.map +1 -0
- package/dist/src/commands/notice.js +42 -0
- package/dist/src/commands/notice.js.map +1 -0
- package/dist/src/commands/room.d.ts +3 -0
- package/dist/src/commands/room.d.ts.map +1 -0
- package/dist/src/commands/room.js +79 -0
- package/dist/src/commands/room.js.map +1 -0
- package/dist/src/commands/team.d.ts +3 -0
- package/dist/src/commands/team.d.ts.map +1 -0
- package/dist/src/commands/team.js +20 -0
- package/dist/src/commands/team.js.map +1 -0
- package/dist/src/constants.d.ts +43 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +62 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/credential-manager.d.ts +15 -0
- package/dist/src/credential-manager.d.ts.map +1 -0
- package/dist/src/credential-manager.js +40 -0
- package/dist/src/credential-manager.js.map +1 -0
- package/dist/src/formatters.d.ts +15 -0
- package/dist/src/formatters.d.ts.map +1 -0
- package/dist/src/formatters.js +382 -0
- package/dist/src/formatters.js.map +1 -0
- package/dist/src/http.d.ts +32 -0
- package/dist/src/http.d.ts.map +1 -0
- package/dist/src/http.js +143 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/shared/utils/error-handler.d.ts +2 -0
- package/dist/src/shared/utils/error-handler.d.ts.map +1 -0
- package/dist/src/shared/utils/error-handler.js +7 -0
- package/dist/src/shared/utils/error-handler.js.map +1 -0
- package/dist/src/shared/utils/mentoring-params.d.ts +15 -0
- package/dist/src/shared/utils/mentoring-params.d.ts.map +1 -0
- package/dist/src/shared/utils/mentoring-params.js +39 -0
- package/dist/src/shared/utils/mentoring-params.js.map +1 -0
- package/dist/src/shared/utils/output.d.ts +2 -0
- package/dist/src/shared/utils/output.d.ts.map +1 -0
- package/dist/src/shared/utils/output.js +4 -0
- package/dist/src/shared/utils/output.js.map +1 -0
- package/dist/src/shared/utils/stderr.d.ts +5 -0
- package/dist/src/shared/utils/stderr.d.ts.map +1 -0
- package/dist/src/shared/utils/stderr.js +19 -0
- package/dist/src/shared/utils/stderr.js.map +1 -0
- package/dist/src/shared/utils/swmaestro.d.ts +33 -0
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -0
- package/dist/src/shared/utils/swmaestro.js +164 -0
- package/dist/src/shared/utils/swmaestro.js.map +1 -0
- package/dist/src/token-extractor.d.ts +23 -0
- package/dist/src/token-extractor.d.ts.map +1 -0
- package/dist/src/token-extractor.js +163 -0
- package/dist/src/token-extractor.js.map +1 -0
- package/dist/src/types.d.ts +176 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +110 -0
- package/dist/src/types.js.map +1 -0
- package/e2e/.gitkeep +0 -0
- package/package.json +56 -0
- package/scripts/postbuild.ts +11 -0
- package/scripts/prepublish.ts +9 -0
- package/scripts/test.ts +82 -0
- package/skills/opensoma/SKILL.md +345 -0
- package/skills/opensoma/references/common-patterns.md +182 -0
- package/skills/opensoma/references/output-format.md +130 -0
- package/src/cli.ts +57 -0
- package/src/client.test.ts +210 -0
- package/src/client.ts +264 -0
- package/src/commands/auth.ts +153 -0
- package/src/commands/dashboard.ts +39 -0
- package/src/commands/event.ts +74 -0
- package/src/commands/helpers.ts +12 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/member.ts +29 -0
- package/src/commands/mentoring.ts +209 -0
- package/src/commands/notice.ts +56 -0
- package/src/commands/room.ts +102 -0
- package/src/commands/team.ts +26 -0
- package/src/constants.ts +70 -0
- package/src/credential-manager.test.ts +66 -0
- package/src/credential-manager.ts +52 -0
- package/src/formatters.test.ts +382 -0
- package/src/formatters.ts +489 -0
- package/src/http.test.ts +152 -0
- package/src/http.ts +196 -0
- package/src/index.ts +6 -0
- package/src/shared/utils/error-handler.ts +7 -0
- package/src/shared/utils/mentoring-params.test.ts +112 -0
- package/src/shared/utils/mentoring-params.ts +57 -0
- package/src/shared/utils/output.ts +3 -0
- package/src/shared/utils/stderr.ts +23 -0
- package/src/shared/utils/swmaestro.ts +218 -0
- package/src/token-extractor.test.ts +119 -0
- package/src/token-extractor.ts +205 -0
- package/src/types.test.ts +172 -0
- package/src/types.ts +134 -0
- package/tsconfig.json +38 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createCipheriv, pbkdf2Sync } from 'node:crypto'
|
|
3
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { mkdtemp } from 'node:fs/promises'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
import { Database } from 'bun:sqlite'
|
|
8
|
+
|
|
9
|
+
import { BROWSERS, TokenExtractor } from './token-extractor'
|
|
10
|
+
|
|
11
|
+
const CHROMIUM_IV = Buffer.alloc(16, 0x20)
|
|
12
|
+
const CHROMIUM_SALT = 'saltysalt'
|
|
13
|
+
|
|
14
|
+
let createdDirs: string[] = []
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
for (const dir of createdDirs) {
|
|
18
|
+
rmSync(dir, { recursive: true, force: true })
|
|
19
|
+
}
|
|
20
|
+
createdDirs = []
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('TokenExtractor', () => {
|
|
24
|
+
test('finds cookie databases for all browsers on macOS', async () => {
|
|
25
|
+
const home = await makeTempDir()
|
|
26
|
+
|
|
27
|
+
for (const browser of BROWSERS) {
|
|
28
|
+
createCookieFile(join(home, 'Library', 'Application Support', browser.macPath, 'Default', 'Cookies'))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const extractor = new TokenExtractor('darwin', home)
|
|
32
|
+
const paths = extractor.findCookieDatabases()
|
|
33
|
+
|
|
34
|
+
expect(paths).toHaveLength(BROWSERS.length)
|
|
35
|
+
for (const browser of BROWSERS) {
|
|
36
|
+
expect(paths).toContainEqual(
|
|
37
|
+
join(home, 'Library', 'Application Support', browser.macPath, 'Default', 'Cookies'),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('finds cookie databases for all browsers on Linux', async () => {
|
|
43
|
+
const home = await makeTempDir()
|
|
44
|
+
|
|
45
|
+
for (const browser of BROWSERS) {
|
|
46
|
+
createCookieFile(join(home, '.config', browser.linuxPath, 'Default', 'Cookies'))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const extractor = new TokenExtractor('linux', home)
|
|
50
|
+
const paths = extractor.findCookieDatabases()
|
|
51
|
+
|
|
52
|
+
expect(paths).toHaveLength(BROWSERS.length)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('returns null when no cookie databases exist', async () => {
|
|
56
|
+
const extractor = new TokenExtractor('linux', await makeTempDir())
|
|
57
|
+
expect(await extractor.extract()).toBeNull()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('extracts plaintext cookie value', async () => {
|
|
61
|
+
const home = await makeTempDir()
|
|
62
|
+
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
63
|
+
createCookieDbWithPlaintext(dbPath, 'my-session-id')
|
|
64
|
+
|
|
65
|
+
const extractor = new TokenExtractor('linux', home)
|
|
66
|
+
expect(await extractor.extract()).toEqual({ sessionCookie: 'my-session-id' })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('decrypts encrypted cookie on Linux', async () => {
|
|
70
|
+
const home = await makeTempDir()
|
|
71
|
+
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
72
|
+
const encrypted = encryptLinuxCookie('decrypted-session')
|
|
73
|
+
createCookieDbWithEncrypted(dbPath, encrypted)
|
|
74
|
+
|
|
75
|
+
const extractor = new TokenExtractor('linux', home)
|
|
76
|
+
expect(await extractor.extract()).toEqual({ sessionCookie: 'decrypted-session' })
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
async function makeTempDir(): Promise<string> {
|
|
81
|
+
const dir = await mkdtemp(join(tmpdir(), 'opensoma-token-extractor-'))
|
|
82
|
+
createdDirs.push(dir)
|
|
83
|
+
return dir
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createCookieFile(filePath: string): void {
|
|
87
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
88
|
+
writeFileSync(filePath, '')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createCookieDbWithPlaintext(filePath: string, value: string): void {
|
|
92
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
93
|
+
const db = new Database(filePath)
|
|
94
|
+
db.run(
|
|
95
|
+
'CREATE TABLE cookies (host_key TEXT, name TEXT, value TEXT, encrypted_value BLOB, creation_utc INTEGER, expires_utc INTEGER, is_httponly INTEGER, has_expires INTEGER, is_persistent INTEGER, priority INTEGER, samesite INTEGER, source_scheme INTEGER, is_secure INTEGER, path TEXT, last_access_utc INTEGER, last_update_utc INTEGER, source_port INTEGER, source_type INTEGER)',
|
|
96
|
+
)
|
|
97
|
+
db.run("INSERT INTO cookies (host_key, name, value, encrypted_value) VALUES ('swmaestro.ai', 'JSESSIONID', ?, '')", [
|
|
98
|
+
value,
|
|
99
|
+
])
|
|
100
|
+
db.close()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createCookieDbWithEncrypted(filePath: string, encrypted: Buffer): void {
|
|
104
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
105
|
+
const db = new Database(filePath)
|
|
106
|
+
db.run(
|
|
107
|
+
'CREATE TABLE cookies (host_key TEXT, name TEXT, value TEXT, encrypted_value BLOB, creation_utc INTEGER, expires_utc INTEGER, is_httponly INTEGER, has_expires INTEGER, is_persistent INTEGER, priority INTEGER, samesite INTEGER, source_scheme INTEGER, is_secure INTEGER, path TEXT, last_access_utc INTEGER, last_update_utc INTEGER, source_port INTEGER, source_type INTEGER)',
|
|
108
|
+
)
|
|
109
|
+
db.run("INSERT INTO cookies (host_key, name, value, encrypted_value) VALUES ('swmaestro.ai', 'JSESSIONID', '', ?)", [
|
|
110
|
+
encrypted,
|
|
111
|
+
])
|
|
112
|
+
db.close()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function encryptLinuxCookie(value: string): Buffer {
|
|
116
|
+
const key = pbkdf2Sync('peanuts', CHROMIUM_SALT, 1, 16, 'sha1')
|
|
117
|
+
const cipher = createCipheriv('aes-128-cbc', key, CHROMIUM_IV)
|
|
118
|
+
return Buffer.concat([Buffer.from('v10'), cipher.update(value, 'utf8'), cipher.final()])
|
|
119
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { createDecipheriv, pbkdf2Sync } from 'node:crypto'
|
|
3
|
+
import { copyFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs'
|
|
4
|
+
import { homedir, tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const COOKIE_QUERY =
|
|
8
|
+
"SELECT encrypted_value, value FROM cookies WHERE host_key LIKE '%swmaestro.ai' AND name = 'JSESSIONID' ORDER BY last_access_utc DESC LIMIT 1"
|
|
9
|
+
const CHROMIUM_SALT = 'saltysalt'
|
|
10
|
+
const CHROMIUM_IV = Buffer.alloc(16, 0x20)
|
|
11
|
+
|
|
12
|
+
type BrowserConfig = {
|
|
13
|
+
name: string
|
|
14
|
+
macPath: string
|
|
15
|
+
linuxPath: string
|
|
16
|
+
keychainService: string
|
|
17
|
+
keychainAccount: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type CookieRow = {
|
|
21
|
+
encrypted_value: Uint8Array | Buffer | null
|
|
22
|
+
value: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const BROWSERS: BrowserConfig[] = [
|
|
26
|
+
{
|
|
27
|
+
name: 'Chrome',
|
|
28
|
+
macPath: 'Google Chrome',
|
|
29
|
+
linuxPath: 'google-chrome',
|
|
30
|
+
keychainService: 'Chrome Safe Storage',
|
|
31
|
+
keychainAccount: 'Chrome',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'Edge',
|
|
35
|
+
macPath: 'Microsoft Edge',
|
|
36
|
+
linuxPath: 'microsoft-edge',
|
|
37
|
+
keychainService: 'Microsoft Edge Safe Storage',
|
|
38
|
+
keychainAccount: 'Microsoft Edge',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'Brave',
|
|
42
|
+
macPath: 'BraveSoftware/Brave-Browser',
|
|
43
|
+
linuxPath: 'BraveSoftware/Brave-Browser',
|
|
44
|
+
keychainService: 'Brave Safe Storage',
|
|
45
|
+
keychainAccount: 'Brave',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Arc',
|
|
49
|
+
macPath: join('Arc', 'User Data'),
|
|
50
|
+
linuxPath: join('Arc', 'User Data'),
|
|
51
|
+
keychainService: 'Arc Safe Storage',
|
|
52
|
+
keychainAccount: 'Arc',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'Vivaldi',
|
|
56
|
+
macPath: 'Vivaldi',
|
|
57
|
+
linuxPath: 'Vivaldi',
|
|
58
|
+
keychainService: 'Vivaldi Safe Storage',
|
|
59
|
+
keychainAccount: 'Vivaldi',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'Chromium',
|
|
63
|
+
macPath: 'Chromium',
|
|
64
|
+
linuxPath: 'Chromium',
|
|
65
|
+
keychainService: 'Chromium Safe Storage',
|
|
66
|
+
keychainAccount: 'Chromium',
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
function queryCookieDb(dbPath: string): CookieRow | undefined {
|
|
71
|
+
if (typeof globalThis.Bun !== 'undefined') {
|
|
72
|
+
const { Database } = require('bun:sqlite')
|
|
73
|
+
const db = new Database(dbPath, { readonly: true })
|
|
74
|
+
try {
|
|
75
|
+
const row = db.query(COOKIE_QUERY).get() as CookieRow | undefined
|
|
76
|
+
return row ?? undefined
|
|
77
|
+
} finally {
|
|
78
|
+
db.close()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const Database = require('better-sqlite3')
|
|
83
|
+
const db = new Database(dbPath, { readonly: true })
|
|
84
|
+
try {
|
|
85
|
+
return db.prepare(COOKIE_QUERY).get() as CookieRow | undefined
|
|
86
|
+
} finally {
|
|
87
|
+
db.close()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class TokenExtractor {
|
|
92
|
+
constructor(
|
|
93
|
+
private readonly platform: NodeJS.Platform = process.platform,
|
|
94
|
+
private readonly homeDirectory: string = homedir(),
|
|
95
|
+
) {}
|
|
96
|
+
|
|
97
|
+
async extract(): Promise<{ sessionCookie: string } | null> {
|
|
98
|
+
for (const databasePath of this.findCookieDatabases()) {
|
|
99
|
+
const browser = this.getBrowserByPath(databasePath)
|
|
100
|
+
if (!browser) {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const tempDirectory = mkdtempSync(join(tmpdir(), 'opensoma-cookie-db-'))
|
|
105
|
+
const tempDatabasePath = join(tempDirectory, 'Cookies')
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
copyFileSync(databasePath, tempDatabasePath)
|
|
109
|
+
|
|
110
|
+
const row = queryCookieDb(tempDatabasePath)
|
|
111
|
+
if (!row) {
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const plaintextValue = typeof row.value === 'string' ? row.value.trim() : ''
|
|
116
|
+
if (plaintextValue) {
|
|
117
|
+
return { sessionCookie: plaintextValue }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!row.encrypted_value || row.encrypted_value.length === 0) {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const decryptedValue = await this.decryptCookie(Buffer.from(row.encrypted_value), browser.name)
|
|
125
|
+
if (decryptedValue) {
|
|
126
|
+
return { sessionCookie: decryptedValue }
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
continue
|
|
130
|
+
} finally {
|
|
131
|
+
rmSync(tempDirectory, { recursive: true, force: true })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
findCookieDatabases(): string[] {
|
|
139
|
+
const browserPaths =
|
|
140
|
+
this.platform === 'darwin'
|
|
141
|
+
? BROWSERS.map((browser) =>
|
|
142
|
+
join(this.homeDirectory, 'Library', 'Application Support', browser.macPath, 'Default', 'Cookies'),
|
|
143
|
+
)
|
|
144
|
+
: this.platform === 'linux'
|
|
145
|
+
? BROWSERS.map((browser) => join(this.homeDirectory, '.config', browser.linuxPath, 'Default', 'Cookies'))
|
|
146
|
+
: []
|
|
147
|
+
|
|
148
|
+
return browserPaths.filter((databasePath) => existsSync(databasePath))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async decryptCookie(encryptedValue: Buffer, browserName: string): Promise<string> {
|
|
152
|
+
if (encryptedValue.length === 0) {
|
|
153
|
+
return ''
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.platform === 'linux') {
|
|
157
|
+
return this.decryptChromiumValue(encryptedValue, pbkdf2Sync('peanuts', CHROMIUM_SALT, 1, 16, 'sha1'))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (this.platform === 'darwin') {
|
|
161
|
+
const key = await this.getMacOSEncryptionKey(browserName)
|
|
162
|
+
return this.decryptChromiumValue(encryptedValue, key)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return ''
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async getMacOSEncryptionKey(browserName: string): Promise<Buffer> {
|
|
169
|
+
const browser = BROWSERS.find((entry) => entry.name === browserName)
|
|
170
|
+
if (!browser) {
|
|
171
|
+
throw new Error(`Unsupported browser: ${browserName}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const password = execSync(
|
|
175
|
+
`security find-generic-password -s ${JSON.stringify(browser.keychainService)} -a ${JSON.stringify(browser.keychainAccount)} -w`,
|
|
176
|
+
{ encoding: 'utf8' },
|
|
177
|
+
).trimEnd()
|
|
178
|
+
|
|
179
|
+
return pbkdf2Sync(password, CHROMIUM_SALT, 1003, 16, 'sha1')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private decryptChromiumValue(encryptedValue: Buffer, key: Buffer): string {
|
|
183
|
+
const encryptedPayload =
|
|
184
|
+
encryptedValue.subarray(0, 3).toString('utf8') === 'v10' ? encryptedValue.subarray(3) : encryptedValue
|
|
185
|
+
const decipher = createDecipheriv('aes-128-cbc', key, CHROMIUM_IV)
|
|
186
|
+
decipher.setAutoPadding(true)
|
|
187
|
+
const decrypted = Buffer.concat([decipher.update(encryptedPayload), decipher.final()])
|
|
188
|
+
|
|
189
|
+
// Chromium v130+ prepends a 32-byte integrity hash before the actual cookie value
|
|
190
|
+
if (decrypted.length > 32) {
|
|
191
|
+
const hasNonPrintablePrefix = decrypted.subarray(0, 32).some((b) => b < 0x20 || b > 0x7e)
|
|
192
|
+
if (hasNonPrintablePrefix) {
|
|
193
|
+
return decrypted.subarray(32).toString('utf8')
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return decrypted.toString('utf8')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private getBrowserByPath(databasePath: string): BrowserConfig | undefined {
|
|
201
|
+
return BROWSERS.find(
|
|
202
|
+
(browser) => databasePath.includes(`${browser.macPath}/`) || databasePath.includes(`${browser.linuxPath}/`),
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ApplicationHistoryItemSchema,
|
|
5
|
+
CredentialsSchema,
|
|
6
|
+
DashboardSchema,
|
|
7
|
+
EventListItemSchema,
|
|
8
|
+
MemberInfoSchema,
|
|
9
|
+
MentoringDetailSchema,
|
|
10
|
+
MentoringListItemSchema,
|
|
11
|
+
NoticeDetailSchema,
|
|
12
|
+
NoticeListItemSchema,
|
|
13
|
+
PaginationSchema,
|
|
14
|
+
RoomCardSchema,
|
|
15
|
+
TeamInfoSchema,
|
|
16
|
+
} from '@/types'
|
|
17
|
+
|
|
18
|
+
describe('schemas', () => {
|
|
19
|
+
test('accept valid values', () => {
|
|
20
|
+
expect(
|
|
21
|
+
MentoringListItemSchema.parse({
|
|
22
|
+
id: 9572,
|
|
23
|
+
title: 'AI 멘토링',
|
|
24
|
+
type: '자유 멘토링',
|
|
25
|
+
registrationPeriod: { start: '2026-04-01', end: '2026-04-10' },
|
|
26
|
+
sessionDate: '2026-04-11',
|
|
27
|
+
sessionTime: { start: '14:00', end: '15:30' },
|
|
28
|
+
attendees: { current: 3, max: 4 },
|
|
29
|
+
approved: true,
|
|
30
|
+
status: '접수중',
|
|
31
|
+
author: '전수열',
|
|
32
|
+
createdAt: '2026-04-01',
|
|
33
|
+
}),
|
|
34
|
+
).toBeDefined()
|
|
35
|
+
|
|
36
|
+
expect(
|
|
37
|
+
MentoringDetailSchema.parse({
|
|
38
|
+
id: 9572,
|
|
39
|
+
title: 'AI 멘토링',
|
|
40
|
+
type: '멘토 특강',
|
|
41
|
+
registrationPeriod: { start: '2026-04-01', end: '2026-04-10' },
|
|
42
|
+
sessionDate: '2026-04-11',
|
|
43
|
+
sessionTime: { start: '14:00', end: '15:30' },
|
|
44
|
+
attendees: { current: 6, max: 20 },
|
|
45
|
+
approved: false,
|
|
46
|
+
status: '마감',
|
|
47
|
+
author: '전수열',
|
|
48
|
+
createdAt: '2026-04-01',
|
|
49
|
+
content: '<p>내용</p>',
|
|
50
|
+
venue: '온라인(Webex)',
|
|
51
|
+
}),
|
|
52
|
+
).toBeDefined()
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
RoomCardSchema.parse({
|
|
56
|
+
itemId: 17,
|
|
57
|
+
name: '스페이스 A1',
|
|
58
|
+
capacity: 4,
|
|
59
|
+
availablePeriod: { start: '2026-04-01', end: '2026-12-31' },
|
|
60
|
+
description: '소회의실 : 4인',
|
|
61
|
+
timeSlots: [{ time: '09:00', available: true }],
|
|
62
|
+
}),
|
|
63
|
+
).toBeDefined()
|
|
64
|
+
|
|
65
|
+
expect(
|
|
66
|
+
DashboardSchema.parse({
|
|
67
|
+
name: '전수열',
|
|
68
|
+
role: '멘토',
|
|
69
|
+
organization: 'Indent',
|
|
70
|
+
position: '',
|
|
71
|
+
team: { name: '오픈소마', members: '전수열, 김개발', mentor: '전수열' },
|
|
72
|
+
mentoringSessions: [{ title: 'AI 멘토링', url: '/mentoring/1', status: '접수중' }],
|
|
73
|
+
roomReservations: [{ title: 'A1 회의', url: '/room/1', status: '예약완료' }],
|
|
74
|
+
}),
|
|
75
|
+
).toBeDefined()
|
|
76
|
+
|
|
77
|
+
expect(
|
|
78
|
+
NoticeListItemSchema.parse({
|
|
79
|
+
id: 1,
|
|
80
|
+
title: '공지',
|
|
81
|
+
author: '관리자',
|
|
82
|
+
createdAt: '2026-04-01',
|
|
83
|
+
}),
|
|
84
|
+
).toBeDefined()
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
NoticeDetailSchema.parse({
|
|
88
|
+
id: 1,
|
|
89
|
+
title: '공지',
|
|
90
|
+
author: '관리자',
|
|
91
|
+
createdAt: '2026-04-01',
|
|
92
|
+
content: '<p>내용</p>',
|
|
93
|
+
}),
|
|
94
|
+
).toBeDefined()
|
|
95
|
+
|
|
96
|
+
expect(
|
|
97
|
+
TeamInfoSchema.parse({
|
|
98
|
+
teams: [
|
|
99
|
+
{ name: '오픈소마', memberCount: 3, joinStatus: '참여중' },
|
|
100
|
+
{ name: '김앤강', memberCount: 5, joinStatus: '참여하기' },
|
|
101
|
+
],
|
|
102
|
+
currentTeams: 1,
|
|
103
|
+
maxTeams: 100,
|
|
104
|
+
}),
|
|
105
|
+
).toBeDefined()
|
|
106
|
+
|
|
107
|
+
expect(
|
|
108
|
+
MemberInfoSchema.parse({
|
|
109
|
+
email: 'neo@example.com',
|
|
110
|
+
name: '전수열',
|
|
111
|
+
gender: '남자',
|
|
112
|
+
birthDate: '1995-01-14',
|
|
113
|
+
phone: '01012345678',
|
|
114
|
+
organization: 'Indent',
|
|
115
|
+
position: '',
|
|
116
|
+
}),
|
|
117
|
+
).toBeDefined()
|
|
118
|
+
|
|
119
|
+
expect(
|
|
120
|
+
EventListItemSchema.parse({
|
|
121
|
+
id: 11,
|
|
122
|
+
category: '행사',
|
|
123
|
+
title: '데모데이',
|
|
124
|
+
registrationPeriod: { start: '2026-04-01', end: '2026-04-05' },
|
|
125
|
+
eventPeriod: { start: '2026-04-10', end: '2026-04-10' },
|
|
126
|
+
status: '접수중',
|
|
127
|
+
createdAt: '2026-03-30',
|
|
128
|
+
}),
|
|
129
|
+
).toBeDefined()
|
|
130
|
+
|
|
131
|
+
expect(
|
|
132
|
+
ApplicationHistoryItemSchema.parse({
|
|
133
|
+
id: 99,
|
|
134
|
+
category: '멘토 특강',
|
|
135
|
+
title: 'AI 멘토링',
|
|
136
|
+
author: '전수열',
|
|
137
|
+
sessionDate: '2026-04-11',
|
|
138
|
+
appliedAt: '2026-04-02 09:00',
|
|
139
|
+
applicationStatus: '신청완료',
|
|
140
|
+
approvalStatus: 'OK',
|
|
141
|
+
applicationDetail: '승인대기',
|
|
142
|
+
note: '-',
|
|
143
|
+
}),
|
|
144
|
+
).toBeDefined()
|
|
145
|
+
|
|
146
|
+
expect(PaginationSchema.parse({ total: 23, currentPage: 2, totalPages: 3 })).toBeDefined()
|
|
147
|
+
|
|
148
|
+
expect(
|
|
149
|
+
CredentialsSchema.parse({
|
|
150
|
+
sessionCookie: 'session-token',
|
|
151
|
+
csrfToken: 'csrf-token',
|
|
152
|
+
username: 'neo@example.com',
|
|
153
|
+
loggedInAt: '2026-04-09T00:00:00.000Z',
|
|
154
|
+
}),
|
|
155
|
+
).toBeDefined()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('reject invalid values', () => {
|
|
159
|
+
expect(() => MentoringListItemSchema.parse({})).toThrow()
|
|
160
|
+
expect(() => MentoringDetailSchema.parse({ content: 123 })).toThrow()
|
|
161
|
+
expect(() => RoomCardSchema.parse({ itemId: '17' })).toThrow()
|
|
162
|
+
expect(() => DashboardSchema.parse({ name: '전수열' })).toThrow()
|
|
163
|
+
expect(() => NoticeListItemSchema.parse({ id: 1, title: '공지' })).toThrow()
|
|
164
|
+
expect(() => NoticeDetailSchema.parse({ id: 1, title: '공지', author: '관리자', createdAt: '2026-04-01' })).toThrow()
|
|
165
|
+
expect(() => TeamInfoSchema.parse({ teams: [{ name: '김개발' }], currentTeams: 1, maxTeams: 100 })).toThrow()
|
|
166
|
+
expect(() => MemberInfoSchema.parse({ email: 1, name: '전수열' })).toThrow()
|
|
167
|
+
expect(() => EventListItemSchema.parse({ id: 1, title: '행사', status: '접수중' })).toThrow()
|
|
168
|
+
expect(() => ApplicationHistoryItemSchema.parse({ id: 1, status: '신청완료' })).toThrow()
|
|
169
|
+
expect(() => PaginationSchema.parse({ total: '23', currentPage: 2, totalPages: 3 })).toThrow()
|
|
170
|
+
expect(() => CredentialsSchema.parse({ sessionCookie: 'cookie' })).toThrow()
|
|
171
|
+
})
|
|
172
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from 'zod/v4'
|
|
2
|
+
|
|
3
|
+
const DateRangeSchema = z.object({ start: z.string(), end: z.string() })
|
|
4
|
+
const TimeRangeSchema = z.object({ start: z.string(), end: z.string() })
|
|
5
|
+
const RoomTimeSlotSchema = z.object({ time: z.string(), available: z.boolean() })
|
|
6
|
+
const DashboardStatusItemSchema = z.object({
|
|
7
|
+
title: z.string(),
|
|
8
|
+
url: z.string(),
|
|
9
|
+
status: z.string(),
|
|
10
|
+
})
|
|
11
|
+
const TeamListItemSchema = z.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
memberCount: z.number(),
|
|
14
|
+
joinStatus: z.string(),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const MentoringListItemSchema = z.object({
|
|
18
|
+
id: z.number(),
|
|
19
|
+
title: z.string(),
|
|
20
|
+
type: z.enum(['자유 멘토링', '멘토 특강']),
|
|
21
|
+
registrationPeriod: DateRangeSchema,
|
|
22
|
+
sessionDate: z.string(),
|
|
23
|
+
sessionTime: TimeRangeSchema,
|
|
24
|
+
attendees: z.object({ current: z.number(), max: z.number() }),
|
|
25
|
+
approved: z.boolean(),
|
|
26
|
+
status: z.enum(['접수중', '마감']),
|
|
27
|
+
author: z.string(),
|
|
28
|
+
createdAt: z.string(),
|
|
29
|
+
})
|
|
30
|
+
export type MentoringListItem = z.infer<typeof MentoringListItemSchema>
|
|
31
|
+
|
|
32
|
+
export const MentoringDetailSchema = MentoringListItemSchema.extend({
|
|
33
|
+
content: z.string(),
|
|
34
|
+
venue: z.string(),
|
|
35
|
+
})
|
|
36
|
+
export type MentoringDetail = z.infer<typeof MentoringDetailSchema>
|
|
37
|
+
|
|
38
|
+
export const RoomCardSchema = z.object({
|
|
39
|
+
itemId: z.number(),
|
|
40
|
+
name: z.string(),
|
|
41
|
+
capacity: z.number(),
|
|
42
|
+
availablePeriod: DateRangeSchema,
|
|
43
|
+
description: z.string(),
|
|
44
|
+
timeSlots: z.array(RoomTimeSlotSchema),
|
|
45
|
+
})
|
|
46
|
+
export type RoomCard = z.infer<typeof RoomCardSchema>
|
|
47
|
+
|
|
48
|
+
export const DashboardSchema = z.object({
|
|
49
|
+
name: z.string(),
|
|
50
|
+
role: z.string(),
|
|
51
|
+
organization: z.string(),
|
|
52
|
+
position: z.string(),
|
|
53
|
+
team: z
|
|
54
|
+
.object({
|
|
55
|
+
name: z.string(),
|
|
56
|
+
members: z.string(),
|
|
57
|
+
mentor: z.string(),
|
|
58
|
+
})
|
|
59
|
+
.optional(),
|
|
60
|
+
mentoringSessions: z.array(DashboardStatusItemSchema),
|
|
61
|
+
roomReservations: z.array(DashboardStatusItemSchema),
|
|
62
|
+
})
|
|
63
|
+
export type Dashboard = z.infer<typeof DashboardSchema>
|
|
64
|
+
|
|
65
|
+
export const NoticeListItemSchema = z.object({
|
|
66
|
+
id: z.number(),
|
|
67
|
+
title: z.string(),
|
|
68
|
+
author: z.string(),
|
|
69
|
+
createdAt: z.string(),
|
|
70
|
+
})
|
|
71
|
+
export type NoticeListItem = z.infer<typeof NoticeListItemSchema>
|
|
72
|
+
|
|
73
|
+
export const NoticeDetailSchema = NoticeListItemSchema.extend({
|
|
74
|
+
content: z.string(),
|
|
75
|
+
})
|
|
76
|
+
export type NoticeDetail = z.infer<typeof NoticeDetailSchema>
|
|
77
|
+
|
|
78
|
+
export const TeamInfoSchema = z.object({
|
|
79
|
+
teams: z.array(TeamListItemSchema),
|
|
80
|
+
currentTeams: z.number(),
|
|
81
|
+
maxTeams: z.number(),
|
|
82
|
+
})
|
|
83
|
+
export type TeamInfo = z.infer<typeof TeamInfoSchema>
|
|
84
|
+
|
|
85
|
+
export const MemberInfoSchema = z.object({
|
|
86
|
+
email: z.string(),
|
|
87
|
+
name: z.string(),
|
|
88
|
+
gender: z.string(),
|
|
89
|
+
birthDate: z.string(),
|
|
90
|
+
phone: z.string(),
|
|
91
|
+
organization: z.string(),
|
|
92
|
+
position: z.string(),
|
|
93
|
+
})
|
|
94
|
+
export type MemberInfo = z.infer<typeof MemberInfoSchema>
|
|
95
|
+
|
|
96
|
+
export const EventListItemSchema = z.object({
|
|
97
|
+
id: z.number(),
|
|
98
|
+
category: z.string(),
|
|
99
|
+
title: z.string(),
|
|
100
|
+
registrationPeriod: DateRangeSchema,
|
|
101
|
+
eventPeriod: DateRangeSchema,
|
|
102
|
+
status: z.string(),
|
|
103
|
+
createdAt: z.string(),
|
|
104
|
+
})
|
|
105
|
+
export type EventListItem = z.infer<typeof EventListItemSchema>
|
|
106
|
+
|
|
107
|
+
export const ApplicationHistoryItemSchema = z.object({
|
|
108
|
+
id: z.number(),
|
|
109
|
+
category: z.string(),
|
|
110
|
+
title: z.string(),
|
|
111
|
+
author: z.string(),
|
|
112
|
+
sessionDate: z.string(),
|
|
113
|
+
appliedAt: z.string(),
|
|
114
|
+
applicationStatus: z.string(),
|
|
115
|
+
approvalStatus: z.string(),
|
|
116
|
+
applicationDetail: z.string(),
|
|
117
|
+
note: z.string(),
|
|
118
|
+
})
|
|
119
|
+
export type ApplicationHistoryItem = z.infer<typeof ApplicationHistoryItemSchema>
|
|
120
|
+
|
|
121
|
+
export const PaginationSchema = z.object({
|
|
122
|
+
total: z.number(),
|
|
123
|
+
currentPage: z.number(),
|
|
124
|
+
totalPages: z.number(),
|
|
125
|
+
})
|
|
126
|
+
export type Pagination = z.infer<typeof PaginationSchema>
|
|
127
|
+
|
|
128
|
+
export const CredentialsSchema = z.object({
|
|
129
|
+
sessionCookie: z.string(),
|
|
130
|
+
csrfToken: z.string(),
|
|
131
|
+
username: z.string().optional(),
|
|
132
|
+
loggedInAt: z.string().optional(),
|
|
133
|
+
})
|
|
134
|
+
export type Credentials = z.infer<typeof CredentialsSchema>
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2022", "DOM"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"baseUrl": ".",
|
|
10
|
+
"types": ["node", "bun"],
|
|
11
|
+
"paths": {
|
|
12
|
+
"@/*": ["src/*"]
|
|
13
|
+
},
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"strict": true,
|
|
18
|
+
"esModuleInterop": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"forceConsistentCasingInFileNames": true,
|
|
21
|
+
"resolveJsonModule": true,
|
|
22
|
+
"allowJs": false,
|
|
23
|
+
"noImplicitAny": true,
|
|
24
|
+
"strictNullChecks": true,
|
|
25
|
+
"strictFunctionTypes": true,
|
|
26
|
+
"strictBindCallApply": true,
|
|
27
|
+
"strictPropertyInitialization": true,
|
|
28
|
+
"ignoreDeprecations": "6.0",
|
|
29
|
+
"noImplicitThis": true,
|
|
30
|
+
"alwaysStrict": true,
|
|
31
|
+
"noUnusedLocals": false,
|
|
32
|
+
"noUnusedParameters": false,
|
|
33
|
+
"noImplicitReturns": true,
|
|
34
|
+
"noFallthroughCasesInSwitch": true
|
|
35
|
+
},
|
|
36
|
+
"include": ["src/**/*"],
|
|
37
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
38
|
+
}
|