mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 08:12:43 +00:00
fix: answer 503 for plugin lists and marketplace installs while the shared store is down #1066
directory-keys-shared.test.ts: the store-down case now expects the store error instead of an 'invalid' signature, since a store outage no longer counts as a bad signature.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAdminAuth } from '@/lib/admin/session';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { storeUnavailableResponse } from '@/lib/admin/store-unavailable-response';
|
||||
import { listHistory } from '@/lib/admin/extension-history';
|
||||
import { getPlugin, getTheme } from '@/lib/admin/plugin-registry';
|
||||
import { EXTENSION_ID_RE } from '@/lib/extension-package';
|
||||
@@ -41,6 +42,8 @@ export async function GET(
|
||||
{ headers: { 'Cache-Control': 'no-store' } },
|
||||
);
|
||||
} catch (error) {
|
||||
const unavailable = storeUnavailableResponse(error);
|
||||
if (unavailable) return unavailable;
|
||||
logger.error('Extension history error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAdminAuth, getClientIP } from '@/lib/admin/session';
|
||||
import { auditLog } from '@/lib/admin/audit';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { storeUnavailableResponse } from '@/lib/admin/store-unavailable-response';
|
||||
import {
|
||||
getPlugin,
|
||||
getPluginRegistry,
|
||||
@@ -46,6 +47,8 @@ export async function GET(request: NextRequest) {
|
||||
headers: { 'Cache-Control': 'no-store' },
|
||||
});
|
||||
} catch (error) {
|
||||
const unavailable = storeUnavailableResponse(error);
|
||||
if (unavailable) return unavailable;
|
||||
logger.error('Plugin list error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAdminAuth, getClientIP } from '@/lib/admin/session';
|
||||
import { auditLog } from '@/lib/admin/audit';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { storeUnavailableResponse } from '@/lib/admin/store-unavailable-response';
|
||||
import {
|
||||
getTheme,
|
||||
getThemeRegistry,
|
||||
@@ -33,6 +34,8 @@ export async function GET(request: NextRequest) {
|
||||
headers: { 'Cache-Control': 'no-store' },
|
||||
});
|
||||
} catch (error) {
|
||||
const unavailable = storeUnavailableResponse(error);
|
||||
if (unavailable) return unavailable;
|
||||
logger.error('Theme list error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getPluginRegistry, getThemeRegistry } from '@/lib/admin/plugin-registry
|
||||
import { listDevPlugins } from '@/lib/admin/plugin-dev';
|
||||
import { configManager } from '@/lib/admin/config-manager';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { storeUnavailableResponse } from '@/lib/admin/store-unavailable-response';
|
||||
import { isNoThirdPartyTrafficOn } from '@/lib/admin/third-party-mode';
|
||||
import { pluginDeclaresNetwork } from '@/lib/outgoing-hosts';
|
||||
|
||||
@@ -98,6 +99,8 @@ export async function GET(request: NextRequest) {
|
||||
{ headers: { 'Cache-Control': 'no-store' } },
|
||||
);
|
||||
} catch (error) {
|
||||
const unavailable = storeUnavailableResponse(error);
|
||||
if (unavailable) return unavailable;
|
||||
logger.error('Plugin list error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -92,9 +92,9 @@ describe('directory keyring dates in the shared store', () => {
|
||||
expect(existsSync(path.join(dir, 'state', 'directory-keyrings.json'))).toBe(false);
|
||||
});
|
||||
|
||||
it('refuses the bundle while the store is down', async () => {
|
||||
it('fails with the store outage while the store is down, so the install answers 503', async () => {
|
||||
store.down = true;
|
||||
const a = await replica();
|
||||
expect(await a(ringAt('2026-10-20T00:00:00.000Z'))).toMatchObject({ status: 'invalid' });
|
||||
await expect(a(ringAt('2026-10-20T00:00:00.000Z'))).rejects.toMatchObject({ name: 'StoreUnavailableError' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -225,6 +225,22 @@ describe('plugins in the shared store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('lists while the store is down', () => {
|
||||
it('answer 503 on a replica that has no copy yet, not 500', async () => {
|
||||
store.down = true;
|
||||
const r = await otherReplica();
|
||||
const history = await import('@/app/api/admin/extensions/[kind]/[id]/history/route');
|
||||
const publicList = await import('@/app/api/plugins/route');
|
||||
const responses = await Promise.all([
|
||||
r.plugins.GET(new NextRequest('http://localhost/api/admin/plugins')),
|
||||
r.themes.GET(new NextRequest('http://localhost/api/admin/themes')),
|
||||
history.GET(new NextRequest('http://localhost/api/admin/extensions/plugin/demo-plugin/history'), { params: Promise.resolve({ kind: 'plugin', id: 'demo-plugin' }) }),
|
||||
publicList.GET(new NextRequest('http://localhost/api/plugins')),
|
||||
]);
|
||||
expect(responses.map((res) => res.status)).toEqual([503, 503, 503, 503]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('one change of the registry at a time across replicas', () => {
|
||||
const LEASE = 'extension-registry-plugins';
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// none (a custom directory that set none), bundles install as before.
|
||||
import { createHash, createPublicKey, verify as nodeVerify } from 'node:crypto';
|
||||
import { canonicalJson } from '@/lib/extension-package';
|
||||
import { isStoreUnavailableError } from '@/lib/shared-store';
|
||||
import { DIRECTORY_KEYRINGS_ID, directoryKeyringDocs } from './extension-store';
|
||||
|
||||
export const OFFICIAL_DIRECTORY = 'https://extensions.bulwarkmail.org';
|
||||
@@ -174,6 +175,9 @@ export async function checkBundleSignature(opts: {
|
||||
try {
|
||||
keys = await onlineKeys(opts.directoryUrl, rootKeys, opts.fetchImpl ?? fetch);
|
||||
} catch (err) {
|
||||
// The shared store being down says nothing about the bundle: the
|
||||
// install answers 503 instead of calling it badly signed.
|
||||
if (isStoreUnavailableError(err)) throw err;
|
||||
return { status: 'invalid', reason: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
const key = keys.find((k) => k.keyId === keyId);
|
||||
|
||||
Reference in New Issue
Block a user