import * as ApiKeys from './ApiKeys.js' import * as Store from './internal/Store.js' describe('resolve', () => { test('resolves an unknown token to null', async () => { expect(await ApiKeys.resolve(Store.memory(), 'tempo:sk:missing')).toBeNull() }) test('resolves before expiry, rejects after', async () => { vi.useFakeTimers({ now: new Date('2026-01-01T00:00:00.000Z') }) try { const state = Store.memory() const { token } = await ApiKeys.mint(state, { expiresAt: '2026-01-01T00:01:00.000Z', orgId: 'org_a', scopes: ['data:read'], }) expect((await ApiKeys.resolve(state, token))?.orgId).toBe('org_a') // Advance the clock past the expiry. vi.setSystemTime(new Date('2026-01-01T00:02:00.000Z')) expect(await ApiKeys.resolve(state, token)).toBeNull() } finally { vi.useRealTimers() } }) test('treats a corrupt record as absent', async () => { const state = Store.memory() const { token } = await ApiKeys.mint(state, { orgId: 'org_a', scopes: ['data:read'] }) // Overwrite the stored record (keyed by token hash) with invalid JSON. const { keys } = await state.list({ prefix: 'apikey:' }) await state.put(keys[0]!.name, 'not json') expect(await ApiKeys.resolve(state, token)).toBeNull() }) test('rejects a record with an unknown scope', async () => { const state = Store.memory() const { token } = await ApiKeys.mint(state, { orgId: 'org_a', scopes: ['data:read'] }) const { keys } = await state.list({ prefix: 'apikey:' }) await state.put( keys[0]!.name, JSON.stringify({ createdAt: '2026-01-01T00:00:00.000Z', id: 'key_x', orgId: 'org_a', scopes: ['data:read', 'totally:made-up'], tokenLast4: 'xxxx', }), ) expect(await ApiKeys.resolve(state, token)).toBeNull() }) test('normalizes persisted per-minute rate limits', async () => { const state = Store.memory() const { token } = await ApiKeys.mint(state, { orgId: 'org_a', scopes: ['data:read'] }) const { keys } = await state.list({ prefix: 'apikey:' }) const value = JSON.parse((await state.get(keys[0]!.name))!) as Record await state.put( keys[0]!.name, JSON.stringify({ ...value, rateLimits: { '*': { perMinute: 120 } } }), ) expect((await ApiKeys.resolve(state, token))?.rateLimits).toMatchInlineSnapshot(` { "*": { "limit": 120, "period": "minute", }, } `) }) }) // Mock the system clock so timestamps are deterministic without injecting a // clock into the module. describe('mint', () => { beforeEach(() => vi.useFakeTimers({ now: new Date('2026-01-01T00:00:00.000Z') })) afterEach(() => vi.useRealTimers()) test('mints a key and resolves it', async () => { const state = Store.memory() const { record, token } = await ApiKeys.mint(state, { createdBy: 'admin@tempo.xyz', name: 'CI key', orgId: 'org_acme', scopes: ['data:read', 'webhooks:write'], }) expect(token).toMatch(/^tempo:sk:[a-f0-9]{48}$/) expect(record.tokenLast4).toBe(token.slice(-4)) expect({ ...record, id: '', tokenLast4: '' }).toMatchInlineSnapshot(` { "allowedIps": [], "createdAt": "2026-01-01T00:00:00.000Z", "createdBy": "admin@tempo.xyz", "environment": "production", "id": "", "name": "CI key", "orgId": "org_acme", "scopes": [ "data:read", "webhooks:write", ], "tokenLast4": "", } `) const resolved = await ApiKeys.resolve(state, token) expect(resolved?.orgId).toBe('org_acme') expect(resolved?.scopes).toEqual(['data:read', 'webhooks:write']) }) test('rejects an unknown scope at the write boundary', async () => { await expect( ApiKeys.mint(Store.memory(), { orgId: 'org_a', scopes: ['totally:made-up' as never] }), ).rejects.toThrowError(/invalid API key input/) }) test('mints per-zone read and write scopes', async () => { const state = Store.memory() const { token } = await ApiKeys.mint(state, { orgId: 'org_a', scopes: ['zone:421700001:read', 'zone:421700001:write'], }) const resolved = await ApiKeys.resolve(state, token) expect(resolved?.scopes).toEqual(['zone:421700001:read', 'zone:421700001:write']) }) test('rejects malformed zone scopes', async () => { for (const scope of [ 'zone::read', 'zone:abc:read', 'zone:1.5:read', 'zone:-1:read', 'zone:0:read', 'zone:0:write', 'zone:000421700001:read', 'zone:1:admin', ]) await expect( ApiKeys.mint(Store.memory(), { orgId: 'org_a', scopes: [scope as never] }), ).rejects.toThrowError(/invalid API key input/) }) }) describe('list', () => { beforeEach(() => vi.useFakeTimers({ now: new Date('2026-01-01T00:00:00.000Z') })) afterEach(() => vi.useRealTimers()) test('lists keys newest first, filtered by org', async () => { const state = Store.memory() // Advance the clock between mints so their createdAt timestamps differ and // "newest first" is well-defined. await ApiKeys.mint(state, { name: 'a', orgId: 'org_a', scopes: ['data:read'] }) vi.setSystemTime(new Date('2026-01-01T00:00:01.000Z')) await ApiKeys.mint(state, { name: 'b', orgId: 'org_b', scopes: ['data:read'] }) const all = await ApiKeys.list(state) expect(all.map((record) => record.name)).toMatchInlineSnapshot(` [ "b", "a", ] `) const scoped = await ApiKeys.list(state, { orgId: 'org_b' }) expect(scoped.map((record) => record.orgId)).toMatchInlineSnapshot(` [ "org_b", ] `) }) test('excludes a key past its expiry', async () => { const state = Store.memory() await ApiKeys.mint(state, { expiresAt: '2026-01-01T00:00:30.000Z', name: 'short-lived', orgId: 'org_a', scopes: ['data:read'], }) expect((await ApiKeys.list(state)).map((record) => record.name)).toMatchInlineSnapshot(` [ "short-lived", ] `) // Advance the clock past the expiry. vi.setSystemTime(new Date('2026-01-01T00:01:00.000Z')) expect(await ApiKeys.list(state)).toMatchInlineSnapshot(`[]`) }) }) describe('revoke', () => { test('revokes a key by id', async () => { const state = Store.memory() const { record, token } = await ApiKeys.mint(state, { orgId: 'org_a', scopes: ['data:read'] }) expect(await ApiKeys.revoke(state, record.id)).toBe(true) expect(await ApiKeys.resolve(state, token)).toBeNull() expect(await ApiKeys.revoke(state, record.id)).toBe(false) }) test('revoke is O(1): deletes the record without scanning the prefix', async () => { const state = Store.memory() const { record } = await ApiKeys.mint(state, { orgId: 'org_a', scopes: ['data:read'] }) // The id index entry maps id -> record key; revoke uses it directly. expect(await ApiKeys.revoke(state, record.id)).toBe(true) // The record and both index entries are gone. const { keys } = await state.list({ prefix: 'apikey' }) expect(keys).toMatchInlineSnapshot(`[]`) }) }) describe('get', () => { test('reads a record by id, null when absent', async () => { const state = Store.memory() const { record } = await ApiKeys.mint(state, { orgId: 'org_a', projectId: 'prj_a', scopes: ['data:read'], }) expect((await ApiKeys.get(state, record.id))?.projectId).toBe('prj_a') expect(await ApiKeys.get(state, 'key_missing')).toBeNull() }) }) describe('listByOrg', () => { beforeEach(() => vi.useFakeTimers({ now: new Date('2026-01-01T00:00:00.000Z') })) afterEach(() => vi.useRealTimers()) test('lists through the org index, newest first, filtered by project', async () => { const state = Store.memory() await ApiKeys.mint(state, { name: 'a', orgId: 'org_a', projectId: 'prj_a', scopes: ['data:read'] }) // prettier-ignore vi.setSystemTime(new Date('2026-01-01T00:00:01.000Z')) await ApiKeys.mint(state, { name: 'b', orgId: 'org_a', projectId: 'prj_b', scopes: ['data:read'] }) // prettier-ignore await ApiKeys.mint(state, { name: 'c', orgId: 'org_b', scopes: ['data:read'] }) expect((await ApiKeys.listByOrg(state, 'org_a')).map((record) => record.name)) .toMatchInlineSnapshot(` [ "b", "a", ] `) expect( (await ApiKeys.listByOrg(state, 'org_a', { projectId: 'prj_b' })).map( (record) => record.name, ), ).toMatchInlineSnapshot(` [ "b", ] `) }) test('skips and lazily cleans dangling index entries', async () => { const state = Store.memory() await ApiKeys.mint(state, { orgId: 'org_a', scopes: ['data:read'] }) // Simulate a record evicted out-of-band (e.g. native TTL), leaving the // index entry behind. const recordKey = (await state.list({ prefix: 'apikey:' })).keys[0]!.name await state.delete(recordKey) expect(await ApiKeys.listByOrg(state, 'org_a')).toEqual([]) expect((await state.list({ prefix: 'apikey_org:' })).keys).toMatchInlineSnapshot(`[]`) }) }) describe('update', () => { test('updates the name, org, and project while moving the org index', async () => { const state = Store.memory() const { record, token } = await ApiKeys.mint(state, { orgId: 'org_a', scopes: ['data:read'] }) const updated = await ApiKeys.update(state, record.id, { name: 'Explorer', orgId: 'org_b', projectId: 'prj_b', }) expect(updated?.name).toBe('Explorer') expect(updated?.orgId).toBe('org_b') expect(updated?.projectId).toBe('prj_b') // The original token keeps resolving, now carrying the attribution. const resolved = await ApiKeys.resolve(state, token) expect(resolved?.orgId).toBe('org_b') expect(resolved?.projectId).toBe('prj_b') expect(await ApiKeys.listByOrg(state, 'org_a')).toEqual([]) expect((await ApiKeys.listByOrg(state, 'org_b')).map(({ id }) => id)).toEqual([record.id]) expect(await ApiKeys.update(state, 'key_missing', { projectId: 'prj_x' })).toBeNull() expect(await ApiKeys.attribute(state, 'key_missing', { projectId: 'prj_x' })).toBeNull() }) test('replaces and clears the IP allowlist without changing the token', async () => { const state = Store.memory() const { record, token } = await ApiKeys.mint(state, { allowedIps: ['203.0.113.0/24'], orgId: 'org_a', scopes: ['data:read'], }) const updated = await ApiKeys.update(state, record.id, { allowedIps: ['2001:db8::/32'], }) expect(updated?.allowedIps).toEqual(['2001:db8::/32']) expect((await ApiKeys.resolve(state, token))?.allowedIps).toEqual(['2001:db8::/32']) const cleared = await ApiKeys.update(state, record.id, { allowedIps: [] }) expect(cleared?.allowedIps).toEqual([]) expect((await ApiKeys.resolve(state, token))?.allowedIps).toEqual([]) }) test('rejects invalid IP allowlist rules at the write boundary', async () => { const state = Store.memory() const { record } = await ApiKeys.mint(state, { orgId: 'org_a', scopes: ['data:read'] }) await expect( ApiKeys.update(state, record.id, { allowedIps: ['203.0.113.0/33'] }), ).rejects.toThrowError(/invalid API key input/) }) }) describe('setBillingActive', () => { test('stamps billingActive onto the org keys in the target environment only', async () => { const state = Store.memory() const { token: sandboxToken } = await ApiKeys.mint(state, { environment: 'sandbox', orgId: 'org_a', scopes: ['data:read'], }) const { token: prodToken } = await ApiKeys.mint(state, { environment: 'production', orgId: 'org_a', scopes: ['data:read'], }) const { token: otherToken } = await ApiKeys.mint(state, { environment: 'sandbox', orgId: 'org_b', scopes: ['data:read'], }) const updated = await ApiKeys.setBillingActive(state, { active: true, environment: 'sandbox', orgId: 'org_a', }) expect(updated).toBe(1) expect((await ApiKeys.resolve(state, sandboxToken))?.billingActive).toBe(true) // The production key and another org's sandbox key are untouched. expect((await ApiKeys.resolve(state, prodToken))?.billingActive).toBeUndefined() expect((await ApiKeys.resolve(state, otherToken))?.billingActive).toBeUndefined() }) test('skips records already carrying the target value', async () => { const state = Store.memory() await ApiKeys.mint(state, { billingActive: true, environment: 'sandbox', orgId: 'org_a', scopes: ['data:read'], }) expect( await ApiKeys.setBillingActive(state, { active: true, environment: 'sandbox', orgId: 'org_a', }), ).toBe(0) }) }) describe('backfill', () => { test('rebuilds index entries for legacy records', async () => { const state = Store.memory() const { record, token } = await ApiKeys.mint(state, { orgId: 'org_a', scopes: ['data:read'] }) // Legacy state: records exist, index entries do not. for (const { name } of (await state.list({ prefix: 'apikey_id:' })).keys) await state.delete(name) for (const { name } of (await state.list({ prefix: 'apikey_org:' })).keys) await state.delete(name) expect(await ApiKeys.listByOrg(state, 'org_a')).toEqual([]) expect(await ApiKeys.backfill(state)).toEqual({ indexed: 1, scanned: 1 }) expect((await ApiKeys.listByOrg(state, 'org_a')).map(({ id }) => id)).toEqual([record.id]) expect(await ApiKeys.resolve(state, token)).not.toBeNull() // The id index is restored, so revoke-by-id works again. expect(await ApiKeys.revoke(state, record.id)).toBe(true) }) })