admin/plugins: online

Massimo Melina committed May 11, 2022 at 00:11 UTC 7ff290d2c9e6ffb8e5af8938bc44905a185cb99e
14 files changed +756 -187
admin/src/Form.ts
+10 -4
@@ -18,7 +18,7 @@ import {
18 FormControl,
19 FormControlLabel, FormGroup, FormHelperText,
20 FormLabel,
21 - Grid,
21 + Grid, InputAdornment,
22 MenuItem, Radio,
23 RadioGroup,
24 Switch,
@@ -201,7 +201,7 @@ export interface FieldProps<T> {
201 [rest: string]: any
202 }
203
204 -export function StringField({ value, onChange, ...props }: FieldProps<string>) {
204 +export function StringField({ value, onChange, typing, start, end, ...props }: FieldProps<string>) {
205 const setter = () => value ?? ''
206 const [state, setState] = useState(setter)
207
@@ -215,7 +215,8 @@ export function StringField({ value, onChange, ...props }: FieldProps<string>) {
215 props.onChange?.(ev)
216 const val = ev.target.value
217 setState(val)
218 - if (document.activeElement !== ev.target) // autofill ongoing, don't wait onBlur event, just go
218 + if (typing // change state as the user is typing
219 + || document.activeElement !== ev.target) // autofill ongoing, don't wait onBlur event, just go
220 go(ev, val)
221 },
222 onKeyDown(ev) {
@@ -226,7 +227,12 @@ export function StringField({ value, onChange, ...props }: FieldProps<string>) {
227 onBlur(ev) {
228 props.onBlur?.(ev)
229 go(ev)
229 - }
230 + },
231 + InputProps: {
232 + startAdornment: start && h(InputAdornment, { position: 'start' }, start),
233 + endAdornment: end && h(InputAdornment, { position: 'end' }, end),
234 + ...props.InputProps,
235 + },
236 })
237
238 function go(event: any, val: string=state) {
admin/src/InstalledPlugins.ts new
+114
@@ -0,0 +1,114 @@
1 +import { apiCall, useApiList } from './api'
2 +import { createElement as h, Fragment } from 'react'
3 +import { Alert, Box, Tooltip } from '@mui/material'
4 +import { DataGrid } from '@mui/x-data-grid'
5 +import { Error, PlayCircle, Settings, StopCircle } from '@mui/icons-material'
6 +import { IconBtn } from './misc'
7 +import { formDialog, toast } from './dialog'
8 +import _ from 'lodash'
9 +import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from './Form'
10 +import { ArrayField } from './ArrayField'
11 +
12 +export default function InstalledPlugins() {
13 + const { list, error, initializing } = useApiList('get_plugins')
14 + if (error)
15 + return h(Alert, { severity: 'error' }, error)
16 + return h(DataGrid, {
17 + rows: list,
18 + loading: initializing,
19 + disableColumnSelector: true,
20 + disableColumnMenu: true,
21 + columns: [
22 + {
23 + field: 'id',
24 + headerName: "name",
25 + flex: .3,
26 + minWidth: 150,
27 + renderCell({ row, value }) {
28 + return h(Fragment, {},
29 + value,
30 + typeof row.badApi === 'string' && h(Tooltip, { title: row.badApi, children: h(Error, { color: 'warning', sx: { ml: 1 } }) })
31 + )
32 + }
33 + },
34 + {
35 + field: 'started',
36 + width: 180,
37 + valueFormatter: ({ value }) => !value ? "off" : new Date(value as string).toLocaleString()
38 + },
39 + {
40 + field: 'version',
41 + width: 70,
42 + },
43 + {
44 + field: 'description',
45 + flex: 1,
46 + },
47 + {
48 + field: "actions",
49 + width: 80,
50 + align: 'center',
51 + hideSortIcons: true,
52 + disableColumnMenu: true,
53 + renderCell({ row }) {
54 + const { config, id } = row
55 + return h('div', {},
56 + h(IconBtn, row.started ? {
57 + icon: StopCircle,
58 + title: `Stop ${id}`,
59 + onClick: () =>
60 + apiCall('set_plugin', { id, enabled: false }).then(() =>
61 + toast("Plugin is stopping", h(StopCircle, { color: 'warning' })))
62 + } : {
63 + icon: PlayCircle,
64 + title: `Start ${id}`,
65 + onClick: () =>
66 + apiCall('set_plugin', { id, enabled: true }).then(() =>
67 + toast("Plugin is starting", h(PlayCircle, { color: 'success' }))),
68 + }),
69 + h(IconBtn, {
70 + icon: Settings,
71 + title: "Configuration",
72 + disabled: !config,
73 + async onClick() {
74 + const pl = await apiCall('get_plugin', { id })
75 + const values = await formDialog({
76 + title: `${id} configuration`,
77 + fields: [ h(Box, {}, row.description), ...makeFields(config) ],
78 + values: pl.config,
79 + ...row.configDialog,
80 + })
81 + if (!values || _.isEqual(pl.config, values)) return
82 + await apiCall('set_plugin', { id, config: values })
83 + toast("Configuration saved")
84 + }
85 + }),
86 + )
87 + }
88 + },
89 + ]
90 + })
91 +}
92 +
93 +function makeFields(config: any) {
94 + return Object.entries(config).map(([k,o]: [string,any]) => {
95 + if (!_.isPlainObject(o))
96 + return o
97 + let { type, defaultValue, fields, ...rest } = o
98 + const comp = (type2comp as any)[type] as Field<any> | undefined
99 + if (comp === ArrayField)
100 + fields = makeFields(fields)
101 + if (defaultValue !== undefined && type === 'boolean')
102 + rest.placeholder = `Default value is ${JSON.stringify(defaultValue)}`
103 + return { k, comp, fields, ...rest }
104 + })
105 +}
106 +
107 +const type2comp = {
108 + string: StringField,
109 + number: NumberField,
110 + boolean: BoolField,
111 + select: SelectField,
112 + multiselect: MultiSelectField,
113 + array: ArrayField,
114 +}
admin/src/OnlinePlugins.ts new
+67
@@ -0,0 +1,67 @@
1 +import { apiCall, useApiList } from './api'
2 +import { Fragment, createElement as h, useState } from 'react'
3 +import { Alert } from '@mui/material'
4 +import { DataGrid } from '@mui/x-data-grid'
5 +import { IconBtn, spinner } from './misc'
6 +import { Download, Search } from '@mui/icons-material'
7 +import { confirmDialog, toast } from './dialog'
8 +import { StringField } from './Form'
9 +import { useDebounce } from 'use-debounce'
10 +
11 +export default function OnlinePlugins() {
12 + const [search, setSearch] = useState('')
13 + const [debouncedSearch] = useDebounce(search, 1000)
14 + const { list, error, initializing } = useApiList('search_online_plugins', { text: debouncedSearch })
15 + if (error)
16 + return h(Alert, { severity: 'error' }, error)
17 + return h(Fragment, {},
18 + h(StringField, {
19 + value: search,
20 + onChange: setSearch as any,
21 + start: h(Search),
22 + typing: true,
23 + label: "Search text"
24 + }),
25 + h(DataGrid, {
26 + rows: list,
27 + loading: initializing,
28 + columns: [
29 + {
30 + field: 'id',
31 + headerName: "name",
32 + flex: .3,
33 + minWidth: 150,
34 + },
35 + {
36 + field: 'version',
37 + width: 70,
38 + },
39 + {
40 + field: 'description',
41 + flex: 1,
42 + },
43 + {
44 + field: "actions",
45 + width: 80,
46 + align: 'center',
47 + hideSortIcons: true,
48 + disableColumnMenu: true,
49 + renderCell({ row }) {
50 + return row.downloading ? spinner() : h(IconBtn, {
51 + icon: Download,
52 + title: "Download",
53 + disabled: row.installed && "Already installed",
54 + async onClick() {
55 + if (!await confirmDialog("WARNING - Proceed only if you trust this author and this plugin")) return
56 + const { id } = row
57 + return apiCall('download_plugin', { id })
58 + .then(() => toast("Plugin downloaded: " + id))
59 + }
60 + })
61 + }
62 + },
63 + ]
64 + })
65 + )
66 +}
67 +
admin/src/PluginsPage.ts
+11 -111
@@ -1,116 +1,16 @@
1 // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 -import { createElement as h, Fragment } from "react"
4 -import { apiCall, useApiList } from './api'
5 -import { DataGrid } from '@mui/x-data-grid'
6 -import { Alert, Box, Tooltip } from '@mui/material'
7 -import { IconBtn } from './misc'
8 -import { Error, PlayCircle, Settings, StopCircle } from '@mui/icons-material'
9 -import { toast, formDialog } from './dialog'
10 -import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from './Form'
11 -import { ArrayField } from './ArrayField'
12 -import _ from "lodash"
3 +import { createElement as h, Fragment, useState } from "react"
4 +import { Tab, Tabs } from '@mui/material'
5 +import InstalledPlugins from "./InstalledPlugins"
6 +import OnlinePlugins from "./OnlinePlugins"
7
8 export default function PluginsPage() {
15 - const { list, error, initializing } = useApiList('get_plugins')
16 - if (error)
17 - return h(Alert, { severity: 'error' }, error)
18 - return h(DataGrid, {
19 - rows: list,
20 - loading: initializing,
21 - disableColumnSelector: true,
22 - disableColumnMenu: true,
23 - columns: [
24 - {
25 - field: 'id',
26 - headerName: "name",
27 - flex: .3,
28 - minWidth: 150,
29 - renderCell({ row, value }) {
30 - return h(Fragment, {},
31 - value,
32 - typeof row.badApi === 'string' && h(Tooltip, { title: row.badApi, children: h(Error, { color: 'warning', sx: { ml: 1 } }) })
33 - )
34 - }
35 - },
36 - {
37 - field: 'started',
38 - width: 180,
39 - valueFormatter: ({ value }) => !value ? "off" : new Date(value as string).toLocaleString()
40 - },
41 - {
42 - field: 'version',
43 - width: 70,
44 - },
45 - {
46 - field: 'description',
47 - flex: 1,
48 - },
49 - {
50 - field: "actions",
51 - width: 80,
52 - align: 'center',
53 - hideSortIcons: true,
54 - disableColumnMenu: true,
55 - renderCell({ row }) {
56 - const { config, id } = row
57 - return h('div', {},
58 - h(IconBtn, row.started ? {
59 - icon: StopCircle,
60 - title: `Stop ${id}`,
61 - onClick: () =>
62 - apiCall('set_plugin', { id, enabled: false }).then(() =>
63 - toast("Plugin is stopping", h(StopCircle, { color: 'warning' })))
64 - } : {
65 - icon: PlayCircle,
66 - title: `Start ${id}`,
67 - onClick: () =>
68 - apiCall('set_plugin', { id, enabled: true }).then(() =>
69 - toast("Plugin is starting", h(PlayCircle, { color: 'success' }))),
70 - }),
71 - h(IconBtn, {
72 - icon: Settings,
73 - title: "Configuration",
74 - disabled: !config,
75 - async onClick() {
76 - const pl = await apiCall('get_plugin', { id })
77 - const values = await formDialog({
78 - title: `${id} configuration`,
79 - fields: [ h(Box, {}, row.description), ...makeFields(config) ],
80 - values: pl.config,
81 - ...row.configDialog,
82 - })
83 - if (!values || _.isEqual(pl.config, values)) return
84 - await apiCall('set_plugin', { id, config: values })
85 - toast("Configuration saved")
86 - }
87 - }),
88 - )
89 - }
90 - },
91 - ]
92 - })
93 -}
94 -
95 -function makeFields(config: any) {
96 - return Object.entries(config).map(([k,o]: [string,any]) => {
97 - if (!_.isPlainObject(o))
98 - return o
99 - let { type, defaultValue, fields, ...rest } = o
100 - const comp = (type2comp as any)[type] as Field<any> | undefined
101 - if (comp === ArrayField)
102 - fields = makeFields(fields)
103 - if (defaultValue !== undefined && type === 'boolean')
104 - rest.placeholder = `Default value is ${JSON.stringify(defaultValue)}`
105 - return { k, comp, fields, ...rest }
106 - })
107 -}
108 -
109 -const type2comp = {
110 - string: StringField,
111 - number: NumberField,
112 - boolean: BoolField,
113 - select: SelectField,
114 - multiselect: MultiSelectField,
115 - array: ArrayField,
9 + const [tab, setTab] = useState(0)
10 + const tabs = ["Installed", "Search online"]
11 + return h(Fragment, {},
12 + h(Tabs, { value: tab, onChange(ev,i){ setTab(i) } },
13 + tabs.map(f => h(Tab, { label: f, key: f })) ),
14 + h(tab ? OnlinePlugins : InstalledPlugins)
15 + )
16 }
admin/src/api.ts
+2 -1
@@ -120,6 +120,7 @@ export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { addId=fal
120 setList(list => [ ...list, ...chunk ])
121 }
122 setLoading(true)
123 + setInitializing(true)
124 setList([])
125 const timer = setInterval(flush, 1000)
126 const src = apiEvents(cmd, params, (type, data) => {
@@ -133,7 +134,7 @@ export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { addId=fal
134 case 'msg':
135 if (src?.readyState === src?.CLOSED)
136 return stop()
136 - if (data === 'init') {
137 + if (data === 'init' || data === 'end') {
138 flush()
139 setInitializing(false)
140 return
admin/src/misc.ts
+5 -2
@@ -28,10 +28,13 @@ export function modifiedSx(is: boolean) {
28 return is ? { outline: '2px solid' } : undefined
29 }
30
31 -export function IconBtn({ title, icon, onClick, ...rest }: { title?: string, icon: SvgIconComponent, [rest:string]:any }) {
31 +interface IconBtnProps { title?: string, icon: SvgIconComponent, disabled?: boolean | string, [rest:string]:any }
32 +export function IconBtn({ title, icon, onClick, disabled, ...rest }: IconBtnProps) {
33 const [loading, setLoading] = useStateMounted(false)
34 + if (typeof disabled === 'string')
35 + title = disabled
36 let ret: ReturnType<FC> = h(IconButton, {
34 - disabled: loading,
37 + disabled: loading || Boolean(disabled),
38 ...rest,
39 onClick() {
40 const ret = onClick?.apply(this,arguments)
dev-plugins.md
+10
@@ -131,3 +131,13 @@ This is a list of available frontend events, with respective parameters and outp
131 - `m?: Date` modified-time.
132 - output `string | void`
133
134 +## Publish your plug-in
135 +
136 +Suggested method for publishing is to have a dedicated repository on github, with topic `hfs-plugin`.
137 +To set the topic go on the repo home and click on the gear icon near the "About" box.
138 +Be sure to also fill the "description" field, especially with words that people may search for.
139 +
140 +The files intended to be installed must go in a folder named `dist`.
141 +You can keep other files outside.
142 +
143 +You can refer to this dummy plugin for reference https://github.com/rejetto/demo-plugin .
package-lock.json
+349 -47
@@ -5180,6 +5180,15 @@
5180 "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
5181 "dev": true
5182 },
5183 + "node_modules/@types/unzipper": {
5184 + "version": "0.10.5",
5185 + "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.5.tgz",
5186 + "integrity": "sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==",
5187 + "dev": true,
5188 + "dependencies": {
5189 + "@types/node": "*"
5190 + }
5191 + },
5192 "node_modules/@types/ws": {
5193 "version": "8.5.3",
5194 "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -6463,8 +6472,7 @@
6472 "node_modules/balanced-match": {
6473 "version": "1.0.2",
6474 "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
6466 - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
6467 - "dev": true
6475 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
6476 },
6477 "node_modules/base64-js": {
6478 "version": "1.5.1",
@@ -6506,6 +6514,14 @@
6514 "node": ">= 8.0.0"
6515 }
6516 },
6517 + "node_modules/big-integer": {
6518 + "version": "1.6.51",
6519 + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
6520 + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
6521 + "engines": {
6522 + "node": ">=0.6"
6523 + }
6524 + },
6525 "node_modules/big.js": {
6526 "version": "5.2.2",
6527 "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -6515,6 +6531,18 @@
6531 "node": "*"
6532 }
6533 },
6534 + "node_modules/binary": {
6535 + "version": "0.3.0",
6536 + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
6537 + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
6538 + "dependencies": {
6539 + "buffers": "~0.1.1",
6540 + "chainsaw": "~0.1.0"
6541 + },
6542 + "engines": {
6543 + "node": "*"
6544 + }
6545 + },
6546 "node_modules/binary-extensions": {
6547 "version": "2.2.0",
6548 "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -6741,7 +6769,6 @@
6769 "version": "1.1.11",
6770 "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
6771 "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
6744 - "dev": true,
6772 "dependencies": {
6773 "balanced-match": "^1.0.0",
6774 "concat-map": "0.0.1"
@@ -6844,6 +6871,22 @@
6871 "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
6872 "dev": true
6873 },
6874 + "node_modules/buffer-indexof-polyfill": {
6875 + "version": "1.0.2",
6876 + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
6877 + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
6878 + "engines": {
6879 + "node": ">=0.10"
6880 + }
6881 + },
6882 + "node_modules/buffers": {
6883 + "version": "0.1.1",
6884 + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
6885 + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=",
6886 + "engines": {
6887 + "node": ">=0.2.0"
6888 + }
6889 + },
6890 "node_modules/builtin-modules": {
6891 "version": "3.2.0",
6892 "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
@@ -7029,6 +7072,17 @@
7072 "node": ">=4"
7073 }
7074 },
7075 + "node_modules/chainsaw": {
7076 + "version": "0.1.0",
7077 + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
7078 + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
7079 + "dependencies": {
7080 + "traverse": ">=0.3.0 <0.4"
7081 + },
7082 + "engines": {
7083 + "node": "*"
7084 + }
7085 + },
7086 "node_modules/chalk": {
7087 "version": "2.4.2",
7088 "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -7371,8 +7425,7 @@
7425 "node_modules/concat-map": {
7426 "version": "0.0.1",
7427 "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
7374 - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
7375 - "dev": true
7428 + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
7429 },
7430 "node_modules/configstore": {
7431 "version": "5.0.1",
@@ -8581,6 +8634,36 @@
8634 "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
8635 "dev": true
8636 },
8637 + "node_modules/duplexer2": {
8638 + "version": "0.1.4",
8639 + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
8640 + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
8641 + "dependencies": {
8642 + "readable-stream": "^2.0.2"
8643 + }
8644 + },
8645 + "node_modules/duplexer2/node_modules/readable-stream": {
8646 + "version": "2.3.7",
8647 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
8648 + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
8649 + "dependencies": {
8650 + "core-util-is": "~1.0.0",
8651 + "inherits": "~2.0.3",
8652 + "isarray": "~1.0.0",
8653 + "process-nextick-args": "~2.0.0",
8654 + "safe-buffer": "~5.1.1",
8655 + "string_decoder": "~1.1.1",
8656 + "util-deprecate": "~1.0.1"
8657 + }
8658 + },
8659 + "node_modules/duplexer2/node_modules/string_decoder": {
8660 + "version": "1.1.1",
8661 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
8662 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
8663 + "dependencies": {
8664 + "safe-buffer": "~5.1.0"
8665 + }
8666 + },
8667 "node_modules/duplexer3": {
8668 "version": "0.1.4",
8669 "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -10425,8 +10508,7 @@
10508 "node_modules/fs.realpath": {
10509 "version": "1.0.0",
10510 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
10428 - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
10429 - "dev": true
10511 + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
10512 },
10513 "node_modules/fsevents": {
10514 "version": "2.3.2",
@@ -10442,6 +10524,31 @@
10524 "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
10525 }
10526 },
10527 + "node_modules/fstream": {
10528 + "version": "1.0.12",
10529 + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
10530 + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
10531 + "dependencies": {
10532 + "graceful-fs": "^4.1.2",
10533 + "inherits": "~2.0.0",
10534 + "mkdirp": ">=0.5 0",
10535 + "rimraf": "2"
10536 + },
10537 + "engines": {
10538 + "node": ">=0.6"
10539 + }
10540 + },
10541 + "node_modules/fstream/node_modules/rimraf": {
10542 + "version": "2.7.1",
10543 + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
10544 + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
10545 + "dependencies": {
10546 + "glob": "^7.1.3"
10547 + },
10548 + "bin": {
10549 + "rimraf": "bin.js"
10550 + }
10551 + },
10552 "node_modules/function-bind": {
10553 "version": "1.1.1",
10554 "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -10540,7 +10647,6 @@
10647 "version": "7.2.0",
10648 "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
10649 "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
10543 - "dev": true,
10650 "dependencies": {
10651 "fs.realpath": "^1.0.0",
10652 "inflight": "^1.0.4",
@@ -10577,7 +10683,6 @@
10683 "version": "3.1.2",
10684 "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
10685 "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
10580 - "dev": true,
10686 "dependencies": {
10687 "brace-expansion": "^1.1.7"
10688 },
@@ -10712,8 +10817,7 @@
10817 "node_modules/graceful-fs": {
10818 "version": "4.2.10",
10819 "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
10715 - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
10716 - "dev": true
10820 + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
10821 },
10822 "node_modules/growl": {
10823 "version": "1.10.5",
@@ -11309,7 +11413,6 @@
11413 "version": "1.0.6",
11414 "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
11415 "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
11312 - "dev": true,
11416 "dependencies": {
11417 "once": "^1.3.0",
11418 "wrappy": "1"
@@ -11846,8 +11949,7 @@
11949 "node_modules/isarray": {
11950 "version": "1.0.0",
11951 "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
11849 - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
11850 - "dev": true
11952 + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
11953 },
11954 "node_modules/isexe": {
11955 "version": "2.0.0",
@@ -14600,6 +14702,11 @@
14702 "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
14703 "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
14704 },
14705 + "node_modules/listenercount": {
14706 + "version": "1.0.1",
14707 + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
14708 + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
14709 + },
14710 "node_modules/loader-runner": {
14711 "version": "4.3.0",
14712 "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -15150,7 +15257,6 @@
15257 "version": "0.5.6",
15258 "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
15259 "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
15153 - "dev": true,
15260 "dependencies": {
15261 "minimist": "^1.2.6"
15262 },
@@ -15679,7 +15785,6 @@
15785 "version": "1.4.0",
15786 "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
15787 "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
15682 - "dev": true,
15788 "dependencies": {
15789 "wrappy": "1"
15790 }
@@ -15888,7 +15993,6 @@
15993 "version": "1.0.1",
15994 "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
15995 "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
15891 - "dev": true,
15996 "engines": {
15997 "node": ">=0.10.0"
15998 }
@@ -17371,8 +17475,7 @@
17475 "node_modules/process-nextick-args": {
17476 "version": "2.0.1",
17477 "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
17374 - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
17375 - "dev": true
17478 + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
17479 },
17480 "node_modules/promise": {
17481 "version": "8.1.0",
@@ -18991,6 +19094,11 @@
19094 "node": ">= 0.8.0"
19095 }
19096 },
19097 + "node_modules/setimmediate": {
19098 + "version": "1.0.5",
19099 + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
19100 + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
19101 + },
19102 "node_modules/setprototypeof": {
19103 "version": "1.2.0",
19104 "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -20041,6 +20149,14 @@
20149 "node": ">=8"
20150 }
20151 },
20152 + "node_modules/traverse": {
20153 + "version": "0.3.9",
20154 + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
20155 + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=",
20156 + "engines": {
20157 + "node": "*"
20158 + }
20159 + },
20160 "node_modules/trim-newlines": {
20161 "version": "3.0.1",
20162 "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
@@ -20360,6 +20476,50 @@
20476 "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
20477 "dev": true
20478 },
20479 + "node_modules/unzipper": {
20480 + "version": "0.10.11",
20481 + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
20482 + "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
20483 + "dependencies": {
20484 + "big-integer": "^1.6.17",
20485 + "binary": "~0.3.0",
20486 + "bluebird": "~3.4.1",
20487 + "buffer-indexof-polyfill": "~1.0.0",
20488 + "duplexer2": "~0.1.4",
20489 + "fstream": "^1.0.12",
20490 + "graceful-fs": "^4.2.2",
20491 + "listenercount": "~1.0.1",
20492 + "readable-stream": "~2.3.6",
20493 + "setimmediate": "~1.0.4"
20494 + }
20495 + },
20496 + "node_modules/unzipper/node_modules/bluebird": {
20497 + "version": "3.4.7",
20498 + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
20499 + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
20500 + },
20501 + "node_modules/unzipper/node_modules/readable-stream": {
20502 + "version": "2.3.7",
20503 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
20504 + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
20505 + "dependencies": {
20506 + "core-util-is": "~1.0.0",
20507 + "inherits": "~2.0.3",
20508 + "isarray": "~1.0.0",
20509 + "process-nextick-args": "~2.0.0",
20510 + "safe-buffer": "~5.1.1",
20511 + "string_decoder": "~1.1.1",
20512 + "util-deprecate": "~1.0.1"
20513 + }
20514 + },
20515 + "node_modules/unzipper/node_modules/string_decoder": {
20516 + "version": "1.1.1",
20517 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
20518 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
20519 + "dependencies": {
20520 + "safe-buffer": "~5.1.0"
20521 + }
20522 + },
20523 "node_modules/upath": {
20524 "version": "1.2.0",
20525 "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
@@ -20526,8 +20686,7 @@
20686 "node_modules/util-deprecate": {
20687 "version": "1.0.2",
20688 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
20529 - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
20530 - "dev": true
20689 + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
20690 },
20691 "node_modules/util.promisify": {
20692 "version": "1.0.1",
@@ -21615,8 +21774,7 @@
21774 "node_modules/wrappy": {
21775 "version": "1.0.2",
21776 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
21618 - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
21619 - "dev": true
21777 + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
21778 },
21779 "node_modules/write-file-atomic": {
21780 "version": "3.0.3",
@@ -21826,6 +21984,7 @@
21984 "minimist": "^1.2.6",
21985 "open": "^8.4.0",
21986 "tssrp6a": "^3.0.0",
21987 + "unzipper": "^0.10.11",
21988 "yaml": "^2.0.0-10"
21989 },
21990 "bin": {
@@ -21844,6 +22003,7 @@
22003 "@types/minimist": "^1.2.2",
22004 "@types/mocha": "^9.0.0",
22005 "@types/node": "^16.11.12",
22006 + "@types/unzipper": "^0.10.5",
22007 "axios": "^0.24.0",
22008 "koa-better-http-proxy": "^0.2.9",
22009 "mocha": "^9.1.3",
@@ -23523,6 +23683,7 @@
23683 "@types/minimist": "^1.2.2",
23684 "@types/mocha": "^9.0.0",
23685 "@types/node": "^16.11.12",
23686 + "@types/unzipper": "^0.10.5",
23687 "axios": "^0.24.0",
23688 "buffer-crc32": "https://github.com/rejetto/buffer-crc32.git",
23689 "cidr-tools": "^4.3.0",
@@ -23542,6 +23703,7 @@
23703 "open": "^8.4.0",
23704 "ts-node": "^10.4.0",
23705 "tssrp6a": "^3.0.0",
23706 + "unzipper": "^0.10.11",
23707 "yaml": "^2.0.0-10"
23708 }
23709 },
@@ -25517,6 +25679,15 @@
25679 "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
25680 "dev": true
25681 },
25682 + "@types/unzipper": {
25683 + "version": "0.10.5",
25684 + "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.5.tgz",
25685 + "integrity": "sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==",
25686 + "dev": true,
25687 + "requires": {
25688 + "@types/node": "*"
25689 + }
25690 + },
25691 "@types/ws": {
25692 "version": "8.5.3",
25693 "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -26479,8 +26650,7 @@
26650 "balanced-match": {
26651 "version": "1.0.2",
26652 "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
26482 - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
26483 - "dev": true
26653 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
26654 },
26655 "base64-js": {
26656 "version": "1.5.1",
@@ -26505,12 +26675,26 @@
26675 "tryer": "^1.0.1"
26676 }
26677 },
26678 + "big-integer": {
26679 + "version": "1.6.51",
26680 + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
26681 + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
26682 + },
26683 "big.js": {
26684 "version": "5.2.2",
26685 "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
26686 "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
26687 "dev": true
26688 },
26689 + "binary": {
26690 + "version": "0.3.0",
26691 + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
26692 + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
26693 + "requires": {
26694 + "buffers": "~0.1.1",
26695 + "chainsaw": "~0.1.0"
26696 + }
26697 + },
26698 "binary-extensions": {
26699 "version": "2.2.0",
26700 "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -26686,7 +26870,6 @@
26870 "version": "1.1.11",
26871 "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
26872 "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
26689 - "dev": true,
26873 "requires": {
26874 "balanced-match": "^1.0.0",
26875 "concat-map": "0.0.1"
@@ -26752,6 +26935,16 @@
26935 "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
26936 "dev": true
26937 },
26938 + "buffer-indexof-polyfill": {
26939 + "version": "1.0.2",
26940 + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
26941 + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="
26942 + },
26943 + "buffers": {
26944 + "version": "0.1.1",
26945 + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
26946 + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
26947 + },
26948 "builtin-modules": {
26949 "version": "3.2.0",
26950 "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
@@ -26881,6 +27074,14 @@
27074 "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==",
27075 "dev": true
27076 },
27077 + "chainsaw": {
27078 + "version": "0.1.0",
27079 + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
27080 + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
27081 + "requires": {
27082 + "traverse": ">=0.3.0 <0.4"
27083 + }
27084 + },
27085 "chalk": {
27086 "version": "2.4.2",
27087 "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -27154,8 +27355,7 @@
27355 "concat-map": {
27356 "version": "0.0.1",
27357 "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
27157 - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
27158 - "dev": true
27358 + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
27359 },
27360 "configstore": {
27361 "version": "5.0.1",
@@ -28053,6 +28253,38 @@
28253 "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
28254 "dev": true
28255 },
28256 + "duplexer2": {
28257 + "version": "0.1.4",
28258 + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
28259 + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
28260 + "requires": {
28261 + "readable-stream": "^2.0.2"
28262 + },
28263 + "dependencies": {
28264 + "readable-stream": {
28265 + "version": "2.3.7",
28266 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
28267 + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
28268 + "requires": {
28269 + "core-util-is": "~1.0.0",
28270 + "inherits": "~2.0.3",
28271 + "isarray": "~1.0.0",
28272 + "process-nextick-args": "~2.0.0",
28273 + "safe-buffer": "~5.1.1",
28274 + "string_decoder": "~1.1.1",
28275 + "util-deprecate": "~1.0.1"
28276 + }
28277 + },
28278 + "string_decoder": {
28279 + "version": "1.1.1",
28280 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
28281 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
28282 + "requires": {
28283 + "safe-buffer": "~5.1.0"
28284 + }
28285 + }
28286 + }
28287 + },
28288 "duplexer3": {
28289 "version": "0.1.4",
28290 "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -29429,8 +29661,7 @@
29661 "fs.realpath": {
29662 "version": "1.0.0",
29663 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
29432 - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
29433 - "dev": true
29664 + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
29665 },
29666 "fsevents": {
29667 "version": "2.3.2",
@@ -29439,6 +29670,27 @@
29670 "dev": true,
29671 "optional": true
29672 },
29673 + "fstream": {
29674 + "version": "1.0.12",
29675 + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
29676 + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
29677 + "requires": {
29678 + "graceful-fs": "^4.1.2",
29679 + "inherits": "~2.0.0",
29680 + "mkdirp": ">=0.5 0",
29681 + "rimraf": "2"
29682 + },
29683 + "dependencies": {
29684 + "rimraf": {
29685 + "version": "2.7.1",
29686 + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
29687 + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
29688 + "requires": {
29689 + "glob": "^7.1.3"
29690 + }
29691 + }
29692 + }
29693 + },
29694 "function-bind": {
29695 "version": "1.1.1",
29696 "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -29510,7 +29762,6 @@
29762 "version": "7.2.0",
29763 "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
29764 "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
29513 - "dev": true,
29765 "requires": {
29766 "fs.realpath": "^1.0.0",
29767 "inflight": "^1.0.4",
@@ -29524,7 +29775,6 @@
29775 "version": "3.1.2",
29776 "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
29777 "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
29527 - "dev": true,
29778 "requires": {
29779 "brace-expansion": "^1.1.7"
29780 }
@@ -29645,8 +29895,7 @@
29895 "graceful-fs": {
29896 "version": "4.2.10",
29897 "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
29648 - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
29649 - "dev": true
29898 + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
29899 },
29900 "growl": {
29901 "version": "1.10.5",
@@ -30082,7 +30331,6 @@
30331 "version": "1.0.6",
30332 "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
30333 "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
30085 - "dev": true,
30334 "requires": {
30335 "once": "^1.3.0",
30336 "wrappy": "1"
@@ -30461,8 +30709,7 @@
30709 "isarray": {
30710 "version": "1.0.0",
30711 "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
30464 - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
30465 - "dev": true
30712 + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
30713 },
30714 "isexe": {
30715 "version": "2.0.0",
@@ -32571,6 +32818,11 @@
32818 "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
32819 "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
32820 },
32821 + "listenercount": {
32822 + "version": "1.0.1",
32823 + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
32824 + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
32825 + },
32826 "loader-runner": {
32827 "version": "4.3.0",
32828 "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -32990,7 +33242,6 @@
33242 "version": "0.5.6",
33243 "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
33244 "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
32993 - "dev": true,
33245 "requires": {
33246 "minimist": "^1.2.6"
33247 }
@@ -33377,7 +33628,6 @@
33628 "version": "1.4.0",
33629 "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
33630 "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
33380 - "dev": true,
33631 "requires": {
33632 "wrappy": "1"
33633 }
@@ -33531,8 +33781,7 @@
33781 "path-is-absolute": {
33782 "version": "1.0.1",
33783 "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
33534 - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
33535 - "dev": true
33784 + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
33785 },
33786 "path-key": {
33787 "version": "3.1.1",
@@ -34493,8 +34742,7 @@
34742 "process-nextick-args": {
34743 "version": "2.0.1",
34744 "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
34496 - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
34497 - "dev": true
34745 + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
34746 },
34747 "promise": {
34748 "version": "8.1.0",
@@ -35724,6 +35972,11 @@
35972 "send": "0.18.0"
35973 }
35974 },
35975 + "setimmediate": {
35976 + "version": "1.0.5",
35977 + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
35978 + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
35979 + },
35980 "setprototypeof": {
35981 "version": "1.2.0",
35982 "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -36543,6 +36796,11 @@
36796 "punycode": "^2.1.1"
36797 }
36798 },
36799 + "traverse": {
36800 + "version": "0.3.9",
36801 + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
36802 + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk="
36803 + },
36804 "trim-newlines": {
36805 "version": "3.0.1",
36806 "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
@@ -36776,6 +37034,52 @@
37034 "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
37035 "dev": true
37036 },
37037 + "unzipper": {
37038 + "version": "0.10.11",
37039 + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
37040 + "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
37041 + "requires": {
37042 + "big-integer": "^1.6.17",
37043 + "binary": "~0.3.0",
37044 + "bluebird": "~3.4.1",
37045 + "buffer-indexof-polyfill": "~1.0.0",
37046 + "duplexer2": "~0.1.4",
37047 + "fstream": "^1.0.12",
37048 + "graceful-fs": "^4.2.2",
37049 + "listenercount": "~1.0.1",
37050 + "readable-stream": "~2.3.6",
37051 + "setimmediate": "~1.0.4"
37052 + },
37053 + "dependencies": {
37054 + "bluebird": {
37055 + "version": "3.4.7",
37056 + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
37057 + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
37058 + },
37059 + "readable-stream": {
37060 + "version": "2.3.7",
37061 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
37062 + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
37063 + "requires": {
37064 + "core-util-is": "~1.0.0",
37065 + "inherits": "~2.0.3",
37066 + "isarray": "~1.0.0",
37067 + "process-nextick-args": "~2.0.0",
37068 + "safe-buffer": "~5.1.1",
37069 + "string_decoder": "~1.1.1",
37070 + "util-deprecate": "~1.0.1"
37071 + }
37072 + },
37073 + "string_decoder": {
37074 + "version": "1.1.1",
37075 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
37076 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
37077 + "requires": {
37078 + "safe-buffer": "~5.1.0"
37079 + }
37080 + }
37081 + }
37082 + },
37083 "upath": {
37084 "version": "1.2.0",
37085 "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
@@ -36897,8 +37201,7 @@
37201 "util-deprecate": {
37202 "version": "1.0.2",
37203 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
36900 - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
36901 - "dev": true
37204 + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
37205 },
37206 "util.promisify": {
37207 "version": "1.0.1",
@@ -37756,8 +38059,7 @@
38059 "wrappy": {
38060 "version": "1.0.2",
38061 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
37759 - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
37760 - "dev": true
38062 + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
38063 },
38064 "write-file-atomic": {
38065 "version": "3.0.3",
server/package.json
+3 -1
@@ -25,7 +25,8 @@
25 "minimist": "^1.2.6",
26 "open": "^8.4.0",
27 "tssrp6a": "^3.0.0",
28 - "yaml": "^2.0.0-10"
28 + "yaml": "^2.0.0-10",
29 + "unzipper": "^0.10.11"
30 },
31 "devDependencies": {
32 "@types/archiver": "^5.1.1",
@@ -40,6 +41,7 @@
41 "@types/minimist": "^1.2.2",
42 "@types/mocha": "^9.0.0",
43 "@types/node": "^16.11.12",
44 + "@types/unzipper": "^0.10.5",
45 "axios": "^0.24.0",
46 "koa-better-http-proxy": "^0.2.9",
47 "mocha": "^9.1.3",
server/src/api.plugins.ts
+110 -3
@@ -4,12 +4,20 @@ import {
4 getAvailablePlugins,
5 getPluginConfigFields,
6 mapPlugins,
7 - Plugin, pluginsConfig
7 + parsePluginSource,
8 + Plugin, pluginsConfig,
9 + PATH as PLUGINS_PATH
10 } from './plugins'
11 import _ from 'lodash'
12 import assert from 'assert'
11 -import { objSameKeys, same } from './misc'
12 -import { ApiHandlers, sendList } from './apiMiddleware'
13 +import { httpsString, httpsStream, objSameKeys, onOff, same } from './misc'
14 +import { ApiError, ApiHandlers, sendList } from './apiMiddleware'
15 +import events from './events'
16 +import unzipper from 'unzipper'
17 +import { createWriteStream } from 'fs'
18 +import { access, mkdir } from 'fs/promises'
19 +
20 +const DIST_ROOT = 'dist/'
21
22 const apis: ApiHandlers = {
23 get_plugins({}, ctx) {
@@ -57,6 +65,105 @@ const apis: ApiHandlers = {
65 }
66 },
67
68 + search_online_plugins({ text }, ctx) {
69 + const list = sendList()
70 + const repo2id = Object.fromEntries(getAvailablePlugins().map(x => [x.repo, x.id]))
71 + Object.assign(repo2id, Object.fromEntries(mapPlugins(x => [x.getData().repo, x.id]))) // started ones
72 + apiGithub('search/repositories?q=topic:hfs-plugin+' + encodeURI(text)).then(res => {
73 + const jobs = []
74 + for (const it of res.items) {
75 + const repo = it.full_name
76 + const job = httpsString(`https://raw.githubusercontent.com/${repo}/master/${DIST_ROOT}plugin.js`).then(res => {
77 + if (!res.ok)
78 + throw res.statusCode
79 + const pl = parsePluginSource(repo, res.body) // use 'repo' as 'id' client-side
80 + if (!pl.apiRequired || pl.badApi) return
81 + Object.assign(pl, { // inject some extra useful fields
82 + downloading: downloading[repo],
83 + installed: repo2id[repo]
84 + })
85 + list.add(pl)
86 + // watch for events about this plugin, until this request is closed
87 + ctx.req.on('close', onOff(events, {
88 + pluginInstalled: p => {
89 + if (p.repo === repo)
90 + list.update({ id: repo }, { installed: true })
91 + },
92 + pluginUninstalled: id => {
93 + if (repo === _.findKey(repo2id, x => x === id))
94 + list.update({ id: repo }, { installed: false })
95 + },
96 + ['pluginDownload_'+repo](status) {
97 + list.update({ id: repo }, { downloading: status ?? null })
98 + }
99 + }) )
100 + })
101 + jobs.push(job)
102 + }
103 + Promise.allSettled(jobs).then(() => list.end())
104 + })
105 + return list.return
106 + },
107 +
108 + async download_plugin({ id }) {
109 + if (downloading[id])
110 + return new ApiError(409, "already downloading")
111 + downloadPlugin(id).then()
112 + return {}
113 + },
114 +
115 +}
116 +
117 +type DownloadStatus = true | undefined
118 +const downloading: Record<string, DownloadStatus> = {}
119 +
120 +function downloadProgress(id: string, status: DownloadStatus) {
121 + if (status === undefined)
122 + delete downloading[id]
123 + else
124 + downloading[id] = status
125 + events.emit('pluginDownload_'+id, status)
126 +}
127 +
128 +async function downloadPlugin(repo: string) {
129 + downloadProgress(repo, true)
130 + const rec = await apiGithub('repos/'+repo)
131 + const url = `https://github.com/${repo}/archive/${rec.default_branch}.zip`
132 + const res = await httpsStream(url)
133 + const repo2 = repo.split('/')[1] // second part, repo without the owner
134 + const repo2clash = getAvailablePlugins().find(x => x.id === repo2) || mapPlugins(x => x.id === repo2).some(Boolean)
135 + const pluginFolder = repo2clash ? repo.replace('/','-') : repo2 // longer form only if necessary
136 + const installFolder = PLUGINS_PATH + '/' + pluginFolder
137 + const GITHUB_ZIP_ROOT = repo2 + '-' + rec.default_branch // github puts everything within this folder
138 + const rootWithinZip = GITHUB_ZIP_ROOT + '/' + DIST_ROOT
139 + return new Promise(resolve =>
140 + res.pipe(unzipper.Parse())
141 + .on('entry', entry => {
142 + const { path, type } = entry
143 + if (!path.startsWith(rootWithinZip))
144 + return entry.autodrain()
145 + const dest = installFolder + '/' + path.slice(rootWithinZip.length)
146 + if (type === 'File')
147 + return entry.pipe(createWriteStream(dest))
148 + mkdirSync(dest, { recursive: true }) // easy way be sure to have the folder ready before proceeding
149 + })
150 + .on('close', () => {
151 + resolve(undefined)
152 + downloadProgress(repo, undefined)
153 + }))
154 }
155
156 export default apis
157 +
158 +function apiGithub(uri: string) {
159 + return httpsString('https://api.github.com/'+uri, {
160 + headers: {
161 + 'User-Agent': 'HFS',
162 + Accept: 'application/vnd.github.v3+json',
163 + }
164 + }).then(async res => {
165 + if (!res.ok)
166 + throw res.statusCode
167 + return JSON.parse(res.body)
168 + })
169 +}
server/src/apiMiddleware.ts
+9 -4
@@ -66,7 +66,7 @@ async function getJsonFromReq(req: IncomingMessage): Promise<any> {
66 }
67
68 // offer an api for a generic dynamic list
69 -export function sendList<T>(addAtStart: T[]=[]) {
69 +export function sendList<T>(addAtStart?: T[]) {
70 const stream = new Readable({ objectMode: true, read(){} })
71 const ret = {
72 return: stream,
@@ -75,15 +75,20 @@ export function sendList<T>(addAtStart: T[]=[]) {
75 update(search: Partial<T>, change: Partial<T>) {
76 stream.push({ update:[{ search, change }] })
77 },
78 + end() { // notify end of additions
79 + stream.push('end')
80 + },
81 events(ctx: Koa.Context, eventMap: Parameters<typeof onOff>[1]) {
82 const off = onOff(events, eventMap)
83 ctx.res.once('close', off)
84 return stream
85 }
86 }
84 - for (const x of addAtStart)
85 - ret.add(x)
86 - stream.push('init')
87 + if (addAtStart) {
88 + for (const x of addAtStart)
89 + ret.add(x)
90 + stream.push('init')
91 + }
92 return ret
93 }
94
server/src/misc.ts
+28
@@ -12,6 +12,9 @@ import { IS_WINDOWS } from './const'
12 import { execFile } from 'child_process'
13 import { Connection } from './connections'
14 import assert from 'assert'
15 +import https from 'node:https'
16 +import { RequestOptions } from 'https'
17 +import { IncomingMessage } from 'node:http'
18
19 export type Callback<IN=void, OUT=void> = (x:IN) => OUT
20 export type Dict<T = any> = Record<string, T>
@@ -295,3 +298,28 @@ export function same(a: any, b: any) {
298 }
299 catch { return false }
300 }
301 +
302 +export function httpsString(url: string, options:RequestOptions={}): Promise<IncomingMessage & { ok: boolean, body: string }> {
303 + return httpsStream(url, options).then(res =>
304 + new Promise(resolve => {
305 + let buf = ''
306 + res.on('data', chunk => buf += chunk.toString())
307 + res.on('end', () => resolve(Object.assign(res, {
308 + ok: (res.statusCode || 400) < 400,
309 + body: buf
310 + })))
311 + })
312 + )
313 +}
314 +
315 +export function httpsStream(url: string, options:RequestOptions={}): Promise<IncomingMessage> {
316 + return new Promise((resolve, reject) => {
317 + https.request(url, options, res => {
318 + if (!res.statusCode || res.statusCode >= 400)
319 + throw res
320 + if (res.statusCode === 302 && res.headers.location)
321 + return resolve(httpsStream(res.headers.location, options))
322 + resolve(res)
323 + }).on('error', reject).end()
324 + })
325 +}
server/src/plugins.ts
+33 -13
@@ -17,7 +17,8 @@ import { readFile } from 'fs/promises'
17 import { existsSync, mkdirSync } from 'fs'
18 import { getConnections } from './connections'
19
20 -const PATH = 'plugins'
20 +export const PATH = 'plugins'
21 +export const DISABLING_POSTFIX = '-disabled'
22
23 const plugins: Record<string, Plugin> = {}
24
@@ -139,7 +140,15 @@ type PluginMiddleware = (ctx:Koa.Context) => void | Stop | CallMeAfter
140 type Stop = true
141 type CallMeAfter = ()=>void
142
142 -export interface AvailablePlugin { id: string, description?: string, version?: number, apiRequired?: number }
143 +export interface AvailablePlugin {
144 + id: string
145 + description?: string
146 + version?: number
147 + apiRequired?: number
148 + repo?: string
149 + badApi?: string
150 +}
151 +
152 let availablePlugins: Record<string, AvailablePlugin> = {}
153
154 export function getAvailablePlugins() {
@@ -163,17 +172,11 @@ async function rescan() {
172 const foundDisabled: typeof availablePlugins = {}
173 for (let f of await glob(PATH+'/*/plugin.js')) {
174 const id = f.split('/').slice(-2)[0]
166 - if (id.endsWith('-disabled')) continue
175 + if (id.endsWith(DISABLING_POSTFIX)) continue
176 if (!enablePlugins.get().includes(id)) {
168 - const pl = foundDisabled[id] = { id } as typeof foundDisabled[0]
177 try {
178 const source = await readFile(f, 'utf8')
171 - const v = pl.description = /exports.description *= *"(.*)"/.exec(source)?.[1]
172 - if (v)
173 - try { pl.description = JSON.parse(`"${v}"`) }
174 - catch {}
175 - pl.version = Number(/exports.version *= *(\d+)/.exec(source)?.[1]) || undefined
176 - pl.apiRequired = Number(/exports.apiRequired *= *(\d+)/.exec(source)?.[1]) || undefined
179 + foundDisabled[id] = parsePluginSource(id, source)
180 }
181 catch {}
182 continue
@@ -188,9 +191,7 @@ async function rescan() {
191 const { init, ...data } = await import(module)
192 delete data.default
193 deleteModule(require.resolve(module)) // avoid caching at next import
191 - data.badApi = data.apiRequired > API_VERSION ? "may not work correctly as it is designed for a newer version of HFS"
192 - : data.apiRequired < COMPATIBLE_API_VERSION ? "may not work correctly as it is designed for an older version of HFS"
193 - : undefined
194 + calculateBadApi(data)
195 if (data.badApi)
196 console.log("plugin", id, data.badApi)
197
@@ -253,3 +254,22 @@ function deleteModule(id: string) {
254
255 onProcessExit(() =>
256 Promise.allSettled(mapPlugins(pl => pl.unload())))
257 +
258 +export function parsePluginSource(id: string, source: string) {
259 + const pl: AvailablePlugin = { id }
260 + const v = pl.description = /exports.description *= *"(.*)"/.exec(source)?.[1]
261 + if (v)
262 + try { pl.description = JSON.parse(`"${v}"`) }
263 + catch {}
264 + pl.repo = /exports.repo *= *"(.*)"/.exec(source)?.[1]
265 + pl.version = Number(/exports.version *= *(\d+)/.exec(source)?.[1]) || undefined
266 + pl.apiRequired = Number(/exports.apiRequired *= *(\d+)/.exec(source)?.[1]) || undefined
267 + calculateBadApi(pl)
268 + return pl
269 +}
270 +
271 +function calculateBadApi(data: AvailablePlugin) {
272 + data.badApi = data.apiRequired! > API_VERSION ? "may not work correctly as it is designed for a newer version of HFS"
273 + : data.apiRequired! < COMPATIBLE_API_VERSION ? "may not work correctly as it is designed for an older version of HFS"
274 + : undefined
275 +}
todo.md
+5 -1
@@ -1,4 +1,9 @@
1 # To do
2 +- move logs to dedicated folder
3 +- admin: don't rely on google for font
4 +- fix: root without can_download breaks
5 +- use dialogs instead of side-forms on mobile (admin/fs+accounts)
6 +- easier deploy on cloud server
7 - admin/fs: sort items
8 - plugin.api.subscribeConfig
9 - watch certificates for change
@@ -24,7 +29,6 @@
29 - admin: warn before changing page if we have unsaved changes
30 - publish to npm (so people can "npm install hfs")
31 - frontend search supporting masks
27 -- use dialogs instead of side-forms on mobile (admin/fs+accounts)
32 - remove seconds from time
33 - plugin to download latest release
34 - https://api.github.com/repos/rejetto/hfs/releases/latest