Files
multica/packages/core/config/index.ts
Bohan Jiang 6c17771cce fix: re-sign inline attachment media for token-mode clients (#4085)
The two prior MUL-3254 fixes preserved draft/description state across a
modal close, but Desktop still could not RENDER the reopened image: in
CloudFront signed-URL mode every URL the renderer holds after reopen is
unloadable. The persisted record strips the expired signed download_url,
the raw CDN url is unsigned (403 on a signed distribution), and the
durable /api/attachments/<id>/download endpoint needs credentials that a
cross-site file:// <img> fetch cannot carry (web works via the same-site
session cookie, which is why the bug was desktop-only).

Two changes close the last mile:

- /api/config now reports cdn_signed when CloudFront signing is enabled,
  and pickInlineMediaURL stops picking the raw (unsigned) CDN url in
  that mode — it is a guaranteed 403.
- The Attachment renderer upgrades an auth-gated media URL to a freshly
  signed one via authenticated GET /api/attachments/<id> (the same
  re-sign the click-time download path already does), but only on
  clients without a same-origin /api proxy (api.getBaseUrl() non-empty:
  Desktop, mobile webview). Cached via TanStack Query with a 20-minute
  staleTime, inside the server's 30-minute signed-URL TTL.

Old servers omit cdn_signed; the schema defaults it to false so behavior
is unchanged there. Non-CloudFront deployments return the API path again
from the metadata fetch and the renderer keeps the original URL.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-15 13:54:36 +08:00

50 lines
1.8 KiB
TypeScript

import { createStore } from "zustand/vanilla";
import { useStore } from "zustand";
interface ConfigState {
cdnDomain: string;
// True when cdnDomain serves private content via time-bounded signed URLs
// (CloudFront signing enabled server-side). Renderers must not treat a raw
// storage URL on that domain as a loadable media source (MUL-3254).
cdnSigned: boolean;
allowSignup: boolean;
googleClientId: string;
daemonServerUrl: string;
daemonAppUrl: string;
// Self-host gate (#3433): when true, every "Create workspace" affordance
// must be hidden. Defaults to false so unknown / older servers behave like
// the managed-cloud case.
workspaceCreationDisabled: boolean;
setCdnConfig: (config: { cdnDomain: string; cdnSigned?: boolean }) => void;
setAuthConfig: (config: {
allowSignup: boolean;
googleClientId?: string;
workspaceCreationDisabled?: boolean;
}) => void;
setDaemonConfig: (config: {
daemonServerUrl?: string;
daemonAppUrl?: string;
}) => void;
}
export const configStore = createStore<ConfigState>((set) => ({
cdnDomain: "",
cdnSigned: false,
allowSignup: true,
googleClientId: "",
daemonServerUrl: "",
daemonAppUrl: "",
workspaceCreationDisabled: false,
setCdnConfig: ({ cdnDomain, cdnSigned = false }) => set({ cdnDomain, cdnSigned }),
setAuthConfig: ({ allowSignup, googleClientId = "", workspaceCreationDisabled = false }) =>
set({ allowSignup, googleClientId, workspaceCreationDisabled }),
setDaemonConfig: ({ daemonServerUrl = "", daemonAppUrl = "" }) =>
set({ daemonServerUrl, daemonAppUrl }),
}));
export function useConfigStore(): ConfigState;
export function useConfigStore<T>(selector: (state: ConfigState) => T): T;
export function useConfigStore<T>(selector?: (state: ConfigState) => T) {
return useStore(configStore, selector as (state: ConfigState) => T);
}