import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { checkBundle, checkHostedMatchesLocal, checkManifestKey, checkRedirectUris, extensionIdFromKey, redirectUriFor, } from './check-oauth-metadata.mjs' // The real pair from public/manifest.json, so a key or derivation change // that would silently move the unpacked id fails here. const MANIFEST_KEY = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9X42ElF0Js6o5Q4khUvEZJCEnEJuXEswyH4AXc9pA98vP5K/wIyj3grJIREAVHvJYCtAfvWPlLq//WQdnoRt5j7RljVDrwRxuxqDXZ7k3M6ipX8czg7wKfS0Gq2rTgVZD1po96eHTFNyxbaR08maLNIwNNWYqbwCECRR41uXdpw+XS66KvGZ8RybnPArkk0jW9lQ5ro/WlDpyjmTxWWYIM2nkZ/m2e66ZLCuT7HcYpHJmsEOH/xBUkgRZMzmEcQJk5m3rSuEOaB4DzSGZTaRy2OLzxMbAnpgKPTOSqa2RLYvb4MsrUgs2akezzGD0pJD46FkeB6eaLxqCprrIn3fIQIDAQAB' const UNPACKED = 'degljbilkggdpbobomfbgnellecgbkjj' const STORE = 'mecbfognmmefgjekidnddjjlddnfnlki' const IDS = { store: STORE, unpacked: UNPACKED } let dirs = [] afterEach(() => { for (const dir of dirs) rmSync(dir, { recursive: true, force: true }) dirs = [] }) function distWith(contents) { const dir = mkdtempSync(join(tmpdir(), 'check-oauth-')) dirs.push(dir) writeFileSync(join(dir, 'bundle.js'), contents) return dir } describe('extensionIdFromKey', () => { it('derives the unpacked id Chrome assigns to the manifest key', () => { expect(extensionIdFromKey(MANIFEST_KEY)).toBe(UNPACKED) }) it('only ever produces the a-p alphabet Chrome uses', () => { expect(extensionIdFromKey(MANIFEST_KEY)).toMatch(/^[a-p]{32}$/) }) }) describe('checkRedirectUris', () => { it('passes when every declared id has a redirect uri', () => { const metadata = { redirect_uris: [redirectUriFor(STORE), redirectUriFor(UNPACKED)] } expect(checkRedirectUris(metadata, IDS)).toEqual([]) }) // The v1.2.1 regression: metadata carrying only the unpacked dev id, which // makes sign-in throw "Invalid redirect_uri" in every store install. it('fails the exact shape that shipped broken in v1.2.1', () => { const metadata = { redirect_uris: [redirectUriFor(UNPACKED)] } const problems = checkRedirectUris(metadata, IDS) expect(problems).toHaveLength(1) expect(problems[0]).toContain('store') expect(problems[0]).toContain(STORE) }) it('fails a redirect uri that belongs to no declared id', () => { const metadata = { redirect_uris: [ redirectUriFor(STORE), redirectUriFor(UNPACKED), redirectUriFor('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), ], } expect(checkRedirectUris(metadata, IDS)).toEqual([ expect.stringContaining('not a declared id'), ]) }) it('reports missing metadata rather than throwing', () => { expect(checkRedirectUris({}, IDS)).toHaveLength(2) }) }) describe('checkManifestKey', () => { it('accepts the key that derives the declared unpacked id', () => { expect(checkManifestKey({ key: MANIFEST_KEY }, IDS)).toEqual([]) }) it('fails when the key and the declared unpacked id disagree', () => { expect(checkManifestKey({ key: MANIFEST_KEY }, { ...IDS, unpacked: 'b'.repeat(32) })).toEqual([ expect.stringContaining(UNPACKED), ]) }) it('skips the check for a store build, whose key is stripped', () => { expect(checkManifestKey({}, IDS)).toEqual([]) }) }) describe('checkHostedMatchesLocal', () => { const local = { client_id: 'https://example.test/m.json', redirect_uris: ['a'] } it('passes on identical documents regardless of key order', () => { expect( checkHostedMatchesLocal(local, { redirect_uris: ['a'], client_id: local.client_id }), ).toEqual([]) }) it('fails when the hosted copy is missing a redirect uri', () => { const problems = checkHostedMatchesLocal(local, { ...local, redirect_uris: [] }) expect(problems).toEqual([expect.stringContaining('differs from')]) }) // The v1.4.0 scope change failed here with two identical redirect_uris lists // printed as the evidence: the field that had drifted was never named. it('names the field that differs, not always redirect_uris', () => { const withScope = { ...local, scope: 'atproto include:app.userinput.authBasic' } const problems = checkHostedMatchesLocal(withScope, { ...local, scope: 'atproto' }) expect(problems).toHaveLength(1) expect(problems[0]).toContain('in scope:') expect(problems[0]).toContain('include:app.userinput.authBasic') expect(problems[0]).not.toContain('redirect_uris') }) it('reports a field the hosted copy does not have at all', () => { const problems = checkHostedMatchesLocal({ ...local, scope: 'atproto' }, local) expect(problems).toEqual([expect.stringContaining('hosted=(absent)')]) }) it('reports every drifted field, not just the first', () => { const problems = checkHostedMatchesLocal( { ...local, scope: 'atproto', client_name: 'substandard' }, { ...local, scope: 'x', client_name: 'y' }, ) expect(problems).toHaveLength(2) }) }) describe('checkBundle', () => { it('passes when the built JS carries every declared redirect uri', () => { const dir = distWith(`x=${JSON.stringify([redirectUriFor(STORE), redirectUriFor(UNPACKED)])}`) expect(checkBundle(dir, IDS)).toEqual([]) }) // A rebuild from stale sources is invisible everywhere else: the metadata // file, the id list and the hosted copy can all agree while the artifact // about to be uploaded predates them. it('fails a stale bundle that predates the store id', () => { const dir = distWith(`x=${JSON.stringify([redirectUriFor(UNPACKED)])}`) expect(checkBundle(dir, IDS)).toEqual([expect.stringContaining(redirectUriFor(STORE))]) }) })