2 Commits
Author SHA1 Message Date
justme c68acfd78b fix: allow embedded worker API requests
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/tag/release-backend Pipeline was successful
2026-10-02 14:12:14 +08:00
justme 10ea48bf04 fix: clarify quick share creation failures
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/tag/release-backend Pipeline was successful
2026-10-02 13:44:03 +08:00
4 changed files with 15 additions and 10 deletions

No files matched your search

@@ -87,7 +87,9 @@ public static class EmbeddedFrontend
? "public, max-age=31536000, immutable"
: "no-store";
if (path.Equals("index.html", StringComparison.Ordinal))
// Dedicated workers enforce the CSP returned with their own script response.
// Give bundled JS the same app policy so the crypto worker can call the same-origin API.
if (path.Equals("index.html", StringComparison.Ordinal) || path.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
context.Response.Headers["Content-Security-Policy"] = contentSecurityPolicy;
if (!HttpMethods.IsHead(context.Request.Method))
+5 -2
View File
@@ -1,7 +1,7 @@
import { supportsApiContract } from '../contract';
export type ApiError = Error & { status?: number; code?: string };
let apiBase = '/api/v1';
let apiBase = new URL('/api/v1', self.location.origin).toString();
let contractCheck: Promise<void> | undefined;
export function setApiBase(value: string): void {
@@ -11,7 +11,10 @@ export function setApiBase(value: string): void {
}
export const fail = (status: number, code: string): ApiError => Object.assign(new Error(code), { status, code });
export const apiPath = (path: string): string => `${apiBase.replace(/\/$/, '')}${path}`;
export const apiPath = (path: string): string => new URL(
`${apiBase.replace(/\/$/, '')}${path}`,
self.location.origin,
).toString();
function ensureApiContract(): Promise<void> {
contractCheck ??= fetch(apiPath('/info'), {
+6 -5
View File
@@ -6,6 +6,7 @@ import { useGlobalPaste } from '@/lib/useGlobalPaste';
import { useChannels } from '@/store/channels';
import { useShares } from '@/store/shares';
import { solveCreationCaptcha } from '@/core-bridge/captcha';
import { errorMessage } from '@/core-bridge/errors';
import { Button } from '@/ui/Button';
import { ChannelList } from './ChannelList';
import { CreateSheet } from './CreateSheet';
@@ -22,7 +23,7 @@ export function Home() {
const [createOpen, setCreateOpen] = useState(false);
const [scanOpen, setScanOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [failed, setFailed] = useState(false);
const [failedMessage, setFailedMessage] = useState('');
const [shareText, setShareText] = useState('');
const [shareFiles, setShareFiles] = useState<File[]>([]);
@@ -32,15 +33,15 @@ export function Home() {
async (p: ClipboardPayload) => {
if (busy || (p.files.length === 0 && !p.text.trim())) return false;
setBusy(true);
setFailed(false);
setFailedMessage('');
try {
const captchaToken = await solveCreationCaptcha();
open(await createDraft(p, captchaToken));
setShareFiles([]);
setShareText('');
return true;
} catch {
setFailed(true);
} catch (error) {
setFailedMessage(errorMessage(error));
return false;
} finally {
setBusy(false);
@@ -71,7 +72,7 @@ export function Home() {
onFilesChange={setShareFiles}
onShare={share}
/>
{failed ? <p role="alert" className={styles.error}>创建分享失败,请重试。</p> : null}
{failedMessage ? <p role="alert" className={styles.error}>创建分享失败:{failedMessage}</p> : null}
<ChannelList kind="share" title="我的分享" onOpen={open} />
+1 -2
View File
@@ -58,8 +58,7 @@
}
.editor:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
outline: none;
}
.editor::placeholder {