wordeck/apps/api/src/share-handlers/index.ts
Till JS aff4d9536a Phase 9d: Pre-Flight — Protocol-Mirror durch upstream ersetzt
@mana/shared-share-protocol@0.1.0 ist jetzt installierbar (NPM_AUTH_TOKEN
aus claudebot-Verdaccio-Account). Lokaler protocol/-Mirror zeigt jetzt
auf upstream:

- envelope.ts → Re-Export von ShareEnvelopeSchema/Strict, parseEnvelope,
  ENVELOPE_VERSION, ShareEnvelope
- search.ts → Re-Export von SearchHitSchema, SearchResultEnvelopeSchema,
  SEARCH_ENVELOPE_VERSION, SearchHit, SearchResultEnvelope
- payloads.ts → Re-Export der Format-Schemas (Quote/Link/Text);
  Cards-spezifische PAYLOAD_SCHEMAS / validatePayloadForType bleiben
  lokal (Akzeptanz-Liste ist Cards-Layer, nicht Föderation)

Spec-Drift gefixt: der frühere Mirror nutzte MANA_TYPE_URL = 'mana/url',
upstream definiert MANA_TYPE_LINK = 'mana/link'. app-manifest.json,
share-handlers (UrlPayload → LinkPayload, "mana/url" → "mana/link")
und Doku-Kommentare auf den Spec-konformen Namen umgestellt.

DNS-Korrektur in Repo-.npmrc: pkg.mana.how-Tunnel ist Lame-Duck (404),
npm.mana.how ist die produktive Verdaccio-Route nach 2026-05-07-Re-Deploy.
~/.npmrc bleibt unangetastet — Anpassung ist user-side.

Tests + svelte-check 0 errors, 92 Tests grün (41 Domain + 46 API + 5
Web), prod-Build sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:00:56 +02:00

130 lines
3.6 KiB
TypeScript

import { newReview, subIndexCount } from '@cards/domain';
import type { QuotePayload, TextPayload, LinkPayload } from '@cards/domain';
import type { CardsDb } from '../db/connection.ts';
import { cards, reviews } from '../db/schema/index.ts';
import { ensureInboxDeck } from '../lib/inbox-deck.ts';
import { ulid } from '../lib/ulid.ts';
/**
* Antwort-Shape eines Share-Handlers — wird vom Receive-Endpoint
* an den sendenden Peer zurückgegeben.
*/
export type HandlerResult = {
target_link: string;
resulting_id: string;
};
const APP_BASE_URL = process.env.CARDS_PUBLIC_URL ?? 'https://cardecky.mana.how';
/**
* Legt eine basic-Karte mit (front,back) im Inbox-Deck an, inkl.
* der ts-fsrs-Initial-Reviews. Gibt Karte-ID + Deep-Link zurück.
*/
async function persistCardInInbox(
db: CardsDb,
userId: string,
front: string,
back: string
): Promise<HandlerResult> {
const inbox = await ensureInboxDeck(db, userId);
const cardId = ulid();
const now = new Date();
await db.transaction(async (tx) => {
await tx.insert(cards).values({
id: cardId,
deckId: inbox.id,
userId,
type: 'basic',
fields: { front, back },
mediaRefs: [],
createdAt: now,
updatedAt: now,
});
const initial = Array.from({ length: subIndexCount('basic') }, (_, i) => i).map(
(subIndex) => {
const r = newReview({ userId, cardId, subIndex, now });
return {
cardId: r.card_id,
subIndex: r.sub_index,
userId: r.user_id,
due: new Date(r.due),
stability: r.stability,
difficulty: r.difficulty,
elapsedDays: r.elapsed_days,
scheduledDays: r.scheduled_days,
learningSteps: r.learning_steps,
reps: r.reps,
lapses: r.lapses,
state: r.state,
lastReview: r.last_review ? new Date(r.last_review) : null,
};
}
);
if (initial.length > 0) {
await tx.insert(reviews).values(initial);
}
});
return {
target_link: `${APP_BASE_URL}/c/${cardId}`,
resulting_id: cardId,
};
}
/**
* mana/quote → Karte. Front = Zitat-Text, Back = Quelle (URL bevorzugt,
* sonst Source-String, sonst leer).
*/
export async function createCardFromQuote(
db: CardsDb,
userId: string,
payload: QuotePayload
): Promise<HandlerResult> {
const front = payload.text;
const back = payload.source_url ?? payload.source ?? '';
return persistCardInInbox(db, userId, front, back);
}
/**
* mana/link → Karte. Front = Titel (oder URL falls kein Titel),
* Back = URL + Description/Snippet.
*/
export async function saveLinkAsCard(
db: CardsDb,
userId: string,
payload: LinkPayload
): Promise<HandlerResult> {
const front = payload.title ?? payload.url;
const summary = payload.description ?? payload.snippet ?? '';
const back = summary ? `${payload.url}\n\n${summary}` : payload.url;
return persistCardInInbox(db, userId, front, back);
}
/**
* mana/text → Karte. Front = erste Zeile (oder erste 200 Zeichen),
* Back = Rest. Heuristisch — User kann später trennen.
*/
export async function createCardFromText(
db: CardsDb,
userId: string,
payload: TextPayload
): Promise<HandlerResult> {
const text = payload.text.trim();
const firstNl = text.indexOf('\n');
const frontEnd = firstNl > 0 && firstNl < 200 ? firstNl : Math.min(200, text.length);
const front = text.slice(0, frontEnd).trim();
const back = text.slice(frontEnd).trim();
return persistCardInInbox(db, userId, front, back || '(weiter ausarbeiten)');
}
/** Manifest-Handler-Map. Key = `accepts[].handler` aus app-manifest.json. */
export const SHARE_HANDLERS = {
create_card_from_quote: createCardFromQuote,
save_link_as_card: saveLinkAsCard,
create_card_from_text: createCardFromText,
} as const;
export type ShareHandlerName = keyof typeof SHARE_HANDLERS;