mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 15:46:43 +00:00
fix: request registry tokens with the target repository scope (#1478)
* fix: request registry tokens with the target repository scope The image-update detector authenticated to registries by reusing the scope echoed in the registry's GET /v2/ ping. That ping carries no repository context, and ghcr.io answers it with a placeholder scope (repository:user/image:pull), so the token was requested for the wrong repository and rejected. Every ghcr.io-backed image (including lscr.io, which delegates auth to ghcr.io) then failed its manifest lookup and was reported as "Registry unreachable", while Docker Hub and quay.io kept working. Always request a pull scope for the repository being checked rather than the echoed placeholder. Also report the actual failure cause: getRemoteDigestResult now distinguishes an authentication failure, a rate limit (with retry-after), a missing image, a registry error, and a genuinely unreachable registry, instead of collapsing every failure into "Registry unreachable". getRemoteDigest stays a digest-or-null wrapper so the update-preview path is unchanged, and listRegistryTags shares the same token path so it now resolves on ghcr.io/lscr.io too. * fix: neutralize control characters in the registry digest error log The error-path console.error in getRemoteDigestResult interpolated the image ref and the caught error message, both of which originate from compose-authored input. Route them through sanitizeForLog so a crafted image string or upstream error text cannot forge multi-line log entries (log injection). The returned reason and the digest logic are unchanged.
This commit is contained in:
@@ -94,6 +94,14 @@ vi.mock('../services/NodeRegistry', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// getRemoteDigestResult is module-scoped inside checkImage; mock it to drive the remote
|
||||
// outcome while keeping the real parseImageRef / repoDigestMatchesRef.
|
||||
const { mockGetRemoteDigestResult } = vi.hoisted(() => ({ mockGetRemoteDigestResult: vi.fn() }));
|
||||
vi.mock('../services/registry-api', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../services/registry-api')>();
|
||||
return { ...actual, getRemoteDigestResult: mockGetRemoteDigestResult };
|
||||
});
|
||||
|
||||
// ── Re-export internal helpers via the module ─────────────────────────
|
||||
|
||||
// We need the internal functions. Import the module after mocks are set up.
|
||||
@@ -186,6 +194,38 @@ describe('ImageUpdateService - image ref parsing (via checkImage)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── checkImage surfaces the remote-digest reason ───────────────────────
|
||||
|
||||
describe('ImageUpdateService - checkImage surfaces the remote-digest reason', () => {
|
||||
let service: ImageUpdateService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(ImageUpdateService as any).instance = undefined;
|
||||
service = ImageUpdateService.getInstance();
|
||||
});
|
||||
|
||||
// One RepoDigest matching the ref so the local digest resolves and the flow reaches
|
||||
// getRemoteDigestResult.
|
||||
const dockerWithLocalDigest = (digest: string) => ({
|
||||
getDocker: () => ({
|
||||
getImage: () => ({ inspect: vi.fn().mockResolvedValue({ RepoDigests: [`ghcr.io/linuxserver/radarr@${digest}`] }) }),
|
||||
}),
|
||||
} as any);
|
||||
|
||||
it('surfaces the specific failure reason (not a generic "unreachable") as the check error', async () => {
|
||||
mockGetRemoteDigestResult.mockResolvedValue({ ok: false, reason: 'Authentication failed for ghcr.io/linuxserver/radarr:latest' });
|
||||
const result = await service.checkImage(dockerWithLocalDigest('sha256:local'), 'ghcr.io/linuxserver/radarr:latest');
|
||||
expect(result).toEqual({ hasUpdate: false, error: 'Authentication failed for ghcr.io/linuxserver/radarr:latest' });
|
||||
});
|
||||
|
||||
it('reports an update when the resolved remote digest differs from the local one', async () => {
|
||||
mockGetRemoteDigestResult.mockResolvedValue({ ok: true, digest: 'sha256:remote' });
|
||||
const result = await service.checkImage(dockerWithLocalDigest('sha256:local'), 'ghcr.io/linuxserver/radarr:latest');
|
||||
expect(result).toEqual({ hasUpdate: true });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Rate limiting ─────────────────────────────────────────────────────
|
||||
|
||||
describe('ImageUpdateService - manual refresh cooldown', () => {
|
||||
|
||||
Reference in New Issue
Block a user