AI Implementation feature(881): Authenticated Shell, Quick Tabs and Change Password 1.05 (#20)

This commit was merged in pull request #20.
This commit is contained in:
2026-07-22 10:35:27 +00:00
parent 90e570c43c
commit de527ec6d6
15 changed files with 688 additions and 56 deletions
@@ -13,7 +13,10 @@ if (typeof (globalThis as any).TextDecoder === 'undefined') {
}
interface EventSourceLike {
addEventListener(type: 'open' | 'message' | 'error', listener: (ev: MessageEvent | Event) => void): void;
addEventListener(
type: 'open' | 'message' | 'error' | 'unauthorized',
listener: (ev: MessageEvent | Event) => void,
): void;
close(): void;
}
@@ -33,6 +36,13 @@ function makeCaptureableFetch(frames: string[]) {
return { fetchMock, get captured() { return captured; } };
}
function makeUnauthorizedFetch(status: number) {
const fetchMock = jest.fn(async () => {
return { ok: false, status, body: null } as unknown as Response;
});
return fetchMock;
}
// Inline contract for the production AuthenticatedEventSourceService,
// kept here because the project's jest config cannot transform Angular's
// ESM runtime modules; this contract guards the regression.
@@ -42,12 +52,20 @@ function openAuthenticatedLike(
fetchImpl: typeof globalThis.fetch,
): EventSourceLike {
let handler: ((ev: MessageEvent) => void) | null = null;
let unauthorizedHandler: ((ev: Event) => void) | null = null;
void (async () => {
const headers: Record<string, string> = { Accept: 'text/event-stream' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetchImpl(url, { headers, credentials: 'include' });
if (!res || !(res as any).body) return;
const reader = ((res as any).body as ReadableStream<Uint8Array>).getReader();
const res = (await fetchImpl(url, { headers, credentials: 'include' })) as unknown as Response;
const status = (res as { status?: number }).status ?? 0;
if (status === 401 || status === 403) {
if (unauthorizedHandler) unauthorizedHandler(new Event('unauthorized'));
return;
}
if (!res || !(res as { body?: unknown }).body) return;
const reader = ((res as { body: ReadableStream<Uint8Array> }).body as ReadableStream<
Uint8Array
>).getReader();
const decoder = new TextDecoder();
let buf = '';
// eslint-disable-next-line no-constant-condition
@@ -70,11 +88,13 @@ function openAuthenticatedLike(
}
})();
return {
addEventListener(_type, cb) {
handler = cb as (ev: MessageEvent) => void;
addEventListener(type, cb) {
if (type === 'unauthorized') unauthorizedHandler = cb as (ev: Event) => void;
else handler = cb as (ev: MessageEvent) => void;
},
close() {
handler = null;
unauthorizedHandler = null;
},
};
}
@@ -150,4 +170,38 @@ describe('authenticated event source transport contract', () => {
expect(headers['Authorization']).toBeUndefined();
source.close();
});
it('fires an unauthorized event on a 401 response', async () => {
const fetchMock = makeUnauthorizedFetch(401);
(globalThis as any).fetch = fetchMock;
const source = openAuthenticatedLike(
'/api/v1/events/status',
'test-jwt-token',
fetchMock as unknown as typeof globalThis.fetch,
);
const received: string[] = [];
source.addEventListener('unauthorized', (ev) => {
received.push(ev.type);
});
await new Promise((r) => setTimeout(r, 30));
expect(received).toEqual(['unauthorized']);
source.close();
});
it('fires an unauthorized event on a 403 response', async () => {
const fetchMock = makeUnauthorizedFetch(403);
(globalThis as any).fetch = fetchMock;
const source = openAuthenticatedLike(
'/api/v1/events/status',
'test-jwt-token',
fetchMock as unknown as typeof globalThis.fetch,
);
const received: string[] = [];
source.addEventListener('unauthorized', (ev) => {
received.push(ev.type);
});
await new Promise((r) => setTimeout(r, 30));
expect(received).toEqual(['unauthorized']);
source.close();
});
});