This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user