import { describe, expect, it } from 'vitest'; import { decodeJwt, expiryOf } from '#/lib/jwt.js'; /** * @param {object} header @param {object} payload * @returns {string} */ function jwt(header, payload) { const part = (value) => btoa(JSON.stringify(value)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); return `${part(header)}.${part(payload)}.c2ln`; } describe('decodeJwt', () => { it('reads both segments of a space credential', () => { const token = jwt( { alg: 'ES256', typ: 'atproto-space-credential+jwt', kid: '#atproto' }, { iss: 'did:plc:authority', sub: 'at://did:plc:authority/space/com.example.forum/general', cnf: { jkt: 'a-thumbprint' }, exp: 1_800_000_000, }, ); const decoded = decodeJwt(token); expect(decoded?.header.typ).toBe('atproto-space-credential+jwt'); expect(decoded?.payload.cnf.jkt).toBe('a-thumbprint'); }); // A segment whose length is not a multiple of four is the normal case: these // tokens are base64url with the padding stripped. it('decodes a segment whose padding was stripped', () => { const token = jwt({ alg: 'ES256' }, { iss: 'did:plc:a' }); expect(token.split('.')[1].length % 4).not.toBe(0); expect(decodeJwt(token)?.payload.iss).toBe('did:plc:a'); }); it('answers null for anything that is not a JWT', () => { expect(decodeJwt('not-a-token')).toBeNull(); expect(decodeJwt('')).toBeNull(); expect(decodeJwt('a.b.c')).toBeNull(); }); }); describe('expiryOf', () => { it('reads exp as milliseconds', () => { expect(expiryOf(jwt({}, { exp: 1_800_000_000 }))).toBe(1_800_000_000_000); }); // A token stating no expiry gets a held-for-ten-minutes fallback at the call // site, which only fires because this answers zero rather than NaN. it('answers zero for a token that states none', () => { expect(expiryOf(jwt({}, { iss: 'did:plc:a' }))).toBe(0); expect(expiryOf('rubbish')).toBe(0); }); });