mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 00:41:26 +02:00
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
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:
parent
0b44acdde1
commit
e9e43abaa0
7 changed files with 412 additions and 3996 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
4157
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue