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.
- package/dist/package.json +1 -3
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +6 -6
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/auth.d.ts +1 -11
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +4 -81
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/helpers.d.ts +1 -5
- package/dist/src/commands/helpers.d.ts.map +1 -1
- package/dist/src/commands/helpers.js +4 -32
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/team.js +1 -1
- package/dist/src/commands/team.js.map +1 -1
- package/dist/src/credential-manager.js +2 -2
- package/dist/src/credential-manager.js.map +1 -1
- package/dist/src/errors.d.ts +0 -4
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +1 -5
- package/dist/src/errors.js.map +1 -1
- package/dist/src/shared/utils/config-dir.d.ts +11 -0
- package/dist/src/shared/utils/config-dir.d.ts.map +1 -0
- package/dist/src/shared/utils/config-dir.js +19 -0
- package/dist/src/shared/utils/config-dir.js.map +1 -0
- package/dist/src/toz-pending-store.d.ts.map +1 -1
- package/dist/src/toz-pending-store.js +2 -2
- package/dist/src/toz-pending-store.js.map +1 -1
- package/package.json +1 -3
- package/src/client.test.ts +55 -4
- package/src/client.ts +7 -8
- package/src/commands/auth.test.ts +6 -98
- package/src/commands/auth.ts +2 -115
- package/src/commands/helpers.test.ts +5 -116
- package/src/commands/helpers.ts +3 -35
- package/src/commands/team.ts +1 -1
- package/src/credential-manager.ts +2 -2
- package/src/errors.ts +1 -5
- package/src/shared/utils/config-dir.test.ts +41 -0
- package/src/shared/utils/config-dir.ts +20 -0
- package/src/toz-pending-store.ts +3 -2
- package/dist/src/token-extractor.d.ts +0 -43
- package/dist/src/token-extractor.d.ts.map +0 -1
- package/dist/src/token-extractor.js +0 -302
- package/dist/src/token-extractor.js.map +0 -1
- package/src/token-extractor.test.ts +0 -220
- package/src/token-extractor.ts +0 -392
package/src/token-extractor.ts
DELETED
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
2
|
-
import { createDecipheriv, pbkdf2Sync } from 'node:crypto'
|
|
3
|
-
import { copyFileSync, existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs'
|
|
4
|
-
import { createRequire } from 'node:module'
|
|
5
|
-
import { homedir, tmpdir } from 'node:os'
|
|
6
|
-
import { basename, dirname, join } from 'node:path'
|
|
7
|
-
|
|
8
|
-
const require = createRequire(import.meta.url)
|
|
9
|
-
|
|
10
|
-
const COOKIE_QUERY =
|
|
11
|
-
"SELECT encrypted_value, last_access_utc, value FROM cookies WHERE (host_key LIKE '%swmaestro.ai' OR host_key LIKE '%opensoma.dev') AND name = 'JSESSIONID' ORDER BY last_access_utc DESC LIMIT 1"
|
|
12
|
-
const CHROMIUM_SALT = 'saltysalt'
|
|
13
|
-
const CHROMIUM_IV = Buffer.alloc(16, 0x20)
|
|
14
|
-
const PROFILE_DIR_PATTERN = /^Profile\s+\d+$/
|
|
15
|
-
|
|
16
|
-
type BrowserConfig = {
|
|
17
|
-
name: string
|
|
18
|
-
macPath: string
|
|
19
|
-
linuxPath: string
|
|
20
|
-
keychainService: string
|
|
21
|
-
keychainAccount: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type CookieRow = {
|
|
25
|
-
encrypted_value: ArrayBuffer | Uint8Array | Buffer | null
|
|
26
|
-
last_access_utc?: number | bigint | null
|
|
27
|
-
value: ArrayBuffer | Uint8Array | Buffer | string | null
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ExtractedSessionCandidate {
|
|
31
|
-
browser: string
|
|
32
|
-
lastAccessUtc: number
|
|
33
|
-
profile: string
|
|
34
|
-
sessionCookie: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const BROWSERS: BrowserConfig[] = [
|
|
38
|
-
{
|
|
39
|
-
name: 'Chrome',
|
|
40
|
-
macPath: 'Google Chrome',
|
|
41
|
-
linuxPath: 'google-chrome',
|
|
42
|
-
keychainService: 'Chrome Safe Storage',
|
|
43
|
-
keychainAccount: 'Chrome',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: 'Edge',
|
|
47
|
-
macPath: 'Microsoft Edge',
|
|
48
|
-
linuxPath: 'microsoft-edge',
|
|
49
|
-
keychainService: 'Microsoft Edge Safe Storage',
|
|
50
|
-
keychainAccount: 'Microsoft Edge',
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: 'Brave',
|
|
54
|
-
macPath: 'BraveSoftware/Brave-Browser',
|
|
55
|
-
linuxPath: 'BraveSoftware/Brave-Browser',
|
|
56
|
-
keychainService: 'Brave Safe Storage',
|
|
57
|
-
keychainAccount: 'Brave',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
name: 'Arc',
|
|
61
|
-
macPath: join('Arc', 'User Data'),
|
|
62
|
-
linuxPath: join('Arc', 'User Data'),
|
|
63
|
-
keychainService: 'Arc Safe Storage',
|
|
64
|
-
keychainAccount: 'Arc',
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: 'Vivaldi',
|
|
68
|
-
macPath: 'Vivaldi',
|
|
69
|
-
linuxPath: 'Vivaldi',
|
|
70
|
-
keychainService: 'Vivaldi Safe Storage',
|
|
71
|
-
keychainAccount: 'Vivaldi',
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
name: 'Chromium',
|
|
75
|
-
macPath: 'Chromium',
|
|
76
|
-
linuxPath: 'Chromium',
|
|
77
|
-
keychainService: 'Chromium Safe Storage',
|
|
78
|
-
keychainAccount: 'Chromium',
|
|
79
|
-
},
|
|
80
|
-
]
|
|
81
|
-
|
|
82
|
-
function queryCookieDb(dbPath: string): CookieRow | undefined {
|
|
83
|
-
if (typeof globalThis.Bun !== 'undefined') {
|
|
84
|
-
const { Database } = require('bun:sqlite')
|
|
85
|
-
const db = new Database(dbPath, { readonly: true })
|
|
86
|
-
try {
|
|
87
|
-
const row = db.query(COOKIE_QUERY).get() as CookieRow | undefined
|
|
88
|
-
return row ?? undefined
|
|
89
|
-
} finally {
|
|
90
|
-
db.close()
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const Database = require('better-sqlite3') as new (
|
|
96
|
-
path: string,
|
|
97
|
-
options?: { readonly?: boolean },
|
|
98
|
-
) => {
|
|
99
|
-
close: () => void
|
|
100
|
-
prepare: (query: string) => { get: () => CookieRow | undefined }
|
|
101
|
-
}
|
|
102
|
-
const db = new Database(dbPath, { readonly: true })
|
|
103
|
-
try {
|
|
104
|
-
return db.prepare(COOKIE_QUERY).get() ?? undefined
|
|
105
|
-
} finally {
|
|
106
|
-
db.close()
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
const { DatabaseSync } = require('node:sqlite') as {
|
|
110
|
-
DatabaseSync: new (
|
|
111
|
-
path: string,
|
|
112
|
-
options?: { readonly?: boolean },
|
|
113
|
-
) => {
|
|
114
|
-
close: () => void
|
|
115
|
-
prepare: (query: string) => { get: () => CookieRow | undefined }
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const db = new DatabaseSync(dbPath, { readonly: true })
|
|
119
|
-
try {
|
|
120
|
-
return db.prepare(COOKIE_QUERY).get() ?? undefined
|
|
121
|
-
} finally {
|
|
122
|
-
db.close()
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export type TokenExtractorOptions = {
|
|
128
|
-
platform?: NodeJS.Platform
|
|
129
|
-
homeDirectory?: string
|
|
130
|
-
debug?: boolean
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export class TokenExtractor {
|
|
134
|
-
private readonly platform: NodeJS.Platform
|
|
135
|
-
private readonly homeDirectory: string
|
|
136
|
-
private readonly debugEnabled: boolean
|
|
137
|
-
|
|
138
|
-
constructor(options?: TokenExtractorOptions)
|
|
139
|
-
constructor(platform?: NodeJS.Platform, homeDirectory?: string, debug?: boolean)
|
|
140
|
-
constructor(platformOrOptions?: NodeJS.Platform | TokenExtractorOptions, homeDirectory?: string, debug?: boolean) {
|
|
141
|
-
if (typeof platformOrOptions === 'object' && platformOrOptions !== null) {
|
|
142
|
-
this.platform = platformOrOptions.platform ?? process.platform
|
|
143
|
-
this.homeDirectory = platformOrOptions.homeDirectory ?? homedir()
|
|
144
|
-
this.debugEnabled = platformOrOptions.debug ?? false
|
|
145
|
-
} else {
|
|
146
|
-
this.platform = platformOrOptions ?? process.platform
|
|
147
|
-
this.homeDirectory = homeDirectory ?? homedir()
|
|
148
|
-
this.debugEnabled = debug ?? false
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
private log(...args: unknown[]): void {
|
|
153
|
-
if (!this.debugEnabled) return
|
|
154
|
-
const message = args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')
|
|
155
|
-
process.stderr.write(`[extract] ${message}\n`)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async extract(): Promise<{ sessionCookie: string } | null> {
|
|
159
|
-
const candidates = await this.extractCandidates()
|
|
160
|
-
const firstCandidate = candidates[0]
|
|
161
|
-
|
|
162
|
-
if (!firstCandidate) {
|
|
163
|
-
return null
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return { sessionCookie: firstCandidate.sessionCookie }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async extractCandidates(): Promise<ExtractedSessionCandidate[]> {
|
|
170
|
-
const candidates = new Map<string, ExtractedSessionCandidate>()
|
|
171
|
-
const cookieDatabases = this.findCookieDatabases()
|
|
172
|
-
this.log(`Found ${cookieDatabases.length} cookie database(s)`)
|
|
173
|
-
|
|
174
|
-
for (const databasePath of cookieDatabases) {
|
|
175
|
-
const browser = this.getBrowserByPath(databasePath)
|
|
176
|
-
if (!browser) {
|
|
177
|
-
this.log(`Skipping ${databasePath} (no matching browser config)`)
|
|
178
|
-
continue
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const profile = basename(dirname(databasePath))
|
|
182
|
-
this.log(`Processing ${browser.name} / ${profile}`)
|
|
183
|
-
|
|
184
|
-
const tempDirectory = mkdtempSync(join(tmpdir(), 'opensoma-cookie-db-'))
|
|
185
|
-
const tempDatabasePath = join(tempDirectory, 'Cookies')
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
copySqliteDatabase(databasePath, tempDatabasePath)
|
|
189
|
-
this.log(` Copied DB to ${tempDatabasePath}`)
|
|
190
|
-
|
|
191
|
-
const row = queryCookieDb(tempDatabasePath)
|
|
192
|
-
if (!row) {
|
|
193
|
-
this.log(' No JSESSIONID cookie found')
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const plaintextValue = normalizeCookieText(row.value)
|
|
198
|
-
if (plaintextValue) {
|
|
199
|
-
this.log(` Found plaintext cookie (${plaintextValue.length} chars)`)
|
|
200
|
-
this.addCandidate(candidates, {
|
|
201
|
-
browser: browser.name,
|
|
202
|
-
lastAccessUtc: this.normalizeLastAccessUtc(row.last_access_utc),
|
|
203
|
-
profile,
|
|
204
|
-
sessionCookie: plaintextValue,
|
|
205
|
-
})
|
|
206
|
-
continue
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const encryptedValue = normalizeCookieBytes(row.encrypted_value)
|
|
210
|
-
if (!encryptedValue || encryptedValue.length === 0) {
|
|
211
|
-
this.log(' No plaintext or encrypted value')
|
|
212
|
-
continue
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
this.log(` Decrypting encrypted cookie (${encryptedValue.length} bytes)...`)
|
|
216
|
-
const decryptedValue = await this.decryptCookie(encryptedValue, browser.name)
|
|
217
|
-
if (decryptedValue) {
|
|
218
|
-
this.log(` Decrypted successfully (${decryptedValue.length} chars)`)
|
|
219
|
-
this.addCandidate(candidates, {
|
|
220
|
-
browser: browser.name,
|
|
221
|
-
lastAccessUtc: this.normalizeLastAccessUtc(row.last_access_utc),
|
|
222
|
-
profile,
|
|
223
|
-
sessionCookie: decryptedValue,
|
|
224
|
-
})
|
|
225
|
-
} else {
|
|
226
|
-
this.log(' Decryption returned empty value')
|
|
227
|
-
}
|
|
228
|
-
} catch (error) {
|
|
229
|
-
this.log(` Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
230
|
-
} finally {
|
|
231
|
-
rmSync(tempDirectory, { recursive: true, force: true })
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
this.log(`Total unique candidates: ${candidates.size}`)
|
|
236
|
-
return [...candidates.values()].sort((left, right) => right.lastAccessUtc - left.lastAccessUtc)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
findCookieDatabases(): string[] {
|
|
240
|
-
const results = BROWSERS.flatMap((browser) => this.findBrowserCookieDatabases(browser))
|
|
241
|
-
if (results.length === 0) {
|
|
242
|
-
this.log('No cookie databases found. Searched browsers:', BROWSERS.map((b) => b.name).join(', '))
|
|
243
|
-
}
|
|
244
|
-
return results
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private async decryptCookie(encryptedValue: Buffer, browserName: string): Promise<string> {
|
|
248
|
-
if (encryptedValue.length === 0) {
|
|
249
|
-
return ''
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (this.platform === 'linux') {
|
|
253
|
-
this.log(' Using Linux PBKDF2 decryption')
|
|
254
|
-
return this.decryptChromiumValue(encryptedValue, pbkdf2Sync('peanuts', CHROMIUM_SALT, 1, 16, 'sha1'))
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (this.platform === 'darwin') {
|
|
258
|
-
this.log(` Fetching macOS Keychain key for ${browserName}...`)
|
|
259
|
-
const key = await this.getMacOSEncryptionKey(browserName)
|
|
260
|
-
this.log(' Keychain key obtained')
|
|
261
|
-
return this.decryptChromiumValue(encryptedValue, key)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
this.log(` Unsupported platform for decryption: ${this.platform}`)
|
|
265
|
-
return ''
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private async getMacOSEncryptionKey(browserName: string): Promise<Buffer> {
|
|
269
|
-
const browser = BROWSERS.find((entry) => entry.name === browserName)
|
|
270
|
-
if (!browser) {
|
|
271
|
-
throw new Error(`Unsupported browser: ${browserName}`)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const command = `security find-generic-password -s ${JSON.stringify(browser.keychainService)} -a ${JSON.stringify(browser.keychainAccount)} -w`
|
|
275
|
-
this.log(` Keychain command: ${command}`)
|
|
276
|
-
|
|
277
|
-
const password = execSync(command, { encoding: 'utf8' }).trimEnd()
|
|
278
|
-
return pbkdf2Sync(password, CHROMIUM_SALT, 1003, 16, 'sha1')
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private decryptChromiumValue(encryptedValue: Buffer, key: Buffer): string {
|
|
282
|
-
const encryptedPayload =
|
|
283
|
-
encryptedValue.subarray(0, 3).toString('utf8') === 'v10' ? encryptedValue.subarray(3) : encryptedValue
|
|
284
|
-
const decipher = createDecipheriv('aes-128-cbc', key, CHROMIUM_IV)
|
|
285
|
-
decipher.setAutoPadding(true)
|
|
286
|
-
const decrypted = Buffer.concat([decipher.update(encryptedPayload), decipher.final()])
|
|
287
|
-
|
|
288
|
-
// Chromium v130+ prepends a 32-byte integrity hash before the actual cookie value
|
|
289
|
-
if (decrypted.length > 32) {
|
|
290
|
-
const hasNonPrintablePrefix = decrypted.subarray(0, 32).some((b) => b < 0x20 || b > 0x7e)
|
|
291
|
-
if (hasNonPrintablePrefix) {
|
|
292
|
-
return decrypted.subarray(32).toString('utf8')
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return decrypted.toString('utf8')
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private getBrowserByPath(databasePath: string): BrowserConfig | undefined {
|
|
300
|
-
return BROWSERS.find(
|
|
301
|
-
(browser) => databasePath.includes(`${browser.macPath}/`) || databasePath.includes(`${browser.linuxPath}/`),
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
private addCandidate(candidates: Map<string, ExtractedSessionCandidate>, candidate: ExtractedSessionCandidate): void {
|
|
306
|
-
const existing = candidates.get(candidate.sessionCookie)
|
|
307
|
-
if (!existing || existing.lastAccessUtc < candidate.lastAccessUtc) {
|
|
308
|
-
candidates.set(candidate.sessionCookie, candidate)
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private findBrowserCookieDatabases(browser: BrowserConfig): string[] {
|
|
313
|
-
const browserRoot = this.getBrowserRoot(browser)
|
|
314
|
-
if (!browserRoot || !existsSync(browserRoot)) {
|
|
315
|
-
this.log(`${browser.name}: not found at ${browserRoot ?? '(unsupported platform)'}`)
|
|
316
|
-
return []
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const profiles = readdirSync(browserRoot, { withFileTypes: true })
|
|
320
|
-
.filter((entry) => entry.isDirectory() && this.isSupportedProfileDirectory(entry.name))
|
|
321
|
-
.sort((left, right) => left.name.localeCompare(right.name))
|
|
322
|
-
|
|
323
|
-
const databases = profiles
|
|
324
|
-
.map((entry) => join(browserRoot, entry.name, 'Cookies'))
|
|
325
|
-
.filter((databasePath) => existsSync(databasePath))
|
|
326
|
-
|
|
327
|
-
this.log(`${browser.name}: ${profiles.length} profile(s), ${databases.length} cookie DB(s)`)
|
|
328
|
-
return databases
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
private getBrowserRoot(browser: BrowserConfig): string | null {
|
|
332
|
-
if (this.platform === 'darwin') {
|
|
333
|
-
return join(this.homeDirectory, 'Library', 'Application Support', browser.macPath)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (this.platform === 'linux') {
|
|
337
|
-
return join(this.homeDirectory, '.config', browser.linuxPath)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return null
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
private isSupportedProfileDirectory(profileName: string): boolean {
|
|
344
|
-
return profileName === 'Default' || PROFILE_DIR_PATTERN.test(profileName)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
private normalizeLastAccessUtc(lastAccessUtc: number | bigint | null | undefined): number {
|
|
348
|
-
if (typeof lastAccessUtc === 'bigint') {
|
|
349
|
-
return Number(lastAccessUtc)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return typeof lastAccessUtc === 'number' ? lastAccessUtc : 0
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function copySqliteDatabase(sourcePath: string, targetPath: string): void {
|
|
357
|
-
copyFileSync(sourcePath, targetPath)
|
|
358
|
-
|
|
359
|
-
for (const suffix of ['-wal', '-shm']) {
|
|
360
|
-
const sidecarSourcePath = `${sourcePath}${suffix}`
|
|
361
|
-
if (!existsSync(sidecarSourcePath)) {
|
|
362
|
-
continue
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
copyFileSync(sidecarSourcePath, `${targetPath}${suffix}`)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function normalizeCookieBytes(value: ArrayBuffer | Uint8Array | Buffer | null): Buffer | null {
|
|
370
|
-
if (!value) {
|
|
371
|
-
return null
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (Buffer.isBuffer(value)) {
|
|
375
|
-
return value
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (value instanceof Uint8Array) {
|
|
379
|
-
return Buffer.from(value)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return Buffer.from(value)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function normalizeCookieText(value: ArrayBuffer | Uint8Array | Buffer | string | null): string {
|
|
386
|
-
if (typeof value === 'string') {
|
|
387
|
-
return value.trim()
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const bytes = normalizeCookieBytes(value)
|
|
391
|
-
return bytes ? bytes.toString('utf8').trim() : ''
|
|
392
|
-
}
|