| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465 |
- import { describe, expect, it, vi } from 'vitest'
- import { Context } from '@deepseek-ai/cordis'
- import { credentialKey } from '@deepseek-ai/dsh-credentials'
- import AuthorizationService, {
- AuthorizationDeclinedError,
- type AuthorizationFlow,
- type AuthorizationInteraction,
- type AuthorizationSession,
- } from '@deepseek-ai/dsh-authorization'
- import { MemoryCredentials } from './memory.ts'
- const KEY = credentialKey('llm-pi-ai', 'openai-codex')
- const OTHER = credentialKey('llm-pi-ai', 'anthropic')
- /** A context with the record store the seam confirms commits against. */
- async function harness(): Promise<Context> {
- const ctx = new Context()
- await ctx.plugin(MemoryCredentials)
- await ctx.plugin(AuthorizationService)
- return ctx
- }
- /** An interaction that answers every prompt with the same string. */
- function surface(answer = 'typed'): AuthorizationInteraction & {
- notices: unknown[]
- prompts: unknown[]
- } {
- const notices: unknown[] = []
- const prompts: unknown[] = []
- return {
- notices,
- prompts,
- notify: (notice) => { notices.push(notice) },
- prompt: (prompt) => {
- prompts.push(prompt)
- return Promise.resolve(answer)
- },
- }
- }
- /** A flow that commits `key` through the record store and then resolves. */
- function committingFlow(
- ctx: Context,
- key = KEY,
- run?: (session: AuthorizationSession) => Promise<void>,
- ): AuthorizationFlow {
- return {
- key,
- label: 'ChatGPT (Codex)',
- methods: [{ id: 'oauth', label: 'Sign in with ChatGPT' }, { id: 'api-key', label: 'Paste a key' }],
- async run(session) {
- await run?.(session)
- await ctx.credentials.modifyRecord(key, () =>
- Promise.resolve({ kind: 'grant', payload: { token: 'granted' } }))
- },
- }
- }
- describe('AuthorizationService registry', () => {
- it('lists a registered flow and drops it when the registration is disposed', async () => {
- const ctx = await harness()
- const dispose = ctx.authorization.registerFlow(committingFlow(ctx))
- expect(ctx.authorization.list()).toEqual([{
- key: KEY,
- label: 'ChatGPT (Codex)',
- methods: [{ id: 'oauth', label: 'Sign in with ChatGPT' }, { id: 'api-key', label: 'Paste a key' }],
- inFlight: false,
- }])
- expect(ctx.authorization.describe(KEY)?.label).toBe('ChatGPT (Codex)')
- expect(ctx.authorization.describe(OTHER)).toBeUndefined()
- dispose()
- expect(ctx.authorization.list()).toEqual([])
- expect(ctx.authorization.describe(KEY)).toBeUndefined()
- })
- it('refuses a second flow for the same key', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx))
- expect(() => ctx.authorization.registerFlow(committingFlow(ctx)))
- .toThrow(/already registered/)
- })
- it('withdraws an attempt still running when its flow leaves', async () => {
- const ctx = await harness()
- let started: (() => void) | undefined
- const running = new Promise<void>((resolve) => {
- started = resolve
- })
- const dispose = ctx.authorization.registerFlow(committingFlow(ctx, KEY, session =>
- new Promise((_resolve, reject) => {
- started?.()
- session.signal.addEventListener('abort', () => { reject(new Error('withdrawn')) }, { once: true })
- })))
- const attempt = ctx.authorization.begin({ key: KEY, interaction: surface() })
- await running
- dispose()
- await expect(attempt).resolves.toEqual({ status: 'cancelled' })
- })
- })
- describe('AuthorizationService.begin', () => {
- it('runs the flow, confirms the committed record, and reports the settlement', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx))
- const settled = vi.fn()
- ctx.on('authorization/settled', settled)
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .resolves.toEqual({ status: 'authorized' })
- expect(await ctx.credentials.readRecord(KEY)).toEqual({ kind: 'grant', payload: { token: 'granted' } })
- expect(settled).toHaveBeenCalledWith(KEY, 'authorized')
- })
- it('runs the flow first method when the caller names none, and the named one when it does', async () => {
- const ctx = await harness()
- const seen: string[] = []
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, (session) => {
- seen.push(session.method)
- return Promise.resolve()
- }))
- await ctx.authorization.begin({ key: KEY, interaction: surface() })
- await ctx.authorization.begin({ key: KEY, method: 'api-key', interaction: surface() })
- expect(seen).toEqual(['oauth', 'api-key'])
- })
- it('carries notices and prompts between the flow and the calling surface', async () => {
- const ctx = await harness()
- const answers: string[] = []
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, async (session) => {
- session.notify({ message: 'Continue in your browser', url: 'https://auth.example/start' })
- answers.push(await session.prompt({ kind: 'text', message: 'Paste the code' }))
- }))
- const ui = surface('code-123')
- await ctx.authorization.begin({ key: KEY, interaction: ui })
- expect(ui.notices).toEqual([{ message: 'Continue in your browser', url: 'https://auth.example/start' }])
- expect(ui.prompts).toEqual([{ kind: 'text', message: 'Paste the code' }])
- expect(answers).toEqual(['code-123'])
- })
- it('refuses a key no flow claims', async () => {
- const ctx = await harness()
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .rejects.toThrow(/no authorization flow is registered/)
- })
- it('refuses a method the flow does not offer', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx))
- await expect(ctx.authorization.begin({ key: KEY, method: 'device', interaction: surface() }))
- .rejects.toThrow(/offers no method "device"/)
- })
- it('refuses a second attempt while one is running, and admits one after it settles', async () => {
- const ctx = await harness()
- // Only the first attempt blocks; the later ones must be free to complete,
- // which is what shows the key was released rather than merely idle-looking.
- const held = Promise.withResolvers<undefined>()
- const started = Promise.withResolvers<undefined>()
- let first = true
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, () => {
- if (!first) return Promise.resolve()
- first = false
- started.resolve(undefined)
- return held.promise
- }))
- const attempt = ctx.authorization.begin({ key: KEY, interaction: surface() })
- await started.promise
- expect(ctx.authorization.describe(KEY)?.inFlight).toBe(true)
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .rejects.toThrow(/already running/)
- held.resolve(undefined)
- await expect(attempt).resolves.toEqual({ status: 'authorized' })
- expect(ctx.authorization.describe(KEY)?.inFlight).toBe(false)
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .resolves.toEqual({ status: 'authorized' })
- })
- it('never starts a flow whose caller withdrew before begin', async () => {
- const ctx = await harness()
- const ran = vi.fn()
- const settled = vi.fn()
- ctx.on('authorization/settled', settled)
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, () => {
- ran()
- return new Promise(() => {})
- }))
- await expect(ctx.authorization.begin({
- key: KEY,
- interaction: surface(),
- signal: AbortSignal.abort(),
- })).resolves.toEqual({ status: 'cancelled' })
- expect(ran).not.toHaveBeenCalled()
- // Nothing occupied the key, so nothing settled on it either.
- expect(settled).not.toHaveBeenCalled()
- expect(ctx.authorization.describe(KEY)?.inFlight).toBe(false)
- })
- it('still reports an unknown method to a caller that already withdrew', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx))
- await expect(ctx.authorization.begin({
- key: KEY,
- method: 'device',
- interaction: surface(),
- signal: AbortSignal.abort(),
- })).rejects.toThrow(/offers no method "device"/)
- })
- it('reports a caller that withdraws mid-flight as cancelled', async () => {
- const ctx = await harness()
- const controller = new AbortController()
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, session =>
- new Promise((_resolve, reject) => {
- session.signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
- controller.abort()
- })))
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface(), signal: controller.signal }))
- .resolves.toEqual({ status: 'cancelled' })
- })
- it('withdraws a running attempt through cancel(), and ignores cancel() for an idle key', async () => {
- const ctx = await harness()
- const started = Promise.withResolvers<undefined>()
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, session =>
- new Promise((_resolve, reject) => {
- session.signal.addEventListener('abort', () => { reject(new Error('cancelled')) }, { once: true })
- started.resolve(undefined)
- })))
- ctx.authorization.cancel(OTHER)
- const attempt = ctx.authorization.begin({ key: KEY, interaction: surface() })
- await started.promise
- ctx.authorization.cancel(KEY)
- await expect(attempt).resolves.toEqual({ status: 'cancelled' })
- })
- it('settles a withdrawn attempt even when its flow never reacts to the signal', async () => {
- const ctx = await harness()
- const orphan = Promise.withResolvers<undefined>()
- const started = Promise.withResolvers<undefined>()
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, () => {
- started.resolve(undefined)
- return orphan.promise
- }))
- const attempt = ctx.authorization.begin({ key: KEY, interaction: surface() })
- await started.promise
- ctx.authorization.cancel(KEY)
- await expect(attempt).resolves.toEqual({ status: 'cancelled' })
- // The key is free again immediately, rather than at the mercy of a flow
- // that may never settle.
- expect(ctx.authorization.describe(KEY)?.inFlight).toBe(false)
- // The orphan's own failure is nobody's to await, and must not surface as an
- // unhandled rejection.
- orphan.reject(new Error('gave up long after the human left'))
- await expect(orphan.promise).rejects.toThrow('gave up long after the human left')
- })
- it('propagates a flow failure to its caller and settles the key as failed', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, () =>
- Promise.reject(new Error('the token endpoint said no'))))
- const settled = vi.fn()
- ctx.on('authorization/settled', settled)
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .rejects.toThrow('the token endpoint said no')
- expect(settled).toHaveBeenCalledWith(KEY, 'failed')
- expect(ctx.authorization.describe(KEY)?.inFlight).toBe(false)
- })
- it('refuses a flow that resolves without committing its record', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow({
- key: KEY,
- label: 'Forgetful',
- methods: [{ id: 'oauth', label: 'Sign in' }],
- run: () => Promise.resolve(),
- })
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .rejects.toThrow(/resolved without committing a credential record/)
- })
- })
- describe('commit confirmation', () => {
- it('refuses a re-auth that left only the record of an earlier attempt', async () => {
- const ctx = await harness()
- await ctx.credentials.modifyRecord(KEY, () =>
- Promise.resolve({ kind: 'grant', payload: { token: 'stale' } }))
- ctx.authorization.registerFlow({
- key: KEY,
- label: 'Forgetful',
- methods: [{ id: 'oauth', label: 'Sign in' }],
- // A commit for another key is not this flow's commit either.
- async run() {
- await ctx.credentials.modifyRecord(OTHER, () =>
- Promise.resolve({ kind: 'grant', payload: { token: 'other' } }))
- },
- })
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .rejects.toThrow(/without committing a credential record in this attempt/)
- // Refused, not cleaned up: the stale record still belongs to its owner.
- expect(await ctx.credentials.readRecord(KEY)).toEqual({ kind: 'grant', payload: { token: 'stale' } })
- })
- it('refuses a flow that deleted its record instead of committing one', async () => {
- const ctx = await harness()
- await ctx.credentials.modifyRecord(KEY, () =>
- Promise.resolve({ kind: 'grant', payload: { token: 'stale' } }))
- ctx.authorization.registerFlow({
- key: KEY,
- label: 'Destructive',
- methods: [{ id: 'oauth', label: 'Sign in' }],
- run: () => ctx.credentials.deleteRecord(KEY),
- })
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .rejects.toThrow(/deleted its credential record/)
- })
- })
- describe('declined prompts', () => {
- it('reports an attempt whose prompt the human declined as cancelled, not failed', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, async (session) => {
- await session.prompt({ kind: 'text', message: 'Paste the code' })
- }))
- const settled = vi.fn()
- ctx.on('authorization/settled', settled)
- const declining: AuthorizationInteraction = {
- notify: () => undefined,
- prompt: () => Promise.reject(new AuthorizationDeclinedError()),
- }
- await expect(ctx.authorization.begin({ key: KEY, interaction: declining }))
- .resolves.toEqual({ status: 'cancelled' })
- expect(settled).toHaveBeenCalledWith(KEY, 'cancelled')
- })
- it('reads a decline through a flow that rewraps the rejection on its way out', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, session =>
- session.prompt({ kind: 'text', message: 'Paste the code' }).then(
- () => undefined,
- () => {
- throw new Error('sign-in aborted')
- })))
- const declining: AuthorizationInteraction = {
- notify: () => undefined,
- prompt: () => Promise.reject(new AuthorizationDeclinedError()),
- }
- await expect(ctx.authorization.begin({ key: KEY, interaction: declining }))
- .resolves.toEqual({ status: 'cancelled' })
- })
- it('keeps a prompt failure that is not a decline a flow failure', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, async (session) => {
- await session.prompt({ kind: 'text', message: 'Paste the code' })
- }))
- const settled = vi.fn()
- ctx.on('authorization/settled', settled)
- const broken: AuthorizationInteraction = {
- notify: () => undefined,
- prompt: () => Promise.reject(new Error('the transport dropped')),
- }
- await expect(ctx.authorization.begin({ key: KEY, interaction: broken }))
- .rejects.toThrow('the transport dropped')
- expect(settled).toHaveBeenCalledWith(KEY, 'failed')
- })
- })
- describe('notice containment', () => {
- it('loses the notice, never the attempt, when the surface cannot render it', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx, KEY, (session) => {
- session.notify({ message: 'Continue in your browser' })
- return Promise.resolve()
- }))
- const broken: AuthorizationInteraction = {
- notify: () => {
- throw new Error('page connection closed')
- },
- prompt: () => Promise.resolve('unused'),
- }
- await expect(ctx.authorization.begin({ key: KEY, interaction: broken }))
- .resolves.toEqual({ status: 'authorized' })
- })
- })
- describe('the settled fan-out', () => {
- it('keeps a throwing listener from changing a finished attempt, and later listeners still run', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx))
- ctx.on('authorization/settled', () => {
- throw new Error('watcher boom')
- })
- const second = vi.fn()
- ctx.on('authorization/settled', second)
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .resolves.toEqual({ status: 'authorized' })
- expect(second).toHaveBeenCalledWith(KEY, 'authorized')
- })
- it('contains an async listener rejection', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx))
- // An unknown-returning function keeps the typed surface legal while the
- // runtime value is still the rejected promise the containment must handle.
- const boom = (): unknown => Promise.reject(new Error('async watcher boom'))
- ctx.on('authorization/settled', boom)
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .resolves.toEqual({ status: 'authorized' })
- await new Promise(resolve => setTimeout(resolve, 10))
- })
- it('rethrows an invariant-coded listener failure after the remaining listeners', async () => {
- const ctx = await harness()
- ctx.authorization.registerFlow(committingFlow(ctx))
- ctx.on('authorization/settled', () => {
- throw Object.assign(new Error('forged relation'), { code: 'INVARIANT' })
- })
- const second = vi.fn()
- ctx.on('authorization/settled', second)
- await expect(ctx.authorization.begin({ key: KEY, interaction: surface() }))
- .rejects.toThrow(/forged relation/)
- // Harness-fatal by design — but the record itself committed first.
- expect(second).toHaveBeenCalledWith(KEY, 'authorized')
- expect(await ctx.credentials.readRecord(KEY)).toEqual({ kind: 'grant', payload: { token: 'granted' } })
- })
- })
|