managarten/services/mana-persona-runner/src/clients/mana-auth-internal.ts
Till JS 546b94d472 feat(personas): move admin + internal endpoints from mana-auth to apps/api
Schließt die platform/product-split-Lücke: HEAD's apps/api/src/index.ts
referenziert seit dem Forms-M10d-Commit personasInternalRoutes /
personasAdminRoutes — die Implementierung lag aber noch nicht im Repo.
Build war strukturell broken bis hierhin.

Was wandert von mana-auth nach apps/api:

  apps/api/src/modules/personas/
    ├── schema.ts          — pgSchema('personas') mit personas /
    │                        persona_actions / persona_feedback;
    │                        userId ist plain text (Cross-DB-FK auf
    │                        mana-auth's auth.users geht nach Split nicht).
    ├── internal-routes.ts — service-key gated GET /due, POST /:id/actions
    │                        und POST /:id/feedback. Append-only +
    │                        idempotent über deterministische row-ids
    │                        (tickId-i-tool / tickId-module).
    └── admin-routes.ts    — admin-JWT gated CRUD; ruft mana-auth via
                             /api/v1/admin/users + /api/v1/auth/register
                             + /api/v1/internal/users/:id/persona-stamp
                             für den User-Lifecycle.

Persona-runner-Client zeigt jetzt auf apps/api:

  - config.ts: neues apiUrl-Feld (default http://localhost:3060,
    Env MANA_API_URL); authUrl bleibt für /api/v1/auth/login + spaces.
  - clients/mana-auth-internal.ts: drei Calls treffen jetzt
    /api/v1/personas/internal/* statt mana-auth's
    /api/v1/internal/personas/* — Datei-Name bleibt um Call-Site-Diff
    klein zu halten.
  - index.ts: ManaAuthInternalClient bekommt config.apiUrl statt authUrl.

Seed/Cleanup-Skripte:

  - --api= als bevorzugter Flag, --auth= als Legacy-Alias (cached
    Shell-History würde sonst hart brechen).
  - default http://localhost:3060, Env MANA_API_URL.
  - Endpoint-Pfade umgeschrieben:
      POST   /api/v1/admin/personas        → /api/v1/personas/admin
      DELETE /api/v1/admin/personas/:id    → /api/v1/personas/admin/:id

drizzle.config.ts: schema-Array + schemaFilter um 'personas' erweitert.
DB-push ist Pflicht-Schritt vor erstem Boot, sonst 42P01 auf /due.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:38:29 +02:00

76 lines
2.3 KiB
TypeScript

/**
* Service-to-service client for the personas internal endpoints.
*
* Three calls: list due personas, post actions batch, post feedback
* batch. All gated by `X-Service-Key` (not a user JWT).
*
* After the platform/product split, personas live in apps/api
* (`mana-monorepo/apps/api`), not in mana-auth. The constructor takes
* the apps/api URL — the file name stays the same to keep the
* callsite diff small (one import path), but the destination changed.
*/
import type { ActionRow, FeedbackRow } from '../runner/types.ts';
export interface DuePersona {
userId: string;
email: string;
archetype: string;
systemPrompt: string;
moduleMix: Record<string, number>;
tickCadence: 'daily' | 'weekdays' | 'hourly';
lastActiveAt: string | null;
}
export class ManaAuthInternalClient {
constructor(
private readonly apiUrl: string,
private readonly serviceKey: string
) {
if (!serviceKey) {
throw new Error('ManaAuthInternalClient: serviceKey is required (MANA_SERVICE_KEY)');
}
}
private headers(): Record<string, string> {
return {
'content-type': 'application/json',
'x-service-key': this.serviceKey,
};
}
async listDuePersonas(): Promise<DuePersona[]> {
const res = await fetch(`${this.apiUrl}/api/v1/personas/internal/due`, {
headers: this.headers(),
});
if (!res.ok) {
throw new Error(`listDuePersonas failed: HTTP ${res.status}${await res.text()}`);
}
const body = (await res.json()) as { personas: DuePersona[] };
return body.personas;
}
async postActions(personaId: string, actions: ActionRow[]): Promise<void> {
if (actions.length === 0) return;
const res = await fetch(`${this.apiUrl}/api/v1/personas/internal/${personaId}/actions`, {
method: 'POST',
headers: this.headers(),
body: JSON.stringify({ actions }),
});
if (!res.ok) {
throw new Error(`postActions failed: HTTP ${res.status}${await res.text()}`);
}
}
async postFeedback(personaId: string, feedback: FeedbackRow[]): Promise<void> {
if (feedback.length === 0) return;
const res = await fetch(`${this.apiUrl}/api/v1/personas/internal/${personaId}/feedback`, {
method: 'POST',
headers: this.headers(),
body: JSON.stringify({ feedback }),
});
if (!res.ok) {
throw new Error(`postFeedback failed: HTTP ${res.status}${await res.text()}`);
}
}
}