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>
This commit is contained in:
Till JS 2026-05-08 18:00:56 +02:00
parent 47419b3cac
commit aff4d9536a
8 changed files with 78 additions and 170 deletions

11
.npmrc
View file

@ -1,7 +1,10 @@
# Cards consumes @mana/* shared packages from the Verein's private
# Verdaccio registry (pkg.mana.how on Mac Mini).
# Local dev: run `npm login --registry=https://pkg.mana.how` once;
# Verdaccio registry (npm.mana.how — Mac Mini, Container mana-verdaccio
# auf :4873). Stand 2026-05-08: pkg.mana.how-Tunnel ist Lame-Duck und
# antwortet 404, npm.mana.how ist die produktive Route.
#
# Local dev: run `npm login --registry=https://npm.mana.how` once;
# the resulting ~/.npmrc token is read via $NPM_AUTH_TOKEN substitution.
# Production CI: set NPM_AUTH_TOKEN in workflow secrets.
@mana:registry=https://pkg.mana.how/
//pkg.mana.how/:_authToken=${NPM_AUTH_TOKEN}
@mana:registry=https://npm.mana.how/
//npm.mana.how/:_authToken=${NPM_AUTH_TOKEN}

View file

@ -32,7 +32,7 @@
"shares": [{ "type": "mana/card", "schema_ref": "/payload-schemas/card.json" }],
"accepts": [
{ "type": "mana/quote", "handler": "create_card_from_quote" },
{ "type": "mana/url", "handler": "save_link_as_card" },
{ "type": "mana/link", "handler": "save_link_as_card" },
{ "type": "mana/text", "handler": "create_card_from_text" }
],
"tools": [

View file

@ -1,5 +1,5 @@
import { newReview, subIndexCount } from '@cards/domain';
import type { QuotePayload, TextPayload, UrlPayload } 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';
@ -89,13 +89,13 @@ export async function createCardFromQuote(
}
/**
* mana/url Karte. Front = Titel (oder URL falls kein Titel),
* mana/link Karte. Front = Titel (oder URL falls kein Titel),
* Back = URL + Description/Snippet.
*/
export async function saveLinkAsCard(
db: CardsDb,
userId: string,
payload: UrlPayload
payload: LinkPayload
): Promise<HandlerResult> {
const front = payload.title ?? payload.url;
const summary = payload.description ?? payload.snippet ?? '';

View file

@ -20,6 +20,7 @@
"clean": "rm -rf dist .turbo coverage"
},
"dependencies": {
"@mana/shared-share-protocol": "^0.1.0",
"ts-fsrs": "^5.3.2",
"zod": "3"
},

View file

@ -1,69 +1,15 @@
// TEMPORARY MIRROR von @mana/shared-share-protocol/envelope.
// Re-Export der Föderations-Envelope-Schemas aus
// `@mana/shared-share-protocol` (Klasse-A-Vertrag der Föderation).
//
// Solange `pkg.mana.how` für Cards nicht erreichbar ist (NPM_AUTH_TOKEN
// in `~/.npmrc` fehlt), halten wir die Schemas hier lokal. Sobald
// Verdaccio offen ist, wird diese Datei gegen einen Re-Export aus
// `@mana/shared-share-protocol` getauscht — alle Imports sind in der
// Form `import { ShareEnvelopeSchema } from '@cards/domain'` formuliert,
// damit der Swap eine reine 1-Liner-Edit-Aufgabe ist.
//
// Bei jedem Update der mana-Spec muss diese Datei nachgezogen werden,
// bis der Swap erfolgt. Stand: 2026-05-08, ENVELOPE_VERSION 0.1.
// Die Datei war bis 2026-05-08 ein lokaler Mirror, weil das Verdaccio-
// Token noch nicht im Cards-Repo verfügbar war. Mit Sprint 8 (Pre-
// Flight) ist `npm.mana.how` produktiv und wir konsumieren die Quelle
// direkt — kein Drift-Risiko mehr.
import { z } from 'zod';
export const ENVELOPE_VERSION = '0.1' as const;
const ULID_REGEX = /^[0-9A-HJKMNP-TV-Z]{26}$/;
const TYPE_NAME_REGEX = /^mana\/[a-z][a-z0-9-]+$/;
const SignatureSchema = z.object({
algorithm: z.literal('eddsa'),
key_id: z.string().min(1),
signature: z.string().min(1),
});
export const ShareEnvelopeSchema = z.object({
envelope_version: z.literal(ENVELOPE_VERSION),
share_id: z.string().regex(ULID_REGEX, 'must be ULID'),
from: z.object({
app: z.string().min(1),
app_version: z.string().min(1),
user_id: z.string().uuid(),
timestamp: z.string().datetime(),
instance_id: z.string().max(120).optional(),
}),
to: z.object({
app: z.string().min(1),
user_id: z.string().uuid(),
}),
type: z.string().regex(TYPE_NAME_REGEX, 'must be "mana/<kind>"'),
payload: z.unknown(),
source_link: z.string().max(2000).optional(),
user_note: z.string().max(500).optional(),
ttl_seconds: z.number().int().positive().optional(),
intent: z.enum(['user_action', 'automation', 'agent_tool']).default('user_action'),
consent_recorded_at: z.string().datetime(),
signature: SignatureSchema.optional(),
});
export type ShareEnvelope = z.infer<typeof ShareEnvelopeSchema>;
/** Strict-Variante: Cross-User-Shares hart verboten. */
export const ShareEnvelopeStrictSchema = ShareEnvelopeSchema.refine(
(env) => env.from.user_id === env.to.user_id,
{
message: 'cross-user shares forbidden — from.user_id must equal to.user_id',
path: ['to', 'user_id'],
}
);
export function parseEnvelope(raw: unknown) {
return ShareEnvelopeStrictSchema.safeParse(raw);
}
export {
ENVELOPE_VERSION,
ShareEnvelopeSchema,
ShareEnvelopeStrictSchema,
parseEnvelope,
type ShareEnvelope,
} from '@mana/shared-share-protocol';

View file

@ -1,65 +1,38 @@
// TEMPORARY MIRROR — siehe envelope.ts.
// Cards-spezifischer Payload-Adapter über `@mana/shared-share-protocol`.
//
// Payload-Schemas für die `accepts[]` aus dem Cards-Manifest:
// `mana/quote`, `mana/url`, `mana/text`. Andere known types
// (`mana/transcript`, `mana/link`, `mana/note`, etc.) werden wir
// erst ergänzen, wenn Cards sie aktiv akzeptiert.
// Die Format-Schemas (Quote, Link, Text) kommen aus dem Föderations-
// Vertrag — Cards selbst hält nur die Akzeptanz-Liste und einen kleinen
// Validate-Helper, weil das ein Cards-internes Layer ist.
//
// Hinweis: der frühere lokale Mirror nutzte `MANA_TYPE_URL = 'mana/url'`.
// Der Spec-konforme Name ist `mana/link` — beim Pre-Flight-Swap am
// 2026-05-08 wurde das app-manifest.json + Handler-Code mit umgestellt.
import { z } from 'zod';
export {
MANA_TYPE_QUOTE,
MANA_TYPE_LINK,
MANA_TYPE_TEXT,
QuotePayloadSchema,
LinkPayloadSchema,
TextPayloadSchema,
type QuotePayload,
type LinkPayload,
type TextPayload,
} from '@mana/shared-share-protocol';
export const MANA_TYPE_QUOTE = 'mana/quote' as const;
export const MANA_TYPE_URL = 'mana/url' as const;
export const MANA_TYPE_TEXT = 'mana/text' as const;
import {
MANA_TYPE_QUOTE,
MANA_TYPE_LINK,
MANA_TYPE_TEXT,
QuotePayloadSchema,
LinkPayloadSchema,
TextPayloadSchema,
} from '@mana/shared-share-protocol';
export const QuotePayloadSchema = z
.object({
text: z.string().min(1).max(8000),
source: z.string().max(500).optional(),
source_url: z.string().url().optional(),
source_kind: z
.enum(['book', 'article', 'talk', 'conversation', 'transcript', 'link', 'manual', 'other'])
.optional(),
language: z
.string()
.regex(/^[a-z]{2}(-[A-Z]{2})?$/)
.optional(),
tags: z.array(z.string().max(64)).max(50).optional(),
})
.strict();
export type QuotePayload = z.infer<typeof QuotePayloadSchema>;
/**
* `mana/url` externe Web-Adresse mit optionalen OG-Metadaten.
* (Mana-Spec nennt dies `mana/link`. Wir akzeptieren dieselben
* Felder, aber unter unserem deklarierten Type-Namen `mana/url`.)
*/
export const UrlPayloadSchema = z
.object({
url: z.string().url(),
title: z.string().max(500).optional(),
description: z.string().max(2000).optional(),
snippet: z.string().max(2000).optional(),
image_url: z.string().url().optional(),
site_name: z.string().max(200).optional(),
favicon_url: z.string().url().optional(),
})
.strict();
export type UrlPayload = z.infer<typeof UrlPayloadSchema>;
export const TextPayloadSchema = z
.object({
text: z.string().min(1).max(50000),
format: z.enum(['plain', 'markdown', 'html']).default('plain'),
source: z.string().max(500).optional(),
source_url: z.string().url().optional(),
})
.strict();
export type TextPayload = z.infer<typeof TextPayloadSchema>;
/** Map vom Type-String zum Payload-Schema. */
/** Map vom Type-String zum Payload-Schema, beschränkt auf was Cards akzeptiert. */
export const PAYLOAD_SCHEMAS = {
[MANA_TYPE_QUOTE]: QuotePayloadSchema,
[MANA_TYPE_URL]: UrlPayloadSchema,
[MANA_TYPE_LINK]: LinkPayloadSchema,
[MANA_TYPE_TEXT]: TextPayloadSchema,
} as const;
@ -68,7 +41,9 @@ export type AcceptedShareType = keyof typeof PAYLOAD_SCHEMAS;
export function validatePayloadForType(
type: string,
payload: unknown
): { success: true; data: unknown } | { success: false; error: 'unknown_type' | 'invalid_payload'; issues?: string[] } {
):
| { success: true; data: unknown }
| { success: false; error: 'unknown_type' | 'invalid_payload'; issues?: string[] } {
const schema = PAYLOAD_SCHEMAS[type as AcceptedShareType];
if (!schema) return { success: false, error: 'unknown_type' };
const r = schema.safeParse(payload);

View file

@ -1,39 +1,11 @@
// TEMPORARY MIRROR — siehe envelope.ts.
// Re-Export der Search-Result-Envelope-Schemas aus
// `@mana/shared-share-protocol`. Lokaler Mirror seit 2026-05-08 ersetzt
// (siehe envelope.ts für den Kontext).
import { z } from 'zod';
export const SEARCH_ENVELOPE_VERSION = '0.1' as const;
export const SearchHitSchema = z.object({
id: z.string().min(1),
type: z.string().min(1),
title: z.string().min(1).max(300),
snippet: z.string().max(1000).optional(),
link: z.string().min(1),
score: z.number().min(0).max(1),
highlights: z
.array(
z.object({
field: z.string().max(80),
fragment: z.string().max(500),
})
)
.max(10)
.optional(),
meta: z.record(z.string(), z.unknown()).optional(),
created_at: z.string().datetime().optional(),
updated_at: z.string().datetime().optional(),
});
export type SearchHit = z.infer<typeof SearchHitSchema>;
export const SearchResultEnvelopeSchema = z.object({
envelope_version: z.literal(SEARCH_ENVELOPE_VERSION),
query: z.string().min(1).max(500),
app: z.string().min(1),
app_version: z.string().min(1),
results: z.array(SearchHitSchema).max(200),
total: z.number().int().nonnegative(),
partial: z.boolean(),
took_ms: z.number().int().nonnegative(),
});
export type SearchResultEnvelope = z.infer<typeof SearchResultEnvelopeSchema>;
export {
SEARCH_ENVELOPE_VERSION,
SearchHitSchema,
SearchResultEnvelopeSchema,
type SearchHit,
type SearchResultEnvelope,
} from '@mana/shared-share-protocol';

11
pnpm-lock.yaml generated
View file

@ -112,6 +112,9 @@ importers:
packages/cards-domain:
dependencies:
'@mana/shared-share-protocol':
specifier: ^0.1.0
version: 0.1.0
ts-fsrs:
specifier: ^5.3.2
version: 5.3.2
@ -560,6 +563,10 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@mana/shared-share-protocol@0.1.0':
resolution: {integrity: sha512-I1fIDbS3nu++9LUXc08ICrLXE/cdV/n9D0Jm8LOhVH9izUXQSSg2EO4M2+m7K5vc5KdjGBcYrFPhAg48+KE6Kw==}
hasBin: true
'@petamoriken/float16@3.9.3':
resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==}
@ -1868,6 +1875,10 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@mana/shared-share-protocol@0.1.0':
dependencies:
zod: 3.25.76
'@petamoriken/float16@3.9.3': {}
'@polka/url@1.0.0-next.29': {}