shared-uload: HTTP-federation (Option B) — schreibt jetzt gegen uload-api
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

Nach dem uLoad-Cutover 2026-05-18 (Code/uload/ als Standalone) war
@mana/shared-uload structurell broken: ShareModal-Calls aus presi+
music landeten in mana_sync.sync_changes, aber der alte Konsument
(mana-app-uload-server) ist abgeschaltet → 404 auf ulo.ad/r/<code>.

Fix: shared-uload schreibt jetzt direkt via HTTP gegen die föderierte
uload-API.

- create-link.ts: createShortLink() → POST {apiUrl}/api/v1/links
  mit Authorization: Bearer <token>. Init-Signatur ist neu
  initSharedUload({ apiUrl, getAuthToken, shortUrlOrigin? }).
- types.ts: UloadLink (Dexie-internal-Type) entfernt — Caller arbeiten
  nur noch mit CreateShortLinkOptions + CreatedLink (Wire-Shapes).
- package.json: @mana/local-store-Dep entfernt. Version 0.2.0.
- index.ts: getBaseUrl-Export ergänzt, UloadLink raus.

Caller-Site (apps/mana/apps/web/src/routes/(app)/+layout.svelte):
  initSharedUload({
    apiUrl: PUBLIC_ULOAD_API_URL ?? 'https://uload-api.mana.how',
    getAuthToken: () => authStore.getValidToken(),
    shortUrlOrigin: PUBLIC_ULOAD_SHORT_ORIGIN ?? 'https://ulo.ad',
  });

Bonus-Cleanup:
- plaintext-allowlist.ts: uloadFolders + uloadTags raus (Tables sind
  via Dexie v67 gedroppt, Allowlist-Entries waren orphaned).

mana-Web-App: pnpm check grün (0/0 auf 7396 Files).
This commit is contained in:
Till JS 2026-05-18 16:39:44 +02:00
parent 0b44acdde1
commit e9e43abaa0
7 changed files with 412 additions and 3996 deletions

View file

@ -105,8 +105,6 @@ export const PLAINTEXT_ALLOWLIST: readonly string[] = [
'timeTemplates', // TODO: audit
'timeWorldClocks', // TODO: audit
'todoProjects', // TODO: audit
'uloadFolders', // TODO: audit
'uloadTags', // TODO: audit
'userSettings', // TODO: audit
'wetterLocations', // TODO: audit
'wetterSettings', // TODO: audit

View file

@ -573,7 +573,14 @@
// lets the first paint + interaction land without waiting on
// event-bridge wiring or LLM-queue reclaim work.
idle(() => {
initSharedUload();
// uload-Federation: schreibt direkt gegen die föderierte
// uload-API (Code/uload/), nicht mehr in lokale Dexie+mana-sync.
// Override URL via PUBLIC_ULOAD_API_URL (dev: http://localhost:3107).
initSharedUload({
apiUrl: import.meta.env.PUBLIC_ULOAD_API_URL ?? 'https://uload-api.mana.how',
getAuthToken: () => authStore.getValidToken(),
shortUrlOrigin: import.meta.env.PUBLIC_ULOAD_SHORT_ORIGIN ?? 'https://ulo.ad',
});
startEventStore();
initTools();
startEventBridge();

View file

@ -1,6 +1,6 @@
{
"name": "@mana/shared-uload",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"type": "module",
"svelte": "./src/index.ts",
@ -17,7 +17,6 @@
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"dependencies": {
"@mana/local-store": "workspace:*",
"@mana/shared-icons": "workspace:*"
},
"devDependencies": {

View file

@ -1,107 +1,126 @@
import { createLocalStore, type LocalCollection } from '@mana/local-store';
import type { UloadLink, CreateShortLinkOptions, CreatedLink } from './types';
import { generateShortCode, getShortUrl, getQrCodeUrl } from './utils';
let _linkCollection: LocalCollection<UloadLink> | null = null;
let _store: ReturnType<typeof createLocalStore> | null = null;
let _baseUrl: string | undefined;
let _initPromise: Promise<void> | null = null;
/**
* Initialize the shared uLoad utility.
* Cross-App-Share-via-uLoad HTTP-Client (Federation-Mode).
*
* Option A: Pass an existing linkCollection (from inside uLoad's own app).
* Option B: Call with no collection it opens the uLoad IndexedDB directly (from any other app).
* Bis 2026-05-18 schrieb diese Lib in eine eigene Dexie-DB mit
* `appId: 'uload'` und liess `mana-sync` die Records in
* `mana_sync.sync_changes` schieben. Seit der uLoad-Cutover-Migration
* (Code/uload/ als eigenständiges Hono+Bun-Repo + eigene `mana_uload`-
* Postgres) ist `mana_sync` kein Konsument mehr wir schreiben jetzt
* direkt über HTTP gegen die föderierte uload-API.
*
* **Init:** Caller muss `initSharedUload({ apiUrl, getAuthToken })`
* aufrufen, **bevor** `createShortLink` benutzbar ist.
*
* - `apiUrl`: Origin der uload-API (`https://uload-api.mana.how`
* in prod, `http://localhost:3107` lokal).
* - `getAuthToken`: Async-Getter, der einen aktuellen Bearer-Token
* liefert (in der mana-Web-App via `authStore.getValidToken()`).
*
* Backwards-Compat: das alte Init-Pattern (mit `linkCollection`-
* Argument) ist **entfernt**. Wer noch die Dexie-Variante braucht,
* muss eigenen Code schreiben die uLoad-Plattform-Dexie existiert
* nicht mehr.
*/
export function initSharedUload(
linkCollectionOrOptions?: LocalCollection<UloadLink> | { baseUrl?: string },
options?: { baseUrl?: string }
): void {
if (linkCollectionOrOptions && 'insert' in linkCollectionOrOptions) {
// Option A: Existing collection
_linkCollection = linkCollectionOrOptions;
_baseUrl = options?.baseUrl;
} else {
// Option B: Self-initialize by opening the uLoad database
const opts = linkCollectionOrOptions as { baseUrl?: string } | undefined;
_baseUrl = opts?.baseUrl;
_store = createLocalStore({
appId: 'uload',
collections: [
{
name: 'links',
indexes: [
'shortCode',
'isActive',
'folderId',
'order',
'clickCount',
'source',
'[folderId+order]',
'[isActive+order]',
],
},
],
});
import type { CreateShortLinkOptions, CreatedLink } from './types';
import { getQrCodeUrl, getShortUrl } from './utils';
_initPromise = _store.initialize().then(() => {
_linkCollection = _store!.collection<UloadLink>('links');
});
}
interface SharedUloadConfig {
/** Origin der uload-API, z.B. `https://uload-api.mana.how`. */
apiUrl: string;
/** Bearer-Token-Getter; null/undefined → kein Authorization-Header gesetzt → 401. */
getAuthToken: () => Promise<string | null> | string | null;
/** Optional: Kurz-Domain für getShortUrl-Mapping, default `https://ulo.ad`. */
shortUrlOrigin?: string;
}
async function ensureReady(): Promise<LocalCollection<UloadLink>> {
if (_initPromise) {
await _initPromise;
}
if (!_linkCollection) {
throw new Error(
'@mana/shared-uload not initialized. Call initSharedUload() in your app layout.'
);
}
return _linkCollection;
let _config: SharedUloadConfig | null = null;
export function initSharedUload(config: SharedUloadConfig): void {
_config = {
...config,
apiUrl: config.apiUrl.replace(/\/$/, ''),
shortUrlOrigin: config.shortUrlOrigin ?? 'https://ulo.ad',
};
}
/**
* Create a short link from any app.
* The link is inserted into the uLoad local-store and syncs automatically.
*/
export async function createShortLink(options: CreateShortLinkOptions): Promise<CreatedLink> {
const collection = await ensureReady();
const shortCode = options.customCode || generateShortCode();
const id = crypto.randomUUID();
const shortUrl = getShortUrl(shortCode, _baseUrl);
const qrCodeUrl = getQrCodeUrl(shortUrl);
await collection.insert({
id,
shortCode,
customCode: options.customCode || null,
originalUrl: options.url,
title: options.title || null,
description: options.description || null,
isActive: true,
clickCount: 0,
folderId: null,
order: 0,
source: options.source,
expiresAt: options.expiresAt || null,
password: options.password || null,
qrCodeUrl,
} as UloadLink);
return { id, shortCode, shortUrl, qrCodeUrl };
}
/**
* Check if shared-uload has been initialized.
*/
export function isSharedUloadReady(): boolean {
return _linkCollection !== null || _initPromise !== null;
return _config !== null;
}
export function getBaseUrl(): string | undefined {
return _baseUrl;
return _config?.shortUrlOrigin;
}
/**
* Erzeugt einen Kurzlink über die uload-API.
*
* Wirft, wenn `initSharedUload` nicht aufgerufen wurde, oder wenn die
* API einen Fehler liefert (z.B. 401 ohne Token, 409 bei Slug-Kollision,
* 400 bei invalidem Body).
*/
export async function createShortLink(options: CreateShortLinkOptions): Promise<CreatedLink> {
if (!_config) {
throw new Error(
'@mana/shared-uload not initialized. Call initSharedUload({ apiUrl, getAuthToken }) in your app layout.'
);
}
const token = await _config.getAuthToken();
const body: Record<string, unknown> = {
originalUrl: options.url,
};
if (options.title) body.title = options.title;
if (options.description) body.description = options.description;
if (options.customCode) body.customCode = options.customCode;
if (options.expiresAt) body.expiresAt = options.expiresAt;
if (options.password) body.password = options.password;
if (options.source) body.source = options.source;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${_config.apiUrl}/api/v1/links`, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
if (!res.ok) {
let detail = '';
try {
const data = (await res.json()) as { error?: string; details?: unknown };
detail = data.error
? `${data.error}${data.details ? `: ${JSON.stringify(data.details)}` : ''}`
: '';
} catch {
/* swallow */
}
if (res.status === 401) {
throw new Error(detail || 'uload-api: nicht authentifiziert (Login abgelaufen?)');
}
if (res.status === 409) {
throw new Error(detail || `uload-api: Short-Code „${options.customCode}" ist vergeben`);
}
throw new Error(detail || `uload-api: HTTP ${res.status}`);
}
const link = (await res.json()) as {
id: string;
shortCode: string;
qrCodeUrl: string | null;
};
const shortUrl = getShortUrl(link.shortCode, _config.shortUrlOrigin);
const qrCodeUrl = link.qrCodeUrl ?? getQrCodeUrl(shortUrl);
return {
id: link.id,
shortCode: link.shortCode,
shortUrl,
qrCodeUrl,
};
}

View file

@ -1,9 +1,9 @@
// Types
export type { UloadLink, CreateShortLinkOptions, CreatedLink, AppSource } from './types';
export type { CreateShortLinkOptions, CreatedLink, AppSource } from './types';
export { APP_SOURCE_LABELS } from './types';
// Core API
export { initSharedUload, createShortLink, isSharedUloadReady } from './create-link';
export { initSharedUload, createShortLink, isSharedUloadReady, getBaseUrl } from './create-link';
// Utilities
export { generateShortCode, getQrCodeUrl, getShortUrl, downloadQrCode, QR_API } from './utils';

View file

@ -1,24 +1,10 @@
import type { BaseRecord } from '@mana/local-store';
export interface UloadLink extends BaseRecord {
shortCode: string;
customCode?: string | null;
originalUrl: string;
title?: string | null;
description?: string | null;
isActive: boolean;
password?: string | null;
maxClicks?: number | null;
expiresAt?: string | null;
clickCount: number;
qrCodeUrl?: string | null;
utmSource?: string | null;
utmMedium?: string | null;
utmCampaign?: string | null;
folderId?: string | null;
order: number;
source?: string | null;
}
/**
* Public-Wire-Types für Cross-App-Share-via-uLoad.
*
* `UloadLink` (interne Dexie-Repräsentation) ist mit der Federation-
* Migration 2026-05-18 entfallen Caller arbeiten nur noch mit
* `CreateShortLinkOptions` + `CreatedLink`.
*/
export interface CreateShortLinkOptions {
url: string;

4157
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff