Files
pig/apps/api/test/api-keys.test.ts
T
2026-08-13 01:39:01 -07:00

117 lines
3.9 KiB
TypeScript

import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { Principal } from '../src/lib/auth';
import {
assertApiKeyActive,
AuthError,
hashApiKey,
requireCapability,
} from '../src/lib/auth';
import {
apiKeyCreationResponse,
apiKeyCreateSchema,
apiKeyMetadata,
generateApiKey,
resolveApiKeyTarget,
} from '../src/routes/api-keys';
const userId = '00000000-0000-0000-0000-000000000001';
const otherUserId = '00000000-0000-0000-0000-000000000002';
function principal(overrides: Partial<Principal> = {}): Principal {
return {
userId,
email: 'seller@example.com',
name: 'Seller',
isPlatformAdmin: false,
teams: [{ team: 'demand', role: 'member' }],
via: 'jwt',
scopes: ['read', 'write'],
...overrides,
};
}
function storedKey(overrides: Record<string, unknown> = {}) {
return {
id: '00000000-0000-0000-0000-000000000010',
userId,
name: 'Codex',
keyHash: 'stored-hash',
keyPrefix: 'pig_abc123',
scopes: ['read'],
lastUsedAt: null,
expiresAt: null,
revokedAt: null,
createdAt: new Date('2026-08-12T12:00:00.000Z'),
...overrides,
} as Parameters<typeof apiKeyMetadata>[0];
}
describe('API key lifecycle decisions', () => {
it('stores only a SHA-256 hash and returns plaintext only from creation', () => {
const generated = generateApiKey();
assert.match(generated.key, /^pig_[A-Za-z0-9_-]{43}$/);
assert.equal(generated.keyHash, hashApiKey(generated.key));
assert.equal(generated.keyHash.length, 64);
assert.equal(generated.keyPrefix, generated.key.slice(0, 10));
const row = storedKey({ keyHash: generated.keyHash, keyPrefix: generated.keyPrefix });
const created = apiKeyCreationResponse(row, generated.key);
const listed = apiKeyMetadata(row);
assert.equal(created.key, generated.key);
assert.equal('key' in listed, false);
assert.equal('keyHash' in listed, false);
});
it('allows read-only or read/write keys, never write-only keys', () => {
assert.deepEqual(apiKeyCreateSchema.parse({ name: 'Reader' }).scopes, ['read']);
assert.deepEqual(
apiKeyCreateSchema.parse({ name: 'Writer', scopes: ['read', 'write'] }).scopes,
['read', 'write'],
);
assert.equal(apiKeyCreateSchema.safeParse({ name: 'Writer', scopes: ['write'] }).success, false);
assert.equal(
apiKeyCreateSchema.safeParse({ name: 'Duplicate', scopes: ['read', 'read'] }).success,
false,
);
assert.throws(
() => requireCapability(principal({ via: 'api_key', scopes: ['read'] }), 'deal:write', 'demand'),
(error: unknown) => error instanceof AuthError && error.code === 'insufficient_scope',
);
assert.doesNotThrow(() =>
requireCapability(
principal({ via: 'api_key', scopes: ['read', 'write'] }),
'deal:write',
'demand',
),
);
});
it('rejects a revoked key immediately while leaving an active key usable', () => {
const now = new Date('2026-08-12T12:00:00.000Z');
assert.doesNotThrow(() => assertApiKeyActive({ revokedAt: null, expiresAt: null }, now));
assert.throws(
() => assertApiKeyActive({ revokedAt: new Date('2026-08-12T11:59:00.000Z'), expiresAt: null }, now),
(error: unknown) => error instanceof AuthError && error.code === 'revoked_key',
);
});
it('keeps self-service personal, with explicit platform-admin cross-user access', () => {
assert.equal(resolveApiKeyTarget(principal()), userId);
assert.throws(
() => resolveApiKeyTarget(principal(), otherUserId),
(error: unknown) => error instanceof AuthError && error.code === 'insufficient_permission',
);
assert.equal(
resolveApiKeyTarget(principal({ isPlatformAdmin: true }), otherUserId),
otherUserId,
);
assert.throws(
() => resolveApiKeyTarget(principal({ via: 'api_key' })),
(error: unknown) =>
error instanceof AuthError && error.code === 'credential_management_forbidden',
);
});
});