github-issue-tower-defence-management 1.94.2 → 1.94.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/cli/index.js +1 -0
  3. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  4. package/bin/adapter/entry-points/console/consoleImageProxy.js +79 -0
  5. package/bin/adapter/entry-points/console/consoleImageProxy.js.map +1 -0
  6. package/bin/adapter/entry-points/console/consoleServer.js +29 -1
  7. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
  8. package/bin/adapter/entry-points/console/ui-dist/assets/index-fKvnL036.js +101 -0
  9. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  10. package/package.json +1 -1
  11. package/src/adapter/entry-points/cli/index.ts +1 -0
  12. package/src/adapter/entry-points/console/consoleImageProxy.test.ts +140 -0
  13. package/src/adapter/entry-points/console/consoleImageProxy.ts +111 -0
  14. package/src/adapter/entry-points/console/consoleServer.test.ts +182 -0
  15. package/src/adapter/entry-points/console/consoleServer.ts +45 -0
  16. package/src/adapter/entry-points/console/consoleUiDistSync.test.ts +66 -0
  17. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +10 -0
  18. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.tsx +23 -3
  19. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.tsx +7 -1
  20. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +9 -1
  21. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +7 -1
  22. package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.test.ts +78 -0
  23. package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.ts +41 -0
  24. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +9 -0
  25. package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +11 -0
  26. package/src/adapter/entry-points/console/ui-dist/assets/index-fKvnL036.js +101 -0
  27. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  28. package/types/adapter/entry-points/console/consoleImageProxy.d.ts +19 -0
  29. package/types/adapter/entry-points/console/consoleImageProxy.d.ts.map +1 -0
  30. package/types/adapter/entry-points/console/consoleServer.d.ts +4 -0
  31. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  32. package/bin/adapter/entry-points/console/ui-dist/assets/index-eSyo3r3C.js +0 -101
  33. package/src/adapter/entry-points/console/ui-dist/assets/index-eSyo3r3C.js +0 -101
@@ -1,4 +1,5 @@
1
1
  import { useState } from 'react';
2
+ import type { ImageProxyUrlBuilder } from '../../lib/imageProxy';
2
3
  import { formatRelativeTime } from '../../logic/relativeTime';
3
4
  import type { ConsoleComment } from '../../logic/types';
4
5
  import { ConsoleMarkdownContent } from '../content/ConsoleMarkdownContent';
@@ -8,6 +9,7 @@ export type ConsoleCommentListProps = {
8
9
  isLoading: boolean;
9
10
  error: string | null;
10
11
  now: number;
12
+ buildImageProxyUrl?: ImageProxyUrlBuilder;
11
13
  };
12
14
 
13
15
  export const ConsoleCommentList = ({
@@ -15,6 +17,7 @@ export const ConsoleCommentList = ({
15
17
  isLoading,
16
18
  error,
17
19
  now,
20
+ buildImageProxyUrl,
18
21
  }: ConsoleCommentListProps) => {
19
22
  const [showAll, setShowAll] = useState<boolean>(false);
20
23
 
@@ -58,7 +61,10 @@ export const ConsoleCommentList = ({
58
61
  {formatRelativeTime(comment.createdAt, now)}
59
62
  </span>
60
63
  </header>
61
- <ConsoleMarkdownContent body={comment.body} />
64
+ <ConsoleMarkdownContent
65
+ body={comment.body}
66
+ buildImageProxyUrl={buildImageProxyUrl}
67
+ />
62
68
  </article>
63
69
  ))}
64
70
  </div>
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { ImageProxyUrlBuilder } from '../../lib/imageProxy';
2
3
  import { colorFromEnum } from '../../logic/colors';
3
4
  import {
4
5
  formatFullTimestamp,
@@ -54,6 +55,7 @@ export type ConsoleItemDetailProps = {
54
55
  now: number;
55
56
  commentComposer: ReactNode;
56
57
  operationBar: ReactNode;
58
+ buildImageProxyUrl?: ImageProxyUrlBuilder;
57
59
  };
58
60
 
59
61
  export const ConsoleItemDetail = ({
@@ -78,6 +80,7 @@ export const ConsoleItemDetail = ({
78
80
  now,
79
81
  commentComposer,
80
82
  operationBar,
83
+ buildImageProxyUrl,
81
84
  }: ConsoleItemDetailProps) => {
82
85
  const resolvedState = state?.state ?? 'open';
83
86
  const merged = state?.merged ?? false;
@@ -192,7 +195,10 @@ export const ConsoleItemDetail = ({
192
195
  ) : bodyIsLoading ? (
193
196
  <p className="console-detail-body-loading">Loading description...</p>
194
197
  ) : (
195
- <ConsoleMarkdownContent body={body} />
198
+ <ConsoleMarkdownContent
199
+ body={body}
200
+ buildImageProxyUrl={buildImageProxyUrl}
201
+ />
196
202
  )}
197
203
  </ConsolePanel>
198
204
 
@@ -216,6 +222,7 @@ export const ConsoleItemDetail = ({
216
222
  isLoading={commentsAreLoading}
217
223
  error={commentsError}
218
224
  now={now}
225
+ buildImageProxyUrl={buildImageProxyUrl}
219
226
  />
220
227
  </ConsolePanel>
221
228
 
@@ -244,6 +251,7 @@ export const ConsoleItemDetail = ({
244
251
  commitsAreLoading={related.commitsAreLoading}
245
252
  commitsError={related.commitsError}
246
253
  now={now}
254
+ buildImageProxyUrl={buildImageProxyUrl}
247
255
  />
248
256
  ))}
249
257
 
@@ -1,3 +1,4 @@
1
+ import type { ImageProxyUrlBuilder } from '../../lib/imageProxy';
1
2
  import type {
2
3
  ConsoleChangedFile,
3
4
  ConsoleCommit,
@@ -19,6 +20,7 @@ export type ConsolePullRequestSectionProps = {
19
20
  commitsAreLoading: boolean;
20
21
  commitsError: string | null;
21
22
  now: number;
23
+ buildImageProxyUrl?: ImageProxyUrlBuilder;
22
24
  };
23
25
 
24
26
  export const ConsolePullRequestDetail = ({
@@ -32,6 +34,7 @@ export const ConsolePullRequestDetail = ({
32
34
  commitsAreLoading,
33
35
  commitsError,
34
36
  now,
37
+ buildImageProxyUrl,
35
38
  }: ConsolePullRequestSectionProps) => {
36
39
  const summary = pullRequest.summary;
37
40
  const filesCount =
@@ -71,7 +74,10 @@ export const ConsolePullRequestDetail = ({
71
74
  {bodyIsLoading ? (
72
75
  <p className="console-pr-body-loading">Loading description...</p>
73
76
  ) : (
74
- <ConsoleMarkdownContent body={summary?.body ?? body} />
77
+ <ConsoleMarkdownContent
78
+ body={summary?.body ?? body}
79
+ buildImageProxyUrl={buildImageProxyUrl}
80
+ />
75
81
  )}
76
82
  </ConsolePanel>
77
83
  <ConsolePanel title="Changed files" count={filesCount}>
@@ -0,0 +1,78 @@
1
+ import {
2
+ buildImageProxyUrl,
3
+ isProxyableImageUrl,
4
+ rewriteGitHubImageSources,
5
+ } from './imageProxy';
6
+
7
+ describe('isProxyableImageUrl', () => {
8
+ it('matches github user-attachments urls', () => {
9
+ expect(
10
+ isProxyableImageUrl(
11
+ 'https://github.com/user-attachments/assets/0a1b2c3d',
12
+ ),
13
+ ).toBe(true);
14
+ });
15
+
16
+ it('matches githubusercontent subdomain urls', () => {
17
+ expect(
18
+ isProxyableImageUrl(
19
+ 'https://private-user-images.githubusercontent.com/1/2.png',
20
+ ),
21
+ ).toBe(true);
22
+ });
23
+
24
+ it('does not match arbitrary external hosts', () => {
25
+ expect(isProxyableImageUrl('https://example.com/avatar.png')).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe('buildImageProxyUrl', () => {
30
+ it('encodes the source url and appends the token', () => {
31
+ const src = 'https://github.com/user-attachments/assets/abc?x=1';
32
+ expect(buildImageProxyUrl(src, 'tk')).toBe(
33
+ `/api/img?url=${encodeURIComponent(src)}&k=tk`,
34
+ );
35
+ });
36
+
37
+ it('omits the token when it is null', () => {
38
+ const src = 'https://github.com/user-attachments/assets/abc';
39
+ expect(buildImageProxyUrl(src, null)).toBe(
40
+ `/api/img?url=${encodeURIComponent(src)}`,
41
+ );
42
+ });
43
+ });
44
+
45
+ describe('rewriteGitHubImageSources', () => {
46
+ const buildProxyUrl = (src: string): string =>
47
+ buildImageProxyUrl(src, 'console-token');
48
+
49
+ it('rewrites an allow-listed github image src to the proxy url', () => {
50
+ const githubSrc = 'https://github.com/user-attachments/assets/abc';
51
+ const html = `<p><img src="${githubSrc}" alt="shot" /></p>`;
52
+ const result = rewriteGitHubImageSources(html, buildProxyUrl);
53
+ expect(result).toContain(`/api/img?url=${encodeURIComponent(githubSrc)}`);
54
+ expect(result).toContain('k=console-token');
55
+ expect(result).not.toContain(`src="${githubSrc}"`);
56
+ });
57
+
58
+ it('rewrites a githubusercontent image src to the proxy url', () => {
59
+ const githubSrc =
60
+ 'https://private-user-images.githubusercontent.com/1/2.png';
61
+ const html = `<img src="${githubSrc}" />`;
62
+ const result = rewriteGitHubImageSources(html, buildProxyUrl);
63
+ expect(result).toContain(`/api/img?url=${encodeURIComponent(githubSrc)}`);
64
+ });
65
+
66
+ it('leaves a non-matching image src unchanged', () => {
67
+ const externalSrc = 'https://example.com/avatar.png';
68
+ const html = `<img src="${externalSrc}" />`;
69
+ const result = rewriteGitHubImageSources(html, buildProxyUrl);
70
+ expect(result).toContain(`src="${externalSrc}"`);
71
+ expect(result).not.toContain('/api/img');
72
+ });
73
+
74
+ it('returns the original html when there is no image to rewrite', () => {
75
+ const html = '<p>no images here</p>';
76
+ expect(rewriteGitHubImageSources(html, buildProxyUrl)).toBe(html);
77
+ });
78
+ });
@@ -0,0 +1,41 @@
1
+ const ALLOWED_IMAGE_URL =
2
+ /^https:\/\/github\.com\/user-attachments\/|^https:\/\/[a-z0-9][a-z0-9.-]*\.githubusercontent\.com\//;
3
+
4
+ export const isProxyableImageUrl = (src: string): boolean =>
5
+ ALLOWED_IMAGE_URL.test(src);
6
+
7
+ export type ImageProxyUrlBuilder = (src: string) => string;
8
+
9
+ export const buildImageProxyUrl = (
10
+ src: string,
11
+ token: string | null,
12
+ ): string => {
13
+ if (token === null) {
14
+ return `/api/img?url=${encodeURIComponent(src)}`;
15
+ }
16
+ return `/api/img?url=${encodeURIComponent(src)}&k=${encodeURIComponent(token)}`;
17
+ };
18
+
19
+ export const rewriteGitHubImageSources = (
20
+ html: string,
21
+ buildProxyUrl: ImageProxyUrlBuilder,
22
+ ): string => {
23
+ if (typeof document === 'undefined') {
24
+ return html;
25
+ }
26
+ const template = document.createElement('template');
27
+ template.innerHTML = html;
28
+ const images = template.content.querySelectorAll('img[src]');
29
+ let rewrote = false;
30
+ images.forEach((image) => {
31
+ const src = image.getAttribute('src') ?? '';
32
+ if (isProxyableImageUrl(src)) {
33
+ image.setAttribute('src', buildProxyUrl(src));
34
+ rewrote = true;
35
+ }
36
+ });
37
+ if (!rewrote) {
38
+ return html;
39
+ }
40
+ return template.innerHTML;
41
+ };
@@ -1,9 +1,12 @@
1
+ import { useCallback } from 'react';
1
2
  import { ConsoleCommentComposer } from '../components/detail/ConsoleCommentComposer';
2
3
  import { ConsoleItemDetail } from '../components/detail/ConsoleItemDetail';
3
4
  import { ConsoleOperationMenu } from '../components/operations/ConsoleOperationMenu';
4
5
  import type { ConsoleCaches } from '../hooks/useConsoleCaches';
5
6
  import { useConsoleItemDetailData } from '../hooks/useConsoleItemDetailData';
6
7
  import type { ConsoleOperationsApi } from '../hooks/useConsoleOperations';
8
+ import { useConsoleToken } from '../hooks/useConsoleToken';
9
+ import { buildImageProxyUrl } from '../lib/imageProxy';
7
10
  import type { ConsoleActionKind } from '../logic/actionToast';
8
11
  import { resolveStoryColorEnum } from '../logic/grouping';
9
12
  import type { ConsoleOperationHandlers } from '../logic/operations';
@@ -50,6 +53,11 @@ export const ConsoleItemDetailContainer = ({
50
53
  onQueueAction,
51
54
  }: ConsoleItemDetailContainerProps) => {
52
55
  const detail = useConsoleItemDetailData(caches, item);
56
+ const { token } = useConsoleToken();
57
+ const resolveImageProxyUrl = useCallback(
58
+ (src: string): string => buildImageProxyUrl(src, token),
59
+ [token],
60
+ );
53
61
  const hasPullRequest = item.isPr || detail.relatedPullRequests.length > 0;
54
62
 
55
63
  const handlers: ConsoleOperationHandlers = {
@@ -140,6 +148,7 @@ export const ConsoleItemDetailContainer = ({
140
148
  commitsError={detail.commitsError}
141
149
  relatedPullRequests={detail.relatedPullRequests}
142
150
  now={now}
151
+ buildImageProxyUrl={resolveImageProxyUrl}
143
152
  commentComposer={
144
153
  <ConsoleCommentComposer
145
154
  isPr={item.isPr}
@@ -133,6 +133,17 @@ console bundle and the token-protected \`/api\` endpoints.
133
133
  - [x] Token validation via \`?k=\` and the \`x-pv-token\` header
134
134
  `;
135
135
 
136
+ export const consoleMarkdownImageBodyFixture = `## Screenshot
137
+
138
+ The failing screen is attached below.
139
+
140
+ ![console screenshot](https://github.com/user-attachments/assets/0a1b2c3d-4e5f-6789-abcd-ef0123456789)
141
+
142
+ For reference, the avatar comes from an external host:
143
+
144
+ ![external avatar](https://example.com/avatar.png)
145
+ `;
146
+
136
147
  export const consoleMermaidBodyFixture = `Here is the request flow:
137
148
 
138
149
  \`\`\`mermaid