github-issue-tower-defence-management 1.128.0 → 1.129.0
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.
- package/.github/workflows/console-ui.yml +1 -1
- package/.github/workflows/empty-format-test-job.yml +1 -1
- package/.github/workflows/format.yml +1 -1
- package/.github/workflows/publish.yml +1 -1
- package/.github/workflows/test.yml +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +3 -3
- package/bin/adapter/entry-points/console/ui-dist/assets/index-Dr5qin37.js +103 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/webServer.js +56 -3
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/domain/entities/WorkflowStatus.js +6 -1
- package/bin/domain/entities/WorkflowStatus.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +1 -0
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
- package/package.json +3 -3
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +1 -2
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +2 -4
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +7 -7
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +15 -18
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +2 -5
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +22 -25
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +12 -23
- package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.test.ts +7 -10
- package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.ts +2 -9
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +2 -4
- package/src/adapter/entry-points/console/ui-dist/assets/index-Dr5qin37.js +103 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +283 -6
- package/src/adapter/entry-points/console/webServer.ts +64 -0
- package/src/domain/entities/WorkflowStatus.ts +5 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +92 -2
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +2 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +1 -0
- package/types/adapter/entry-points/console/webServer.d.ts +5 -1
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/domain/entities/WorkflowStatus.d.ts +1 -0
- package/types/domain/entities/WorkflowStatus.d.ts.map +1 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-8H0HejYa.js +0 -103
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +0 -41
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.ts +0 -64
- package/src/adapter/entry-points/console/ui-dist/assets/index-8H0HejYa.js +0 -103
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-Dr5qin37.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-N9M1qhkw.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -6,11 +6,13 @@ import { mock } from 'jest-mock-extended';
|
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_WEB_PORT,
|
|
8
8
|
CONSOLE_TOKEN_HEADER,
|
|
9
|
+
CONSOLE_TOKEN_COOKIE,
|
|
9
10
|
hasDotSegment,
|
|
10
11
|
requiresToken,
|
|
11
12
|
isTokenValid,
|
|
12
13
|
isConsoleAppRoute,
|
|
13
14
|
extractProvidedToken,
|
|
15
|
+
extractCookieToken,
|
|
14
16
|
resolveFlatInTmuxFilePath,
|
|
15
17
|
resolveDashboardFilePath,
|
|
16
18
|
startWebServer,
|
|
@@ -109,16 +111,53 @@ describe('webServer pure helpers', () => {
|
|
|
109
111
|
|
|
110
112
|
describe('extractProvidedToken', () => {
|
|
111
113
|
it('prefers the query token', () => {
|
|
112
|
-
expect(
|
|
114
|
+
expect(
|
|
115
|
+
extractProvidedToken('fromQuery', 'fromHeader', 'fromCookie'),
|
|
116
|
+
).toBe('fromQuery');
|
|
113
117
|
});
|
|
114
118
|
|
|
115
|
-
it('falls back to the header token', () => {
|
|
116
|
-
expect(extractProvidedToken(null, 'fromHeader')).toBe(
|
|
119
|
+
it('falls back to the header token before the cookie token', () => {
|
|
120
|
+
expect(extractProvidedToken(null, 'fromHeader', 'fromCookie')).toBe(
|
|
121
|
+
'fromHeader',
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('falls back to the cookie token when query and header are absent', () => {
|
|
126
|
+
expect(extractProvidedToken(null, undefined, 'fromCookie')).toBe(
|
|
127
|
+
'fromCookie',
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns null when none is present', () => {
|
|
132
|
+
expect(extractProvidedToken(null, undefined, null)).toBeNull();
|
|
133
|
+
expect(extractProvidedToken('', undefined, '')).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('extractCookieToken', () => {
|
|
138
|
+
it('reads the token cookie value from a cookie header', () => {
|
|
139
|
+
expect(extractCookieToken(`${CONSOLE_TOKEN_COOKIE}=cookie-value`)).toBe(
|
|
140
|
+
'cookie-value',
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('reads the token cookie when other cookies are present', () => {
|
|
145
|
+
expect(
|
|
146
|
+
extractCookieToken(
|
|
147
|
+
`other=1; ${CONSOLE_TOKEN_COOKIE}=cookie-value; a=b`,
|
|
148
|
+
),
|
|
149
|
+
).toBe('cookie-value');
|
|
117
150
|
});
|
|
118
151
|
|
|
119
|
-
it('
|
|
120
|
-
expect(
|
|
121
|
-
|
|
152
|
+
it('decodes a percent-encoded cookie value', () => {
|
|
153
|
+
expect(extractCookieToken(`${CONSOLE_TOKEN_COOKIE}=a%20b`)).toBe('a b');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('returns null when the token cookie is absent, empty, or undefined', () => {
|
|
157
|
+
expect(extractCookieToken('other=1')).toBeNull();
|
|
158
|
+
expect(extractCookieToken(`${CONSOLE_TOKEN_COOKIE}=`)).toBeNull();
|
|
159
|
+
expect(extractCookieToken(undefined)).toBeNull();
|
|
160
|
+
expect(extractCookieToken('')).toBeNull();
|
|
122
161
|
});
|
|
123
162
|
});
|
|
124
163
|
});
|
|
@@ -1618,3 +1657,241 @@ describe('webServer image proxy', () => {
|
|
|
1618
1657
|
}
|
|
1619
1658
|
});
|
|
1620
1659
|
});
|
|
1660
|
+
|
|
1661
|
+
describe('webServer token cookie redirect', () => {
|
|
1662
|
+
const testToken = 'integration-test-token-value';
|
|
1663
|
+
|
|
1664
|
+
const closeServer = (server: http.Server): Promise<void> =>
|
|
1665
|
+
new Promise((resolve, reject) => {
|
|
1666
|
+
server.close((error) => {
|
|
1667
|
+
if (error) {
|
|
1668
|
+
reject(error);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
resolve();
|
|
1672
|
+
});
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
const requestServer = (
|
|
1676
|
+
server: http.Server,
|
|
1677
|
+
requestPath: string,
|
|
1678
|
+
headers: http.OutgoingHttpHeaders = {},
|
|
1679
|
+
): Promise<{
|
|
1680
|
+
statusCode: number;
|
|
1681
|
+
body: string;
|
|
1682
|
+
location: string | undefined;
|
|
1683
|
+
setCookie: string[] | undefined;
|
|
1684
|
+
referrerPolicy: string | string[] | undefined;
|
|
1685
|
+
cacheControl: string | undefined;
|
|
1686
|
+
}> => {
|
|
1687
|
+
const address = server.address();
|
|
1688
|
+
if (address === null || typeof address === 'string') {
|
|
1689
|
+
throw new Error('server is not listening on a TCP port');
|
|
1690
|
+
}
|
|
1691
|
+
const port = address.port;
|
|
1692
|
+
return new Promise((resolve, reject) => {
|
|
1693
|
+
const httpRequest = http.request(
|
|
1694
|
+
{ host: '127.0.0.1', port, path: requestPath, headers },
|
|
1695
|
+
(response) => {
|
|
1696
|
+
const chunks: Uint8Array[] = [];
|
|
1697
|
+
response.on('data', (chunk: Uint8Array) => chunks.push(chunk));
|
|
1698
|
+
response.on('end', () => {
|
|
1699
|
+
resolve({
|
|
1700
|
+
statusCode: response.statusCode ?? 0,
|
|
1701
|
+
body: Buffer.concat(chunks).toString('utf-8'),
|
|
1702
|
+
location: response.headers['location'],
|
|
1703
|
+
setCookie: response.headers['set-cookie'],
|
|
1704
|
+
referrerPolicy: response.headers['referrer-policy'],
|
|
1705
|
+
cacheControl: response.headers['cache-control'],
|
|
1706
|
+
});
|
|
1707
|
+
});
|
|
1708
|
+
},
|
|
1709
|
+
);
|
|
1710
|
+
httpRequest.on('error', reject);
|
|
1711
|
+
httpRequest.end();
|
|
1712
|
+
});
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
const startWithUiDist = async (): Promise<{
|
|
1716
|
+
server: http.Server;
|
|
1717
|
+
tmpDir: string;
|
|
1718
|
+
}> => {
|
|
1719
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1720
|
+
const uiDistDir = path.join(tmpDir, 'ui-dist');
|
|
1721
|
+
fs.mkdirSync(uiDistDir, { recursive: true });
|
|
1722
|
+
fs.writeFileSync(
|
|
1723
|
+
path.join(uiDistDir, 'index.html'),
|
|
1724
|
+
'<!DOCTYPE html><title>spa</title><div id="root"></div>',
|
|
1725
|
+
);
|
|
1726
|
+
const server = await startWebServer({
|
|
1727
|
+
accessToken: testToken,
|
|
1728
|
+
uiDistDir,
|
|
1729
|
+
consoleDataOutputDir: null,
|
|
1730
|
+
inTmuxDataDir: null,
|
|
1731
|
+
dashboardDir: null,
|
|
1732
|
+
dashboardDataDir: null,
|
|
1733
|
+
dashboardProjectNames: [],
|
|
1734
|
+
port: 0,
|
|
1735
|
+
});
|
|
1736
|
+
return { server, tmpDir };
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
const firstSetCookie = (setCookie: string[] | undefined): string => {
|
|
1740
|
+
expect(setCookie).toBeDefined();
|
|
1741
|
+
expect(setCookie).toHaveLength(1);
|
|
1742
|
+
return (setCookie ?? [''])[0];
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
it('redirects a per-project app route carrying ?k= to the keyless path and sets an HttpOnly SameSite=Strict cookie', async () => {
|
|
1746
|
+
const { server, tmpDir } = await startWithUiDist();
|
|
1747
|
+
try {
|
|
1748
|
+
const response = await requestServer(
|
|
1749
|
+
server,
|
|
1750
|
+
`/projects/umino?k=${testToken}`,
|
|
1751
|
+
);
|
|
1752
|
+
expect(response.statusCode).toBe(302);
|
|
1753
|
+
expect(response.location).toBe('/projects/umino');
|
|
1754
|
+
expect(response.body).not.toContain(testToken);
|
|
1755
|
+
const cookie = firstSetCookie(response.setCookie);
|
|
1756
|
+
expect(cookie).toContain(`${CONSOLE_TOKEN_COOKIE}=${testToken}`);
|
|
1757
|
+
expect(cookie).toContain('HttpOnly');
|
|
1758
|
+
expect(cookie).toContain('SameSite=Strict');
|
|
1759
|
+
expect(cookie).toContain('Path=/');
|
|
1760
|
+
expect(response.referrerPolicy).toBe('no-referrer');
|
|
1761
|
+
} finally {
|
|
1762
|
+
await closeServer(server);
|
|
1763
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1767
|
+
it('redirects a per-project tab route carrying ?k= and preserves other query parameters', async () => {
|
|
1768
|
+
const { server, tmpDir } = await startWithUiDist();
|
|
1769
|
+
try {
|
|
1770
|
+
const response = await requestServer(
|
|
1771
|
+
server,
|
|
1772
|
+
`/projects/xmile/prs?k=${testToken}&foo=bar`,
|
|
1773
|
+
);
|
|
1774
|
+
expect(response.statusCode).toBe(302);
|
|
1775
|
+
expect(response.location).toBe('/projects/xmile/prs?foo=bar');
|
|
1776
|
+
const cookie = firstSetCookie(response.setCookie);
|
|
1777
|
+
expect(cookie).toContain(`${CONSOLE_TOKEN_COOKIE}=${testToken}`);
|
|
1778
|
+
} finally {
|
|
1779
|
+
await closeServer(server);
|
|
1780
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
it('redirects the root and index.html routes carrying ?k= to a keyless path', async () => {
|
|
1785
|
+
const { server, tmpDir } = await startWithUiDist();
|
|
1786
|
+
try {
|
|
1787
|
+
const root = await requestServer(server, `/?k=${testToken}`);
|
|
1788
|
+
expect(root.statusCode).toBe(302);
|
|
1789
|
+
expect(root.location).toBe('/');
|
|
1790
|
+
expect(firstSetCookie(root.setCookie)).toContain(
|
|
1791
|
+
`${CONSOLE_TOKEN_COOKIE}=${testToken}`,
|
|
1792
|
+
);
|
|
1793
|
+
|
|
1794
|
+
const indexHtml = await requestServer(
|
|
1795
|
+
server,
|
|
1796
|
+
`/index.html?k=${testToken}`,
|
|
1797
|
+
);
|
|
1798
|
+
expect(indexHtml.statusCode).toBe(302);
|
|
1799
|
+
expect(indexHtml.location).toBe('/index.html');
|
|
1800
|
+
} finally {
|
|
1801
|
+
await closeServer(server);
|
|
1802
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
it('serves the app route without a token and never leaks a token in the URL', async () => {
|
|
1807
|
+
const { server, tmpDir } = await startWithUiDist();
|
|
1808
|
+
try {
|
|
1809
|
+
const response = await requestServer(server, '/projects/umino');
|
|
1810
|
+
expect(response.statusCode).toBe(200);
|
|
1811
|
+
expect(response.body).toContain('spa');
|
|
1812
|
+
expect(response.setCookie).toBeUndefined();
|
|
1813
|
+
expect(response.referrerPolicy).toBe('no-referrer');
|
|
1814
|
+
} finally {
|
|
1815
|
+
await closeServer(server);
|
|
1816
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
it('sets Referrer-Policy: no-referrer on the console HTML responses', async () => {
|
|
1821
|
+
const { server, tmpDir } = await startWithUiDist();
|
|
1822
|
+
try {
|
|
1823
|
+
const root = await requestServer(server, '/');
|
|
1824
|
+
expect(root.statusCode).toBe(200);
|
|
1825
|
+
expect(root.referrerPolicy).toBe('no-referrer');
|
|
1826
|
+
|
|
1827
|
+
const projectTab = await requestServer(server, '/projects/xmile/prs');
|
|
1828
|
+
expect(projectTab.statusCode).toBe(200);
|
|
1829
|
+
expect(projectTab.referrerPolicy).toBe('no-referrer');
|
|
1830
|
+
} finally {
|
|
1831
|
+
await closeServer(server);
|
|
1832
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
it('authenticates a data list request through the cookie without a token in the URL', async () => {
|
|
1837
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1838
|
+
const dataDir = path.join(tmpDir, 'data');
|
|
1839
|
+
const listDir = path.join(dataDir, 'umino', 'prs');
|
|
1840
|
+
fs.mkdirSync(listDir, { recursive: true });
|
|
1841
|
+
fs.writeFileSync(
|
|
1842
|
+
path.join(listDir, 'list.json'),
|
|
1843
|
+
JSON.stringify({ pjcode: 'umino', items: [] }),
|
|
1844
|
+
);
|
|
1845
|
+
const server = await startWebServer({
|
|
1846
|
+
accessToken: testToken,
|
|
1847
|
+
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
1848
|
+
consoleDataOutputDir: dataDir,
|
|
1849
|
+
inTmuxDataDir: null,
|
|
1850
|
+
dashboardDir: null,
|
|
1851
|
+
dashboardDataDir: null,
|
|
1852
|
+
dashboardProjectNames: [],
|
|
1853
|
+
port: 0,
|
|
1854
|
+
});
|
|
1855
|
+
try {
|
|
1856
|
+
const withCookie = await requestServer(
|
|
1857
|
+
server,
|
|
1858
|
+
'/projects/umino/prs/list.json',
|
|
1859
|
+
{ Cookie: `${CONSOLE_TOKEN_COOKIE}=${testToken}` },
|
|
1860
|
+
);
|
|
1861
|
+
expect(withCookie.statusCode).toBe(200);
|
|
1862
|
+
expect(JSON.parse(withCookie.body)).toEqual({
|
|
1863
|
+
pjcode: 'umino',
|
|
1864
|
+
items: [],
|
|
1865
|
+
});
|
|
1866
|
+
} finally {
|
|
1867
|
+
await closeServer(server);
|
|
1868
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
it('rejects a data or api request when no token and no cookie are present', async () => {
|
|
1873
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1874
|
+
const server = await startWebServer({
|
|
1875
|
+
accessToken: testToken,
|
|
1876
|
+
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
1877
|
+
consoleDataOutputDir: null,
|
|
1878
|
+
inTmuxDataDir: null,
|
|
1879
|
+
dashboardDir: null,
|
|
1880
|
+
dashboardDataDir: null,
|
|
1881
|
+
dashboardProjectNames: [],
|
|
1882
|
+
port: 0,
|
|
1883
|
+
});
|
|
1884
|
+
try {
|
|
1885
|
+
const noToken = await requestServer(server, '/data/situation.json');
|
|
1886
|
+
expect(noToken.statusCode).toBe(401);
|
|
1887
|
+
|
|
1888
|
+
const wrongCookie = await requestServer(server, '/api/review', {
|
|
1889
|
+
Cookie: `${CONSOLE_TOKEN_COOKIE}=wrong-token`,
|
|
1890
|
+
});
|
|
1891
|
+
expect(wrongCookie.statusCode).toBe(401);
|
|
1892
|
+
} finally {
|
|
1893
|
+
await closeServer(server);
|
|
1894
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
});
|
|
@@ -41,6 +41,8 @@ export const DEFAULT_DASHBOARD_PROJECT_NAMES = DASHBOARD_PROJECT_NAMES;
|
|
|
41
41
|
|
|
42
42
|
export const CONSOLE_TOKEN_HEADER = 'x-pv-token';
|
|
43
43
|
|
|
44
|
+
export const CONSOLE_TOKEN_COOKIE = 'pv_token';
|
|
45
|
+
|
|
44
46
|
const PLACEHOLDER_INDEX_HTML = `<!DOCTYPE html>
|
|
45
47
|
<html lang="en">
|
|
46
48
|
<head>
|
|
@@ -112,9 +114,34 @@ export const isTokenValid = (
|
|
|
112
114
|
providedToken: string | null,
|
|
113
115
|
): boolean => providedToken !== null && providedToken === expectedToken;
|
|
114
116
|
|
|
117
|
+
export const extractCookieToken = (
|
|
118
|
+
cookieHeader: string | undefined,
|
|
119
|
+
): string | null => {
|
|
120
|
+
if (typeof cookieHeader !== 'string' || cookieHeader.length === 0) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
for (const segment of cookieHeader.split(';')) {
|
|
124
|
+
const separatorIndex = segment.indexOf('=');
|
|
125
|
+
if (separatorIndex === -1) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const name = segment.slice(0, separatorIndex).trim();
|
|
129
|
+
if (name !== CONSOLE_TOKEN_COOKIE) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const value = segment.slice(separatorIndex + 1).trim();
|
|
133
|
+
if (value.length === 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return decodeURIComponent(value);
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
|
|
115
141
|
export const extractProvidedToken = (
|
|
116
142
|
queryToken: string | string[] | null,
|
|
117
143
|
headerToken: string | string[] | undefined,
|
|
144
|
+
cookieToken: string | null,
|
|
118
145
|
): string | null => {
|
|
119
146
|
if (typeof queryToken === 'string' && queryToken.length > 0) {
|
|
120
147
|
return queryToken;
|
|
@@ -122,9 +149,24 @@ export const extractProvidedToken = (
|
|
|
122
149
|
if (typeof headerToken === 'string' && headerToken.length > 0) {
|
|
123
150
|
return headerToken;
|
|
124
151
|
}
|
|
152
|
+
if (cookieToken !== null && cookieToken.length > 0) {
|
|
153
|
+
return cookieToken;
|
|
154
|
+
}
|
|
125
155
|
return null;
|
|
126
156
|
};
|
|
127
157
|
|
|
158
|
+
export const buildTokenCookie = (token: string): string =>
|
|
159
|
+
`${CONSOLE_TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Strict`;
|
|
160
|
+
|
|
161
|
+
export const buildKeylessLocation = (requestUrl: URL): string => {
|
|
162
|
+
const params = new URLSearchParams(requestUrl.searchParams);
|
|
163
|
+
params.delete('k');
|
|
164
|
+
const query = params.toString();
|
|
165
|
+
return query.length > 0
|
|
166
|
+
? `${requestUrl.pathname}?${query}`
|
|
167
|
+
: requestUrl.pathname;
|
|
168
|
+
};
|
|
169
|
+
|
|
128
170
|
const contentTypeForPath = (filePath: string): string => {
|
|
129
171
|
const extension = path.extname(filePath).toLowerCase();
|
|
130
172
|
return MIME_TYPES[extension] ?? 'application/octet-stream';
|
|
@@ -245,6 +287,7 @@ const serveBootstrapIndex = (response: http.ServerResponse): void => {
|
|
|
245
287
|
response.writeHead(200, {
|
|
246
288
|
'Content-Type': 'text/html; charset=utf-8',
|
|
247
289
|
'Cache-Control': 'no-store',
|
|
290
|
+
'Referrer-Policy': 'no-referrer',
|
|
248
291
|
});
|
|
249
292
|
response.end(PLACEHOLDER_INDEX_HTML);
|
|
250
293
|
};
|
|
@@ -263,10 +306,25 @@ const serveIndexHtml = (
|
|
|
263
306
|
response.writeHead(200, {
|
|
264
307
|
'Content-Type': 'text/html; charset=utf-8',
|
|
265
308
|
'Cache-Control': 'no-store',
|
|
309
|
+
'Referrer-Policy': 'no-referrer',
|
|
266
310
|
});
|
|
267
311
|
response.end(indexContent);
|
|
268
312
|
};
|
|
269
313
|
|
|
314
|
+
const redirectStrippingToken = (
|
|
315
|
+
response: http.ServerResponse,
|
|
316
|
+
requestUrl: URL,
|
|
317
|
+
token: string,
|
|
318
|
+
): void => {
|
|
319
|
+
response.writeHead(302, {
|
|
320
|
+
Location: buildKeylessLocation(requestUrl),
|
|
321
|
+
'Set-Cookie': buildTokenCookie(token),
|
|
322
|
+
'Referrer-Policy': 'no-referrer',
|
|
323
|
+
'Cache-Control': 'no-store',
|
|
324
|
+
});
|
|
325
|
+
response.end();
|
|
326
|
+
};
|
|
327
|
+
|
|
270
328
|
const sendJson = (
|
|
271
329
|
response: http.ServerResponse,
|
|
272
330
|
statusCode: number,
|
|
@@ -626,6 +684,7 @@ export const handleWebRequest = async (
|
|
|
626
684
|
const providedToken = extractProvidedToken(
|
|
627
685
|
requestUrl.searchParams.get('k'),
|
|
628
686
|
request.headers[CONSOLE_TOKEN_HEADER],
|
|
687
|
+
extractCookieToken(request.headers.cookie),
|
|
629
688
|
);
|
|
630
689
|
if (!isTokenValid(options.accessToken, providedToken)) {
|
|
631
690
|
sendUnauthorized(response);
|
|
@@ -646,6 +705,11 @@ export const handleWebRequest = async (
|
|
|
646
705
|
requestPath === '/index.html' ||
|
|
647
706
|
isConsoleAppRoute(requestPath)
|
|
648
707
|
) {
|
|
708
|
+
const queryToken = requestUrl.searchParams.get('k');
|
|
709
|
+
if (queryToken !== null && queryToken.length > 0) {
|
|
710
|
+
redirectStrippingToken(response, requestUrl, queryToken);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
649
713
|
serveIndexHtml(options, response);
|
|
650
714
|
return;
|
|
651
715
|
}
|
|
@@ -6,6 +6,7 @@ export const PREPARATION_STATUS_NAME = 'Preparation';
|
|
|
6
6
|
export const FAILED_PREPARATION_STATUS_NAME = 'Failed Preparation';
|
|
7
7
|
export const AWAITING_QUALITY_CHECK_STATUS_NAME = 'Awaiting Quality Check';
|
|
8
8
|
export const TODO_STATUS_NAME = 'Todo by human';
|
|
9
|
+
export const TODO_BY_AGENT_STATUS_NAME = 'Todo by agent';
|
|
9
10
|
export const PC_TODO_STATUS_NAME = 'PC Todo';
|
|
10
11
|
export const IN_TMUX_STATUS_NAME = 'In Tmux by human';
|
|
11
12
|
export const IN_TMUX_BY_AGENT_STATUS_NAME = 'In Tmux by agent';
|
|
@@ -47,6 +48,10 @@ export const REQUIRED_WORKFLOW_STATUSES: WorkflowStatusDefinition[] = [
|
|
|
47
48
|
name: TODO_STATUS_NAME,
|
|
48
49
|
color: 'PINK',
|
|
49
50
|
},
|
|
51
|
+
{
|
|
52
|
+
name: TODO_BY_AGENT_STATUS_NAME,
|
|
53
|
+
color: 'BLUE',
|
|
54
|
+
},
|
|
50
55
|
{
|
|
51
56
|
name: IN_TMUX_STATUS_NAME,
|
|
52
57
|
color: 'RED',
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
PC_TODO_STATUS_NAME,
|
|
20
20
|
PREPARATION_STATUS_NAME,
|
|
21
21
|
REQUIRED_WORKFLOW_STATUSES,
|
|
22
|
+
TODO_BY_AGENT_STATUS_NAME,
|
|
22
23
|
TODO_STATUS_NAME,
|
|
23
24
|
} from '../entities/WorkflowStatus';
|
|
24
25
|
|
|
@@ -77,7 +78,7 @@ const buildIssue = (overrides: Partial<Issue>): Issue => ({
|
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
describe('SetupTowerDefenceProjectUseCase', () => {
|
|
80
|
-
it('should define exactly the
|
|
81
|
+
it('should define exactly the 11 required statuses in the documented order with the documented colors and no descriptions', () => {
|
|
81
82
|
expect(REQUIRED_WORKFLOW_STATUSES).toEqual([
|
|
82
83
|
{ name: DEFAULT_STATUS_NAME, color: 'ORANGE' },
|
|
83
84
|
{ name: AWAITING_WORKSPACE_STATUS_NAME, color: 'BLUE' },
|
|
@@ -85,6 +86,7 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
85
86
|
{ name: FAILED_PREPARATION_STATUS_NAME, color: 'RED' },
|
|
86
87
|
{ name: AWAITING_QUALITY_CHECK_STATUS_NAME, color: 'GREEN' },
|
|
87
88
|
{ name: TODO_STATUS_NAME, color: 'PINK' },
|
|
89
|
+
{ name: TODO_BY_AGENT_STATUS_NAME, color: 'BLUE' },
|
|
88
90
|
{ name: IN_TMUX_STATUS_NAME, color: 'RED' },
|
|
89
91
|
{ name: IN_TMUX_BY_AGENT_STATUS_NAME, color: 'YELLOW' },
|
|
90
92
|
{ name: DONE_STATUS_NAME, color: 'PURPLE' },
|
|
@@ -253,6 +255,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
253
255
|
color: 'PINK',
|
|
254
256
|
description: '',
|
|
255
257
|
},
|
|
258
|
+
{
|
|
259
|
+
id: null,
|
|
260
|
+
name: TODO_BY_AGENT_STATUS_NAME,
|
|
261
|
+
color: 'BLUE',
|
|
262
|
+
description: '',
|
|
263
|
+
},
|
|
256
264
|
{
|
|
257
265
|
id: null,
|
|
258
266
|
name: IN_TMUX_STATUS_NAME,
|
|
@@ -324,6 +332,7 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
324
332
|
FAILED_PREPARATION_STATUS_NAME,
|
|
325
333
|
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
326
334
|
TODO_STATUS_NAME,
|
|
335
|
+
TODO_BY_AGENT_STATUS_NAME,
|
|
327
336
|
IN_TMUX_STATUS_NAME,
|
|
328
337
|
IN_TMUX_BY_AGENT_STATUS_NAME,
|
|
329
338
|
DONE_STATUS_NAME,
|
|
@@ -432,12 +441,90 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
432
441
|
const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
|
|
433
442
|
const inTmuxEntry = payload.find((s) => s.name === IN_TMUX_STATUS_NAME);
|
|
434
443
|
expect(inTmuxEntry).toBeDefined();
|
|
435
|
-
expect(inTmuxEntry?.id).toBe('id-
|
|
444
|
+
expect(inTmuxEntry?.id).toBe('id-7');
|
|
436
445
|
expect(payload.some((s) => s.name === LEGACY_IN_TMUX_STATUS_NAME)).toBe(
|
|
437
446
|
false,
|
|
438
447
|
);
|
|
439
448
|
});
|
|
440
449
|
|
|
450
|
+
it('should create the "Todo by agent" status with blue color when it is missing', async () => {
|
|
451
|
+
const mockProjectRepository =
|
|
452
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
453
|
+
const mockIssueRepository =
|
|
454
|
+
mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
|
|
455
|
+
const statuses = buildCanonicalStatuses().filter(
|
|
456
|
+
(s) => s.name !== TODO_BY_AGENT_STATUS_NAME,
|
|
457
|
+
);
|
|
458
|
+
const project = buildProject(statuses);
|
|
459
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
460
|
+
mockProjectRepository.updateStatusList.mockResolvedValue([]);
|
|
461
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
462
|
+
project: mock<Project>(),
|
|
463
|
+
issues: [],
|
|
464
|
+
cacheUsed: false,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
468
|
+
mockProjectRepository,
|
|
469
|
+
mockIssueRepository,
|
|
470
|
+
);
|
|
471
|
+
await useCase.run({ projectUrl: project.url });
|
|
472
|
+
|
|
473
|
+
expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
|
|
474
|
+
const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
|
|
475
|
+
const todoByAgentEntry = payload.find(
|
|
476
|
+
(s) => s.name === TODO_BY_AGENT_STATUS_NAME,
|
|
477
|
+
);
|
|
478
|
+
expect(todoByAgentEntry).toBeDefined();
|
|
479
|
+
expect(todoByAgentEntry?.id).toBeNull();
|
|
480
|
+
expect(todoByAgentEntry?.color).toBe('BLUE');
|
|
481
|
+
const names = payload.map((s) => s.name);
|
|
482
|
+
expect(names.indexOf(TODO_BY_AGENT_STATUS_NAME)).toBe(
|
|
483
|
+
names.indexOf(TODO_STATUS_NAME) + 1,
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should leave an already-present "Todo by agent" option unchanged by reusing its existing option ID', async () => {
|
|
488
|
+
const mockProjectRepository =
|
|
489
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
490
|
+
const mockIssueRepository =
|
|
491
|
+
mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
|
|
492
|
+
const statuses = buildCanonicalStatuses().map((s) => {
|
|
493
|
+
if (s.name === TODO_BY_AGENT_STATUS_NAME) {
|
|
494
|
+
return { ...s, id: 'preexisting-todo-agent-id' };
|
|
495
|
+
}
|
|
496
|
+
if (s.name === DEFAULT_STATUS_NAME) {
|
|
497
|
+
return { ...s, description: 'stale description' };
|
|
498
|
+
}
|
|
499
|
+
return s;
|
|
500
|
+
});
|
|
501
|
+
const project = buildProject(statuses);
|
|
502
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
503
|
+
mockProjectRepository.updateStatusList.mockResolvedValue([]);
|
|
504
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
505
|
+
project: mock<Project>(),
|
|
506
|
+
issues: [],
|
|
507
|
+
cacheUsed: false,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
511
|
+
mockProjectRepository,
|
|
512
|
+
mockIssueRepository,
|
|
513
|
+
);
|
|
514
|
+
await useCase.run({ projectUrl: project.url });
|
|
515
|
+
|
|
516
|
+
expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
|
|
517
|
+
const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
|
|
518
|
+
const todoByAgentEntry = payload.find(
|
|
519
|
+
(s) => s.name === TODO_BY_AGENT_STATUS_NAME,
|
|
520
|
+
);
|
|
521
|
+
expect(todoByAgentEntry?.id).toBe('preexisting-todo-agent-id');
|
|
522
|
+
expect(todoByAgentEntry?.color).toBe('BLUE');
|
|
523
|
+
expect(
|
|
524
|
+
payload.filter((s) => s.name === TODO_BY_AGENT_STATUS_NAME),
|
|
525
|
+
).toHaveLength(1);
|
|
526
|
+
});
|
|
527
|
+
|
|
441
528
|
it('should create the "In Tmux by agent" status with yellow color when it is missing', async () => {
|
|
442
529
|
const mockProjectRepository =
|
|
443
530
|
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
@@ -627,6 +714,7 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
627
714
|
FAILED_PREPARATION_STATUS_NAME,
|
|
628
715
|
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
629
716
|
TODO_STATUS_NAME,
|
|
717
|
+
TODO_BY_AGENT_STATUS_NAME,
|
|
630
718
|
IN_TMUX_STATUS_NAME,
|
|
631
719
|
IN_TMUX_BY_AGENT_STATUS_NAME,
|
|
632
720
|
DONE_STATUS_NAME,
|
|
@@ -764,6 +852,7 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
764
852
|
FAILED_PREPARATION_STATUS_NAME,
|
|
765
853
|
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
766
854
|
TODO_STATUS_NAME,
|
|
855
|
+
TODO_BY_AGENT_STATUS_NAME,
|
|
767
856
|
IN_TMUX_STATUS_NAME,
|
|
768
857
|
IN_TMUX_BY_AGENT_STATUS_NAME,
|
|
769
858
|
DONE_STATUS_NAME,
|
|
@@ -917,6 +1006,7 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
917
1006
|
FAILED_PREPARATION_STATUS_NAME,
|
|
918
1007
|
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
919
1008
|
TODO_STATUS_NAME,
|
|
1009
|
+
TODO_BY_AGENT_STATUS_NAME,
|
|
920
1010
|
IN_TMUX_STATUS_NAME,
|
|
921
1011
|
IN_TMUX_BY_AGENT_STATUS_NAME,
|
|
922
1012
|
DONE_STATUS_NAME,
|
|
@@ -23,6 +23,7 @@ const STATUS_OPTIONS: FieldOption[] = [
|
|
|
23
23
|
storyOption('st-failed', 'Failed Preparation', 'RED'),
|
|
24
24
|
storyOption('st-aqc', 'Awaiting Quality Check', 'GREEN'),
|
|
25
25
|
storyOption('st-todo', 'Todo by human', 'PINK'),
|
|
26
|
+
storyOption('st-todo-agent', 'Todo by agent', 'BLUE'),
|
|
26
27
|
storyOption('st-tmux', 'In Tmux by human', 'RED'),
|
|
27
28
|
storyOption('st-tmux-agent', 'In Tmux by agent', 'YELLOW'),
|
|
28
29
|
storyOption('st-done', 'Done', 'PURPLE'),
|
|
@@ -430,6 +431,7 @@ describe('GenerateConsoleListsUseCase', () => {
|
|
|
430
431
|
'Unread',
|
|
431
432
|
'In Tmux by human',
|
|
432
433
|
'In Tmux by agent',
|
|
434
|
+
'Todo by agent',
|
|
433
435
|
]) {
|
|
434
436
|
expect(names).not.toContain(excluded);
|
|
435
437
|
}
|
|
@@ -6,11 +6,15 @@ import { ImageFetcher } from './consoleImageProxy';
|
|
|
6
6
|
export declare const DEFAULT_WEB_PORT = 9981;
|
|
7
7
|
export declare const DEFAULT_DASHBOARD_PROJECT_NAMES: string[];
|
|
8
8
|
export declare const CONSOLE_TOKEN_HEADER = "x-pv-token";
|
|
9
|
+
export declare const CONSOLE_TOKEN_COOKIE = "pv_token";
|
|
9
10
|
export declare const hasDotSegment: (requestPath: string) => boolean;
|
|
10
11
|
export declare const requiresToken: (requestPath: string) => boolean;
|
|
11
12
|
export declare const isConsoleAppRoute: (requestPath: string) => boolean;
|
|
12
13
|
export declare const isTokenValid: (expectedToken: string, providedToken: string | null) => boolean;
|
|
13
|
-
export declare const
|
|
14
|
+
export declare const extractCookieToken: (cookieHeader: string | undefined) => string | null;
|
|
15
|
+
export declare const extractProvidedToken: (queryToken: string | string[] | null, headerToken: string | string[] | undefined, cookieToken: string | null) => string | null;
|
|
16
|
+
export declare const buildTokenCookie: (token: string) => string;
|
|
17
|
+
export declare const buildKeylessLocation: (requestUrl: URL) => string;
|
|
14
18
|
export type WebServerOptions = {
|
|
15
19
|
accessToken: string;
|
|
16
20
|
uiDistDir: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webServer.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/console/webServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,6DAA6D,CAAC;AAM9F,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EAQvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAEL,sBAAsB,EACtB,sBAAsB,EAMvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AAOtE,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,eAAO,MAAM,+BAA+B,UAA0B,CAAC;AAEvE,eAAO,MAAM,oBAAoB,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"webServer.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/console/webServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,6DAA6D,CAAC;AAM9F,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EAQvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAEL,sBAAsB,EACtB,sBAAsB,EAMvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AAOtE,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,eAAO,MAAM,+BAA+B,UAA0B,CAAC;AAEvE,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAEjD,eAAO,MAAM,oBAAoB,aAAa,CAAC;AAmC/C,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,OAGiB,CAAC;AAEtE,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,OAGrB,CAAC;AAIhC,eAAO,MAAM,iBAAiB,GAAI,aAAa,MAAM,KAAG,OAmBvD,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,eAAe,MAAM,EACrB,eAAe,MAAM,GAAG,IAAI,KAC3B,OAAoE,CAAC;AAExE,eAAO,MAAM,kBAAkB,GAC7B,cAAc,MAAM,GAAG,SAAS,KAC/B,MAAM,GAAG,IAoBX,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,YAAY,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,EACpC,aAAa,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAC1C,aAAa,MAAM,GAAG,IAAI,KACzB,MAAM,GAAG,IAWX,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,MAC0C,CAAC;AAE5F,eAAO,MAAM,oBAAoB,GAAI,YAAY,GAAG,KAAG,MAOtD,CAAC;AAuCF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACzC,cAAc,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC/C,kBAAkB,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACnD,oBAAoB,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACnD,sBAAsB,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;CACxD,CAAC;AAMF,eAAO,MAAM,sBAAsB,cAAc,CAAC;AAElD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAInD,eAAO,MAAM,wBAAwB,GACnC,cAAc,MAAM,EACpB,aAAa,MAAM,KAClB,MAAM,GAAG,IAWX,CAAC;AAEF,eAAO,MAAM,yBAAyB,GACpC,eAAe,MAAM,EACrB,aAAa,MAAM,KAClB,MAAM,GAAG,IAeX,CAAC;AA0WF,eAAO,MAAM,uBAAuB,GAClC,SAAS,gBAAgB,EACzB,aAAa,MAAM,KAClB,MAAM,GAAG,IAeX,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,SAAS,gBAAgB,EACzB,SAAS,IAAI,CAAC,eAAe,EAC7B,UAAU,IAAI,CAAC,cAAc,KAC5B,OAAO,CAAC,IAAI,CA8Ed,CAAC;AAcF,eAAO,MAAM,eAAe,GAAI,SAAS,gBAAgB,KAAG,IAAI,CAAC,MAM7D,CAAC;AAEL,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAAG;IACrD,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,SAAS,qBAAqB,KAC7B,OAAO,CAAC,IAAI,CAAC,MAAM,CAQlB,CAAC"}
|