feat(shared-hono): add credit client for Hono backends

Add credits.ts to @manacore/shared-hono as replacement for
CreditClientService from @mana-core/nestjs-integration.

Exports: getBalance, validateCredits, consumeCredits, refundCredits
Calls mana-credits service via MANA_CREDITS_URL + X-Service-Key.

Same API surface as the NestJS version but as pure functions
instead of an @Injectable() service class.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-28 10:13:34 +01:00
parent 87d7966b0f
commit 3fca0de680
3 changed files with 129 additions and 1 deletions

View file

@ -12,7 +12,8 @@
"./db": "./src/db.ts",
"./health": "./src/health.ts",
"./admin": "./src/admin.ts",
"./error": "./src/error.ts"
"./error": "./src/error.ts",
"./credits": "./src/credits.ts"
},
"scripts": {
"type-check": "tsc --noEmit"

View file

@ -0,0 +1,125 @@
/**
* Credit client for Hono backends.
*
* Drop-in replacement for @mana-core/nestjs-integration CreditClientService.
* Calls mana-credits service to validate/consume/refund credits.
*/
export interface CreditBalance {
balance: number;
totalEarned: number;
totalSpent: number;
}
export interface CreditValidationResult {
hasCredits: boolean;
availableCredits: number;
requiredCredits?: number;
}
const CREDITS_URL = () =>
process.env.MANA_CREDITS_URL || process.env.MANA_CORE_AUTH_URL || 'http://localhost:3061';
const SERVICE_KEY = () => process.env.MANA_CORE_SERVICE_KEY || '';
const APP_ID = () => process.env.APP_ID || 'unknown';
const DEFAULT_BALANCE: CreditBalance = { balance: 1000, totalEarned: 0, totalSpent: 0 };
async function callCredits<T>(path: string, options: RequestInit = {}): Promise<T | null> {
const key = SERVICE_KEY();
if (!key) {
console.warn('[credits] Service key not configured');
return null;
}
try {
const res = await fetch(`${CREDITS_URL()}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Service-Key': key,
'X-App-Id': APP_ID(),
...options.headers,
},
});
if (!res.ok) return null;
return res.json();
} catch (error) {
console.error('[credits] Request failed:', error);
return null;
}
}
/**
* Get user's credit balance.
*/
export async function getBalance(userId: string): Promise<CreditBalance> {
const result = await callCredits<CreditBalance>(`/api/v1/internal/credits/balance/${userId}`);
return result || DEFAULT_BALANCE;
}
/**
* Validate that user has enough credits for an operation.
*/
export async function validateCredits(
userId: string,
operation: string,
amount: number
): Promise<CreditValidationResult> {
try {
const balance = await getBalance(userId);
return {
hasCredits: balance.balance >= amount,
availableCredits: balance.balance,
requiredCredits: amount,
};
} catch {
return { hasCredits: true, availableCredits: 0, requiredCredits: amount };
}
}
/**
* Consume credits after a successful operation.
*/
export async function consumeCredits(
userId: string,
operation: string,
amount: number,
description: string,
metadata?: Record<string, unknown>,
creditSource?: { type: 'guild'; guildId: string }
): Promise<boolean> {
const result = await callCredits('/api/v1/internal/credits/use', {
method: 'POST',
body: JSON.stringify({
userId,
amount,
appId: APP_ID(),
description,
metadata: { operation, ...metadata },
...(creditSource && { creditSource }),
}),
});
return !!result;
}
/**
* Refund credits after a failed operation.
*/
export async function refundCredits(
userId: string,
amount: number,
description: string,
metadata?: Record<string, unknown>
): Promise<boolean> {
const result = await callCredits('/api/v1/internal/credits/refund', {
method: 'POST',
body: JSON.stringify({
userId,
amount,
appId: APP_ID(),
description,
metadata,
}),
});
return !!result;
}

View file

@ -38,4 +38,6 @@ export type { DbOptions } from './db';
export { healthRoute } from './health';
export { adminRoutes } from './admin';
export { errorHandler, notFoundHandler } from './error';
export { getBalance, validateCredits, consumeCredits, refundCredits } from './credits';
export type { CreditBalance, CreditValidationResult } from './credits';
export type { CurrentUserData, AuthVariables } from './types';