@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>
130 lines
3.6 KiB
TypeScript
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;
|