Files
web-terminal/control-panel/test/manage-token.test.ts
Yaojia Wang 675de771c7 feat(control-panel): web admin UI for the zero-touch tunnel
Loopback Fastify auth-broker + esbuild SPA. Operator password login (constant-time,
signed HttpOnly session cookie, per-forwarded-IP rate-limit) → session-gated proxy
that mints a fresh 60s manage capability token per call to the control-plane admin
API: list hosts, mint pairing codes (with QR + pair command), revoke hosts. Security
headers + CSP, CP_URL pinned loopback (anti-SSRF), hostId dot-segment guard. 55 tests
pass; security-reviewed. Deployed behind nginx panel.terminal.yaojia.wang.
2026-07-19 19:47:51 +02:00

60 lines
2.2 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { writeFile, mkdtemp } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { peekPasetoClaims } from 'relay-auth/src/crypto/paseto.js'
import { createManageTokenMinter, ManageTokenError } from '../src/manage-token.js'
import { generatePkcs8Pem } from './helpers.js'
async function writeKey(): Promise<string> {
const { pem } = await generatePkcs8Pem()
const dir = await mkdtemp(join(tmpdir(), 'cp-key-'))
const path = join(dir, 'capability-sign.key.pem')
await writeFile(path, pem, 'utf8')
return path
}
describe('createManageTokenMinter', () => {
it('mints a manage token with the correct aud, sub, rights, and TTL', async () => {
const keyPath = await writeKey()
const minter = createManageTokenMinter({
capabilitySignKeyPath: keyPath,
baseDomain: 'terminal.yaojia.wang',
operatorAccountId: 'acct-op-1',
})
const token = await minter.mint()
expect(token.startsWith('v4.public.')).toBe(true)
const claims = peekPasetoClaims(token) as Record<string, unknown>
expect(claims.aud).toBe('terminal.yaojia.wang')
expect(claims.sub).toBe('acct-op-1')
expect(claims.rights).toEqual(['manage'])
expect(typeof claims.host).toBe('string')
expect((claims.exp as number) - (claims.iat as number)).toBe(60)
// The additive DPoP proof-of-possession binding must be present.
expect((claims.cnf as { jkt?: string })?.jkt).toMatch(/^[A-Za-z0-9_-]{43}$/)
})
it('mints a fresh token each call (distinct jti)', async () => {
const keyPath = await writeKey()
const minter = createManageTokenMinter({
capabilitySignKeyPath: keyPath,
baseDomain: 'd',
operatorAccountId: 'a',
})
const a = peekPasetoClaims(await minter.mint()) as Record<string, unknown>
const b = peekPasetoClaims(await minter.mint()) as Record<string, unknown>
expect(a.jti).not.toBe(b.jti)
})
it('throws ManageTokenError when the key file is missing', async () => {
const minter = createManageTokenMinter({
capabilitySignKeyPath: '/nonexistent/does-not-exist.pem',
baseDomain: 'd',
operatorAccountId: 'a',
})
await expect(minter.mint()).rejects.toBeInstanceOf(ManageTokenError)
})
})