| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- /** Browser launch-token and persistent-cookie behavior. */
- import { createHmac } from 'node:crypto'
- import { afterEach, describe, expect, it, vi } from 'vitest'
- import type { CredentialProvider } from '@deepseek-ai/dsh-credentials'
- import { BrowserAuth } from '../src/browser-auth.ts'
- import type { ConnectionIndexRequest, ConnectionIndexResponse } from '../src/rpc.ts'
- import { RecordCredentials } from './browser-credentials.ts'
- function signedCookie(store: RecordCredentials, name: string, payload: unknown): string {
- const body = typeof payload === 'string'
- ? Buffer.from(payload, 'utf8').toString('base64url')
- : Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url')
- return signedBodyCookie(store, name, body)
- }
- function signedBodyCookie(store: RecordCredentials, name: string, body: string): string {
- const record = store.record
- if (record?.kind !== 'grant' || typeof record.payload !== 'object' || record.payload === null) {
- throw new Error('test credential store has no signing secret')
- }
- const secret: unknown = Reflect.get(record.payload, 'secret')
- if (typeof secret !== 'string') throw new Error('test credential record has no string secret')
- const signature = createHmac('sha256', Buffer.from(secret, 'base64url')).update(body).digest('base64url')
- return `${name}=v1.${body}.${signature}`
- }
- interface ResponseState {
- status?: number
- headers?: Readonly<Record<string, string>>
- body?: string
- }
- function response(): { value: ConnectionIndexResponse; state: ResponseState } {
- const state: ResponseState = {}
- return {
- value: {
- writeHead(status, headers) {
- state.status = status
- if (headers !== undefined) state.headers = headers
- },
- end(body) {
- if (body !== undefined) state.body = body
- },
- },
- state,
- }
- }
- function credentials(store: RecordCredentials): CredentialProvider {
- return store as unknown as CredentialProvider
- }
- function createAuth(
- store: RecordCredentials,
- maxAgeDays = 30,
- processOwner: object = {},
- ): Promise<BrowserAuth> {
- return BrowserAuth.create(processOwner, credentials(store), maxAgeDays)
- }
- function request(url: string, authority = '127.0.0.1:3080', init?: {
- cookie?: string
- method?: string
- }): ConnectionIndexRequest {
- return {
- method: init?.method ?? 'GET',
- url,
- headers: {
- host: authority,
- ...init?.cookie === undefined ? {} : { cookie: init.cookie },
- },
- }
- }
- function exchange(
- auth: BrowserAuth,
- authority = '127.0.0.1:3080',
- ): { cookie: string; launchUrl: string; state: ResponseState } {
- const launchUrl = auth.authenticatedUrl(`http://${authority}`)
- const target = new URL(launchUrl)
- const res = response()
- expect(auth.authorizeIndex(request(`${target.pathname}${target.search}`, authority), res.value)).toBe(false)
- const setCookie = res.state.headers?.['set-cookie']
- if (setCookie === undefined) throw new Error('token exchange did not set a cookie')
- return { cookie: setCookie.split(';', 1)[0]!, launchUrl, state: res.state }
- }
- afterEach(() => {
- vi.useRealTimers()
- })
- describe('BrowserAuth', () => {
- it('mints one process token and a persistent authority-bound cookie', async () => {
- const store = new RecordCredentials()
- const processOwner = {}
- const first = await createAuth(store, 30, processOwner)
- const login = exchange(first)
- expect(login.state).toMatchObject({
- status: 303,
- headers: {
- 'cache-control': 'no-store',
- 'location': '/',
- 'referrer-policy': 'no-referrer',
- },
- })
- expect(login.state.headers?.['set-cookie']).toMatch(/; Max-Age=2592000; Path=\/; Expires=.*; HttpOnly; SameSite=Strict$/u)
- expect(login.state.headers?.['set-cookie']).not.toContain('Secure')
- expect(first.isAuthenticated(request('/', '127.0.0.1:3080', { cookie: login.cookie }))).toBe(true)
- expect(first.isAuthenticated({
- headers: new Headers({ host: '127.0.0.1:3080', cookie: login.cookie }),
- })).toBe(true)
- expect(first.isAuthenticated({ headers: new Headers() })).toBe(false)
- expect(first.isAuthenticated(request('/', 'localhost:3080', { cookie: login.cookie }))).toBe(false)
- expect(first.isAuthenticated(request('/', '127.0.0.1:3081', { cookie: login.cookie }))).toBe(false)
- const reloaded = await createAuth(store, 30, processOwner)
- expect(reloaded.authenticatedUrl('http://127.0.0.1:3080')).toBe(login.launchUrl)
- expect(reloaded.isAuthenticated(request('/', '127.0.0.1:3080', { cookie: login.cookie }))).toBe(true)
- const restarted = await createAuth(store)
- expect(new URL(restarted.authenticatedUrl('http://127.0.0.1:3080')).searchParams.get('token'))
- .not.toBe(new URL(login.launchUrl).searchParams.get('token'))
- expect(restarted.isAuthenticated(request('/', '127.0.0.1:3080', { cookie: login.cookie }))).toBe(true)
- const staleUrl = new URL(login.launchUrl)
- const redirected = response()
- expect(restarted.authorizeIndex(request(
- `${staleUrl.pathname}${staleUrl.search}`,
- '127.0.0.1:3080',
- { cookie: login.cookie },
- ), redirected.value)).toBe(false)
- expect(redirected.state).toEqual({
- status: 303,
- headers: {
- 'cache-control': 'no-store',
- 'location': '/',
- 'referrer-policy': 'no-referrer',
- },
- })
- })
- it('accepts the cookie for index serving and gives every unauthenticated request one response', async () => {
- const auth = await createAuth(new RecordCredentials())
- const { cookie } = exchange(auth)
- const allowed = response()
- expect(auth.authorizeIndex(request('/index.html', '127.0.0.1:3080', { cookie }), allowed.value)).toBe(true)
- expect(allowed.state).toEqual({})
- for (const candidate of [
- request('/'),
- request('/?token=wrong'),
- request('/?token=wrong&token=again'),
- request('/index.html?token=wrong'),
- request(auth.authenticatedUrl('http://127.0.0.1:3080'), '127.0.0.1:3080', { method: 'HEAD' }),
- ]) {
- const denied = response()
- expect(auth.authorizeIndex(candidate, denied.value)).toBe(false)
- expect(denied.state.status).toBe(401)
- expect(denied.state.headers).toEqual({
- 'cache-control': 'no-store',
- 'content-type': 'text/plain; charset=utf-8',
- })
- expect(denied.state.body).toBe(candidate.method === 'HEAD'
- ? undefined
- : 'dsh web authentication required; reopen the URL printed by dsh web.\n')
- }
- })
- it('rejects tampering, expiry, future issuance, and a longer lifetime than configured', async () => {
- vi.useFakeTimers()
- vi.setSystemTime(new Date('2026-08-24T00:00:00.000Z'))
- const store = new RecordCredentials()
- const auth = await createAuth(store)
- const { cookie } = exchange(auth)
- const [name, value] = cookie.split('=') as [string, string]
- expect(auth.isAuthenticated(request('/', '127.0.0.1:3080', { cookie: `${name}=broken` }))).toBe(false)
- expect(auth.isAuthenticated(request('/', '127.0.0.1:3080', { cookie: `${name}=${value.slice(0, -1)}x` }))).toBe(false)
- expect(auth.isAuthenticated(request('/', '127.0.0.1:3080', { cookie: `${name}=%` }))).toBe(false)
- expect(auth.isAuthenticated(request('/', '127.0.0.1:3080', {
- cookie: signedBodyCookie(store, name, 'a'),
- }))).toBe(false)
- expect(auth.isAuthenticated({ headers: {} })).toBe(false)
- expect(auth.isAuthenticated({ headers: { host: 'bad host', cookie } })).toBe(false)
- expect(auth.isAuthenticated({ headers: { host: '127.0.0.1:3080' } })).toBe(false)
- const invalidPayloads: unknown[] = [
- 'not json',
- null,
- { version: 2, authority: '127.0.0.1:3080', issuedAt: Date.now(), expiresAt: Date.now() + 1000 },
- { version: 1, authority: 42, issuedAt: Date.now(), expiresAt: Date.now() + 1000 },
- { version: 1, authority: '127.0.0.1:3080', issuedAt: 'now', expiresAt: Date.now() + 1000 },
- { version: 1, authority: '127.0.0.1:3080', issuedAt: Date.now(), expiresAt: 'later' },
- ]
- for (const payload of invalidPayloads) {
- expect(auth.isAuthenticated(request('/', '127.0.0.1:3080', {
- cookie: signedCookie(store, name, payload),
- }))).toBe(false)
- }
- const shorter = await createAuth(store, 1)
- expect(shorter.isAuthenticated(request('/', '127.0.0.1:3080', { cookie }))).toBe(false)
- vi.setSystemTime(new Date('2026-09-24T00:00:00.000Z'))
- expect(auth.isAuthenticated(request('/', '127.0.0.1:3080', { cookie }))).toBe(false)
- vi.setSystemTime(new Date('2026-08-23T00:00:00.000Z'))
- expect(auth.isAuthenticated(request('/', '127.0.0.1:3080', { cookie }))).toBe(false)
- })
- it('loads one secret per activation and replaces it after deletion on the next activation', async () => {
- const store = new RecordCredentials()
- const auth = await createAuth(store)
- const first = exchange(auth)
- expect(store).toMatchObject({ reads: 0, modifies: 1 })
- await store.deleteRecord()
- expect(auth.isAuthenticated(request('/', '127.0.0.1:3080', { cookie: first.cookie }))).toBe(true)
- const sameActivation = exchange(auth)
- expect(auth.isAuthenticated(request('/', '127.0.0.1:3080', { cookie: sameActivation.cookie }))).toBe(true)
- expect(store).toMatchObject({ reads: 0, modifies: 1 })
- const reactivated = await createAuth(store)
- const second = exchange(reactivated)
- expect(second.cookie).not.toBe(first.cookie)
- expect(reactivated.isAuthenticated(request('/', '127.0.0.1:3080', { cookie: first.cookie }))).toBe(false)
- expect(reactivated.isAuthenticated(request('/', '127.0.0.1:3080', { cookie: second.cookie }))).toBe(true)
- expect(store).toMatchObject({ reads: 0, modifies: 2 })
- })
- it('fails loud on an invalid owner record instead of replacing it', async () => {
- const unsupported = new RecordCredentials()
- unsupported.record = { kind: 'api-key', key: 'not-a-cookie-secret' }
- await expect(createAuth(unsupported)).rejects.toThrow(/unsupported format/u)
- const malformed = new RecordCredentials()
- malformed.record = { kind: 'grant', payload: { version: 1, secret: 'short' } }
- await expect(createAuth(malformed)).rejects.toThrow(/invalid secret/u)
- const nonString = new RecordCredentials()
- nonString.record = { kind: 'grant', payload: { version: 1, secret: 42 } }
- await expect(createAuth(nonString)).rejects.toThrow(/invalid secret/u)
- const discarded = new RecordCredentials()
- discarded.discardWrites = true
- await expect(createAuth(discarded)).rejects.toThrow(/was not created/u)
- await expect(createAuth(new RecordCredentials(), Number.MAX_SAFE_INTEGER))
- .rejects.toThrow(/safe timestamp range/u)
- })
- })
|