cards/docs/marketplace/archive/code/index.ts
Till JS 7dbbf63523 Phase 12 R2: Marketplace-Backend α + β — Authors + Deck-Init + Publish
Routes (additiv unter /api/v1/marketplace/*):
- POST/GET /authors/me — eigenes Author-Profil anlegen/updaten/lesen
- GET /authors/:slug — public Profile-Lookup (banned-reason gestrippt)
- POST /decks — Deck-Init (Slug-Validation + Pflicht-Author-Profil +
  CHECK auf paid + Pro-License)
- POST /decks/:slug/publish — Versions-Snapshot mit per-Karte
  cardContentHash aus @cards/domain, per-Version-Hash, AI-Mod-Stub-Log,
  atomarer latest_version_id-Bump in Drizzle-Transaction
- PATCH /decks/:slug — Metadaten-Update (Owner-Only)
- GET /decks/:slug — Public-Detail mit optional-auth-Middleware

Geport aus cards-decommission-base:services/cards-server/, mit
Greenfield-Anpassungen:
- Hashing über @cards/domain.cardContentHash (gemeinsame SoT
  zwischen privatem cards.cards und marketplace.deck_cards), per-
  Version-Hash als SHA-256 über sortierte Karten-Hashes mit Ord-Prefix
- AI-Moderation als R2-Stub (pass+rationale+model='stub'),
  echte mana-llm-Anbindung in späterer Welle
- Auth-Middleware-Shape an Greenfield (userId/tier/authMode in
  c.get(...) statt user-Object), optional-auth als Schwester für
  anonymen Public-Read
- Hono-typing: outer Marketplace-Decks-Router ist Partial<AuthVars>
  weil Public-GET kein JWT braucht; Auth-Subroute ist strict

Lese-Referenz:
- 3331 LOC altes cards-server-Code (routes, services, middleware,
  lib) unter docs/marketplace/archive/code/ archiviert. Read-only,
  nicht im Build-Path.

Verifikation:
- 16 neue Vitest-Tests (Slug + Version-Hash), 72 gesamt grün
- type-check 0 errors
- E2E-Smoke gegen lokale cards-api: Cardecky-Author + Deck
  r2-stoische-ethik mit 3 Karten v1.0.0 (basic + basic + cloze),
  per-Karten-Hashes geschrieben, ai_moderation_log-Row da, semver-409
  + paid-422-Errors verifiziert. Smoke-Daten danach aufgeräumt.

Verbleibend für R3+: Discovery (explore + search), Engagement (stars/
subscribe/fork), Smart-Merge mit FSRS-State-Erhalt; danach R4 PRs +
Card-Discussions, R5 Frontend-Routes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:13:58 +02:00

140 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* cards-server — Cards Marketplace + Community backend.
*
* Hono + Bun. Owns published decks, versions, subscriptions, forks,
* pull-requests, discussions, moderation, and the credits-based
* author payout pipeline.
*
* See apps/cards/docs/MARKETPLACE_PLAN.md for the full design.
*/
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { serviceErrorHandler as errorHandler } from '@mana/shared-hono';
import { loadConfig } from './config';
import { getDb } from './db/connection';
import { jwtAuth, type AuthUser } from './middleware/jwt-auth';
import { optionalAuth } from './middleware/optional-auth';
import { healthRoutes } from './routes/health';
import { AuthorService } from './services/authors';
import { DeckService } from './services/decks';
import { ExploreService } from './services/explore';
import { EngagementService } from './services/engagement';
import { SubscriptionService } from './services/subscriptions';
import { PullRequestService } from './services/pull-requests';
import { DiscussionService } from './services/discussions';
import { PurchaseService } from './services/purchases';
import { ModerationService } from './services/moderation';
import { createAuthorRoutes } from './routes/authors';
import { createDeckRoutes } from './routes/decks';
import { createExploreRoutes } from './routes/explore';
import { createEngagementRoutes } from './routes/engagement';
import { createSubscriptionRoutes } from './routes/subscriptions';
import { createPullRequestRoutes } from './routes/pull-requests';
import { createDiscussionRoutes } from './routes/discussions';
import { createPurchaseRoutes } from './routes/purchases';
import { createModerationRoutes } from './routes/moderation';
import { createNotifyClient } from './lib/notify';
import { createCreditsClient } from './lib/credits';
// ─── Bootstrap ──────────────────────────────────────────────
const config = loadConfig();
const db = getDb(config.databaseUrl);
const notify = createNotifyClient({
url: config.manaNotifyUrl,
serviceKey: config.serviceKey,
});
const credits = createCreditsClient({
url: config.manaCreditsUrl,
serviceKey: config.serviceKey,
});
const authorService = new AuthorService(db);
const deckService = new DeckService(db, config.manaLlmUrl);
const exploreService = new ExploreService(db);
const engagementService = new EngagementService(db);
const subscriptionService = new SubscriptionService(db);
const pullRequestService = new PullRequestService(db, notify);
const discussionService = new DiscussionService(db);
const purchaseService = new PurchaseService(
db,
credits,
{
standardAuthorBps: config.authorPayout.standardAuthorBps,
verifiedAuthorBps: config.authorPayout.verifiedAuthorBps,
},
notify
);
const moderationService = new ModerationService(db, notify);
// ─── App ────────────────────────────────────────────────────
const app = new Hono<{ Variables: { user?: AuthUser } }>();
app.onError(errorHandler);
app.use(
'*',
cors({
origin: config.cors.origins,
credentials: true,
})
);
// Health (no auth)
app.route('/health', healthRoutes);
// Versioned API surface — additive-only changes within v1, breaking
// changes go to /v2 (MARKETPLACE_PLAN §3 architecture principle 1).
//
// Two auth tiers:
// - jwtAuth: strict, used on writes (publish, profile updates,
// star/follow). 401 if missing/invalid token.
// - optionalAuth: opportunistic, used on every read. Sets
// c.get('user') if a token validates, otherwise leaves it
// undefined and lets the route serve anonymous content.
const v1 = new Hono<{ Variables: { user?: AuthUser } }>();
// Phase γ: public reads first — explore + browse + tags + author
// profile lookup + deck profile lookup. All read-only, no token
// required, but a present token enables logged-in extras (star
// state, follow state) once those flags land in the responses
// (MARKETPLACE_PLAN phase γ.3).
v1.use('/*', optionalAuth(config.manaAuthUrl));
// Mounted routers handle their own per-route auth requirements
// via requireUser() helpers when needed.
v1.route('/', createExploreRoutes(exploreService));
v1.route('/', createEngagementRoutes(engagementService));
v1.route('/', createSubscriptionRoutes(subscriptionService));
v1.route('/', createPullRequestRoutes(pullRequestService));
v1.route('/', createDiscussionRoutes(discussionService));
v1.route('/', createPurchaseRoutes(purchaseService));
v1.route('/', createModerationRoutes(moderationService));
v1.route('/authors', createAuthorRoutes(authorService));
v1.route('/decks', createDeckRoutes(authorService, deckService, purchaseService));
v1.get('/', (c) =>
c.json({
service: 'cards-server',
version: 1,
message: 'See apps/cards/docs/MARKETPLACE_PLAN.md for the full plan.',
})
);
app.route('/v1', v1);
// Keep jwtAuth around — re-exported for callers that need to wrap
// individual mutating subroutes by hand. Not currently used at the
// app-level since we moved to optionalAuth + requireUser per route.
void jwtAuth;
// ─── Listen ────────────────────────────────────────────────
console.log(`[cards-server] listening on :${config.port}`);
export default {
port: config.port,
fetch: app.fetch,
};