mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 00:21:26 +02:00
Some checks are pending
CD Mac Mini / Detect Changes (push) Waiting to run
CD Mac Mini / Deploy (push) Blocked by required conditions
CI / Detect Changes (push) Waiting to run
CI / Validate (push) Waiting to run
CI / Build mana-search (push) Blocked by required conditions
CI / Build mana-sync (push) Blocked by required conditions
CI / Build mana-api-gateway (push) Blocked by required conditions
CI / Build mana-crawler (push) Blocked by required conditions
Docker Validate / Validate Dockerfiles (push) Waiting to run
Docker Validate / Build calendar-web (push) Blocked by required conditions
Docker Validate / Build quotes-web (push) Blocked by required conditions
Docker Validate / Build todo-backend (push) Blocked by required conditions
Docker Validate / Build todo-web (push) Blocked by required conditions
Docker Validate / Build mana-auth (push) Blocked by required conditions
Docker Validate / Build mana-sync (push) Blocked by required conditions
Docker Validate / Build mana-media (push) Blocked by required conditions
Mirror to Forgejo / Push to Forgejo (push) Waiting to run
Comic-Surface ist nach Comicello (comicello.mana.how + comicello.com)
umgezogen. Comicello hat seit Phase ω-2 (2026-05-18) Feature-Parität
+ Cross-Module-Hooks via mana-share:
- Character-Variant-Render mit pinned-Variant-Anker
- Panel-Render via mana-image-edits
- Panel-Edit/Delete/Reorder
- POST /api/v1/share/receive für externe Apps (Journal/Notes/Calendar/
Library/Writing können Text-Snippet rüberschicken, Story wird als
Draft angelegt)
- /comics/new?text=…&sourceModule=… URL-Prefill als Alternative zum
Server-Roundtrip
Gelöscht / abgebaut:
- Module: apps/mana/.../modules/comic + Routen + Locales
- Backend: apps/api/src/modules/comic, picture/routes
verifyMediaOwnership-Allowlist auf nur `['me']` reduziert (comic-
Tag war hier Identity-Anchor-Quelle für panel renders, jetzt
Comicello-intern)
- shared-branding: APP_ICONS.comic + MANA_APPS comic-Entry
- shared-ai/tools/schemas: ganzer Comic-Block (list_comic_stories,
create_comic_story, generate_comic_panel, list_comic_characters,
create_comic_character, generate_character_variant,
pin_character_variant)
- shared-ai/agents/templates: comic-author.ts + index.ts Eintrag
- mana-tool-registry: modules/comic.ts + types ModuleId 'comic' raus
- Cross-Module: website/embeds resolveComicStories +
LocalComicStory-Import + 'comic.stories' EmbedSource + Inspector-
Option; crypto-registry comicStories+comicCharacters; exposed-records
comic-Eintrag
- picture/types: comicStoryId, comicPanelIndex, comicCharacterId
Back-Ref-Felder (sowohl LocalImage als auch Image-Public-DTO);
picture/queries to-Public-Mapping
- Registries: app-registry/apps.ts (Comic registerApp + FilmStrip-
Icon + Header), categories, help-content, module-registry,
data/tools/init
- i18n: comic in apps/{de,en,es,fr,it}.json
Was BLEIBT:
- cloudflared `comicello.mana.how` + `comicello-api.mana.how` —
Standalone-Routes
- docker-compose mana-auth CORS_ORIGINS comicello.com + .mana.how —
SSO für Standalone
Dexie v66:
- droppt comicStories + comicCharacters
- Upgrade-Callback strippt comicStoryId/comicPanelIndex/comicCharacterId
aus existierenden Image-Rows (waren nie indiziert, nur Properties)
mana-web svelte-check 0/0 (7281 files), snapshot test 10/10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
4.1 KiB
TypeScript
124 lines
4.1 KiB
TypeScript
/**
|
|
* Shared media helper — routes image uploads through mana-media
|
|
* for CAS deduplication, thumbnail generation, and Photos gallery visibility.
|
|
*/
|
|
|
|
import { MediaClient, type MediaResult } from '@mana/media-client';
|
|
|
|
const MEDIA_URL = process.env.MANA_MEDIA_URL || 'http://localhost:3015';
|
|
let client: MediaClient | null = null;
|
|
|
|
function getMediaClient(): MediaClient {
|
|
if (!client) client = new MediaClient(MEDIA_URL);
|
|
return client;
|
|
}
|
|
|
|
export async function uploadImageToMedia(
|
|
buffer: ArrayBuffer,
|
|
filename: string,
|
|
options: { app: string; userId: string }
|
|
): Promise<MediaResult> {
|
|
return getMediaClient().upload(buffer, {
|
|
app: options.app,
|
|
userId: options.userId,
|
|
filename,
|
|
});
|
|
}
|
|
|
|
export function getMediaUrls(mediaId: string) {
|
|
const c = getMediaClient();
|
|
return {
|
|
original: c.getOriginalUrl(mediaId),
|
|
thumbnail: c.getThumbnailUrl(mediaId),
|
|
medium: c.getMediumUrl(mediaId),
|
|
large: c.getLargeUrl(mediaId),
|
|
};
|
|
}
|
|
|
|
export function isImageMimeType(mimeType: string): boolean {
|
|
return mimeType.startsWith('image/') && mimeType !== 'image/svg+xml';
|
|
}
|
|
|
|
/**
|
|
* Download a media file by id. The mana-media `/file` route is CDN-style
|
|
* public — no auth on the URL itself — so this is a plain fetch. Callers
|
|
* that need to gate on ownership MUST call `verifyMediaOwnership` first.
|
|
*/
|
|
export async function getMediaBuffer(
|
|
mediaId: string
|
|
): Promise<{ buffer: ArrayBuffer; mimeType: string }> {
|
|
const url = getMediaClient().getOriginalUrl(mediaId);
|
|
const res = await fetch(url);
|
|
if (!res.ok) {
|
|
throw new Error(`mana-media fetch failed for ${mediaId}: HTTP ${res.status}`);
|
|
}
|
|
const mimeType = res.headers.get('content-type') ?? 'application/octet-stream';
|
|
const buffer = await res.arrayBuffer();
|
|
return { buffer, mimeType };
|
|
}
|
|
|
|
/**
|
|
* Download a media file normalized to plain RGB PNG, max `longestSide`
|
|
* pixels on its longer edge (default 1024). Uses mana-media's `/transform`
|
|
* endpoint, which pipes the original through `sharp` server-side — that
|
|
* handles HEIC from iPhones, palette-mode PNGs, CMYK JPEGs, weird color
|
|
* profiles, and other formats OpenAI's gpt-image-1 rejects with
|
|
* `invalid_image_file` or `Invalid image file or mode`.
|
|
*
|
|
* `fit=inside` preserves aspect ratio (no distortion on portrait/landscape
|
|
* refs) and only caps the longer side, which keeps payloads comfortably
|
|
* under OpenAI's 4 MB/image limit without losing reference fidelity.
|
|
*/
|
|
export async function getMediaBufferAsPng(
|
|
mediaId: string,
|
|
longestSide = 1024
|
|
): Promise<{ buffer: ArrayBuffer; mimeType: 'image/png' }> {
|
|
const base = getMediaClient()
|
|
.getOriginalUrl(mediaId)
|
|
.replace(/\/file$/, '/transform');
|
|
const url = `${base}?format=png&w=${longestSide}&h=${longestSide}&fit=inside`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) {
|
|
throw new Error(`mana-media transform failed for ${mediaId}: HTTP ${res.status}`);
|
|
}
|
|
const buffer = await res.arrayBuffer();
|
|
return { buffer, mimeType: 'image/png' };
|
|
}
|
|
|
|
/**
|
|
* Verify that every id in `mediaIds` is owned by `userId` under one of
|
|
* the given app scopes. Throws `{ status: 404, missing }` when any id
|
|
* doesn't land in the owned set — the caller turns that into an HTTP
|
|
* response.
|
|
*
|
|
* Accepts a single app string or an array. `['me']` covers the
|
|
* portrait flow; future apps may extend the list with their own
|
|
* upload tags.
|
|
*
|
|
* One `list()` round-trip per app. For N apps this is N calls, each
|
|
* capped at 500 rows — far beyond the product's intended per-app shape
|
|
* but the cap is the sanity fence.
|
|
*/
|
|
export async function verifyMediaOwnership(
|
|
userId: string,
|
|
mediaIds: readonly string[],
|
|
apps: string | readonly string[]
|
|
): Promise<void> {
|
|
if (mediaIds.length === 0) return;
|
|
const appList = typeof apps === 'string' ? [apps] : apps;
|
|
const ownedSet = new Set<string>();
|
|
for (const app of appList) {
|
|
const list = await getMediaClient().list({ userId, app, limit: 500 });
|
|
for (const m of list) ownedSet.add(m.id);
|
|
}
|
|
const missing = mediaIds.filter((id) => !ownedSet.has(id));
|
|
if (missing.length > 0) {
|
|
const err = new Error(`Reference media not owned: ${missing.join(', ')}`) as Error & {
|
|
status?: number;
|
|
missing?: string[];
|
|
};
|
|
err.status = 404;
|
|
err.missing = missing;
|
|
throw err;
|
|
}
|
|
}
|