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>
140 lines
5.4 KiB
TypeScript
140 lines
5.4 KiB
TypeScript
/**
|
||
* 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,
|
||
};
|