Przejdź do treści
Wycena i proces

W umowie klient dostaje pełnię praw do kodu. W LICENSE.md edytora stoi GPL 2 lub nowsza

Klauzula o przeniesieniu autorskich praw majątkowych do całości projektu obejmuje zbiór, którego granic nikt nie wyznaczył. Komponenty osób trzecich w nim siedzą.

Redakcjadeveloper front-end10 minkod + tabela
2liczba z tezy tekstu

„Wykonawca przenosi na zamawiającego autorskie prawa majątkowe do przedmiotu umowy w całości” — to zdanie albo jego wariant stoi w większości umów na wykonanie strony czy panelu. Jeśli w tym samym projekcie siedzi edytor tekstu z paczki ckeditor5, to w pliku node_modules/ckeditor5/LICENSE.md stoi obok niego zdanie sprzeczne: oprogramowanie jest dostępne w modelu dwulicencyjnym, na warunkach GNU General Public License Version 2 or later albo na warunkach komercyjnych CKSource.

Te dwa zapisy nie mogą być jednocześnie prawdziwe. Pierwszy obiecuje klientowi całość. Drugi mówi, że część tej całości należy do kogoś innego i jest tylko licencjonowana, na warunkach, które wykonawca przyjął w momencie instalacji pakietu — bez czytania.

Art. 1 i art. 8: czego klauzula o przeniesieniu praw nie obejmuje

Ustawa o prawie autorskim i prawach pokrewnych definiuje w art. 1 ust. 1 przedmiot ochrony jako „każdy przejaw działalności twórczej o indywidualnym charakterze, ustalony w jakiejkolwiek postaci, niezależnie od wartości, przeznaczenia i sposobu wyrażenia (utwór)”, a w art. 8 ust. 1 stanowi, że „prawo autorskie przysługuje twórcy, o ile ustawa nie stanowi inaczej”.

Z drugiego przepisu wynika rzecz banalna i często pomijana przy podpisywaniu: przenieść da się tylko to, co się ma. Kod cudzego komponentu nie jest przedmiotem umowy między wykonawcą a jego klientem — jest przedmiotem licencji między wydawcą komponentu a tym, kto go używa. Klauzula o całości tego nie zmienia, tylko tworzy rozbieżność między tym, co klient uważa, że kupił, a tym, co dostał.

Osobną warstwą jest kod, który w projekcie napisał generator. Oba przepisy operują pojęciami przypisywanymi człowiekowi, a jak to dokładnie rozkłada się na plik, w którym część linii pochodzi z modelu, a część z pracy developera, pozostaje przedmiotem sporu, a nie rzeczą ustaloną. Tego sporu nie rozstrzygnie ani artykuł, ani narzędzie w pipeline, a przy wątpliwości co do konkretnej umowy odpowiada na nią prawnik patrzący na ten egzemplarz. Wniosek praktyczny jest jednak niezależny od wyniku: klauzula o przeniesieniu praw do „całości dzieła” opisuje zbiór, którego granic nikt w projekcie nie wyznaczył.

Komponenty osób trzecich nie przechodzą razem z repozytorium

Dla czterech rzeczy, które w generowanym kodzie panelu administracyjnego czy dashboardu pojawiają się najczęściej, warunki wyglądają następująco:

Komponent Warunki wg wydawcy Co to znaczy dla oddania projektu
CKEditor 5 GPL 2+ albo licencja komercyjna; dystrybucja open source na warunkach GPL 2+ Ścieżka darmowa nakłada warunki GPL na dystrybuowaną całość. Zamknięte wdrożenie u klienta wymaga licencji komercyjnej
TinyMCE 8 GPL v2 lub nowsza; wdrożenie self-hosted wymaga klucza license_key ustawionego na 'gpl' albo klucza komercyjnego Brak ustawionego klucza to nie jest konfiguracja neutralna. Zmiana licencji weszła w wersji 7, starsze wydania zostały na poprzednich warunkach
Highcharts EULA Highsoft: „Software is licensed, not sold”; „any use of the Software on a Public Website requires the purchase of a SaaS License” Strona publiczna klienta to użycie płatne. Licencję kupuje się na konkretny podmiot i konkretny zakres, a nie przenosi razem z repozytorium
Font Awesome Free Trzy licencje w jednej paczce: CC BY 4.0 na ikony, SIL OFL 1.1 na pliki fontów, MIT na pozostały kod Atrybucja jest wymagana przez wszystkie trzy. Komentarze atrybucyjne w plikach zostają na miejscu, także po minifikacji

Dwie rzeczy warto z tej tabeli wyciągnąć osobno. Pierwsza: ścieżka darmowa w modelu dwulicencyjnym nie jest ścieżką bez zobowiązań, tylko inną ścieżką zobowiązań. Druga: licencja komercyjna jest wystawiana na podmiot. Kupiona na agencję nie pokrywa dziesięciu wdrożeń u dziesięciu klientów, chyba że wprost to przewiduje — a to pytanie zapada przy podpisie umowy z wydawcą, nie przy fakturze dla klienta.

Skąd bierze się rozbieżność: pole license to wskaźnik, nie identyfikator

Powód, dla którego ta sprzeczność przechodzi przez cały projekt niezauważona, siedzi w jednym polu manifestu. Dokumentacja npm dopuszcza w polu license trzy rzeczy: identyfikator SPDX, wyrażenie SPDX dla kilku licencji oraz napis SEE LICENSE IN <filename> dla licencji, która identyfikatora nie ma. Trzecia opcja jest wskaźnikiem do pliku, nie informacją o treści.

Trzy komponenty z tabeli powyżej korzystają właśnie z niej albo z czegoś jeszcze luźniejszego:

ckeditor5:   "SEE LICENSE IN LICENSE.md"
tinymce:     "SEE LICENSE IN license.md"
highcharts:  "https://www.highcharts.com/license"

Ostatnia wartość nie jest nawet dopuszczoną formą — to po prostu adres wpisany w pole tekstowe. Kiedy narzędzie audytowe nie potrafi przepuścić zawartości tego pola przez parser SPDX, sięga po tryb zapasowy. Dokumentacja license-checkera opisuje go wprost: po nieudanej próbie z package.json narzędzie szuka plików LICENSE, LICENCE, COPYING i README w tej kolejności i porównuje ich tekst ze znanymi treściami licencji. Wynik uzyskany tą drogą dostaje gwiazdkę, która oznacza, że licencja została wywnioskowana z pliku innego niż package.json.

Stąd jedna zasada przy czytaniu takiego raportu: gwiazdka to nie przypis, to ostrzeżenie. Odpowiedź przy niej pochodzi z dopasowania tekstu, a nie z deklaracji autora pakietu.

Raport na drzewie 180 pakietów

Jak daleko to dopasowanie potrafi odjechać od treści pliku, widać na projekcie z pięcioma zależnościami przypiętymi do wersji:

npm init -y
npm i ckeditor5@49.0.1 tinymce@8.10.1 highcharts@13.1.1 \
      react@18.3.1 react-dom@18.3.1
npx license-checker@25.0.1 --summary

Pięć bezpośrednich zależności rozwija się do 180 pozycji w raporcie. Zestawienie wygląda tak:

├─ MIT: 113
├─ GPL-2 or later](https://www.gnu.org/licenses/gpl.html)*: 34
├─ MIT*: 24
├─ ISC: 5
├─ ISC*: 2
├─ Custom: https://github.com/highcharts/dashboards-dist: 1
└─ GPL-2 or later](http://www.gnu.org/licenses/gpl.html); or
2. The Tiny Technologies*: 1

Trzy z siedmiu kategorii nie są nazwami licencji. To fragmenty zdań wyrwane z plików tekstowych, razem z niedomkniętym nawiasem po markdownowym odnośniku i z numerem punktu z listy. Czwarta to adres repozytorium. Gwiazdkę ma 61 pozycji z 180, czyli 34 procent drzewa.

Na poziomie pojedynczych pakietów ta sama rodzina, w tej samej wersji, dostaje trzy różne odpowiedzi:

Pakiet Co pokazuje raport Co jest w pliku licencji
ckeditor5@49.0.1 ISC* GPL 2+ albo licencja komercyjna
@ckeditor/ckeditor5-core@49.0.1 MIT* GPL 2+ albo licencja komercyjna
@ckeditor/ckeditor5-adapter-ckfinder@49.0.1 GPL-2 or later](https://…)* GPL 2+ albo licencja komercyjna
tinymce@8.10.1 GPL-2 or later](http://…); or — po złamaniu wiersza — 2. The Tiny Technologies* GPL 2+ albo licencja komercyjna
highcharts@13.1.1 Custom: https://github.com/highcharts/dashboards-dist EULA, licencja płatna
react@18.3.1 MIT MIT

Pakiet, który developer faktycznie importuje w kodzie, czyli ckeditor5, dostaje etykietę licencji permisywnej. Jeden z jego własnych podpakietów dostaje etykietę GPL. Oba pliki licencji mówią to samo. Jeśli załącznik do umowy powstaje przez skopiowanie takiego raportu, to właśnie ta pierwsza etykieta trafia do dokumentu, który klient podpisuje.

Czarna lista wychodzi z kodem 0, biała z kodem 1

Bramka w CI nie uratuje tego automatycznie, bo zależy to od jednej flagi. Dwie konfiguracje tego samego narzędzia na tym samym drzewie:

# wariant A: czarna lista zakazanych licencji
npx license-checker@25.0.1 --production \
  --failOn 'GPL-2.0;GPL-2.0-or-later;GPL-3.0;AGPL-3.0'
# exit code: 0

# wariant B: biała lista dopuszczonych licencji
npx license-checker@25.0.1 --production \
  --onlyAllow 'MIT;ISC;BSD-3-Clause;Apache-2.0'
# exit code: 1
# Package "@ckeditor/ckeditor5-adapter-ckfinder@49.0.1" is licensed under
# "GPL-2 or later](https://www.gnu.org/licenses/gpl.html)*" which is not
# permitted by the --onlyAllow flag. Exiting.

Wariant A przechodzi, mimo że w drzewie są 34 pakiety z GPL 2+ w treści licencji. Przechodzi, bo --failOn porównuje napisy, a napis w raporcie to GPL-2 or later](https://www.gnu.org/licenses/gpl.html)*, nie GPL-2.0-or-later. Zielony wynik mówi w tym momencie tylko tyle, że nie trafiono w wartość z listy.

Wariant B zatrzymuje build na pierwszej pozycji, która nie mieści się w czwórce dopuszczonych identyfikatorów. Zatrzymuje też na Custom: https://github.com/highcharts/dashboards-dist i na zlepku z TinyMCE, bo żaden z nich nie jest jednym z czterech dozwolonych napisów. Na tym polega różnica: lista dozwolonych wyłapuje wszystko, czego nie rozumie, a lista zakazanych tylko to, co umie nazwać.

Ceną za listę dozwolonych są fałszywe alarmy na pakietach z poprawną, permisywną licencją zapisaną wyłącznie w pliku — tych w drzewie jest 24 plus 2. Obsługuje je plik wyjątków, w którym każda pozycja ma datę sprawdzenia i osobę, która czytała plik licencji. Wyjątek bez daty po roku jest nie do odróżnienia od przeoczenia.

Czego w package.json nie ma wcale

Raport z menedżera pakietów widzi tylko to, co menedżer pakietów zainstalował. Poza jego zasięgiem zostaje warstwa, którą model dopisuje równie chętnie:

  • Zasoby z CDN w znacznikach. Font, arkusz ikon albo biblioteka wykresów wciągnięte przez <script src> lub <link rel="stylesheet"> nie mają wpisu w manifeście ani w żadnym raporcie licencyjnym projektu
  • Pliki fontów w repozytorium. Font komercyjny hostowany samodzielnie wymaga licencji webfontowej, często rozliczanej od liczby odsłon. Plik .woff2 w katalogu public nie nosi tej informacji
  • Natywne binaria. Pakiet opakowujący zewnętrzny program ma własną licencję, zwykle permisywną, a program pod nim swoją, często inną. Audyt widzi opakowanie
  • Obrazy i ilustracje. Grafika z banku zdjęć ma licencję przypisaną do konta kupującego, nie do pliku
  • Wklejone fragmenty. Kod przeniesiony z odpowiedzi na forum albo z cudzego repozytorium nie zostawia śladu w drzewie zależności

Przegląd tej warstwy sprowadza się do jednego przejścia po katalogu źródeł:

grep -rnoE '(src|href)="https?://[^"]+' src/ public/ \
  | grep -vE 'localhost|127\.0\.0\.1' | sort -u -t: -k4

Każdy zewnętrzny adres z wyniku albo dostaje własny wiersz w załączniku, albo wypada z projektu. Trzecia możliwość, czyli adres bez wiersza, jest tym, co klient znajduje sam, zwykle w najmniej wygodnym momencie.

Sześć kolumn załącznika i jeden wiersz w kosztorysie

Zdanie o przeniesieniu praw da się utrzymać w umowie, jeśli obok niego stoi załącznik, który z przedmiotu umowy wyłącza komponenty osób trzecich i wymienia je z warunkami. Minimalna zawartość to sześć kolumn: nazwa komponentu, wersja, licencja wyczytana z pliku licencji, podmiot, na który licencja jest wystawiona, data sprawdzenia i osoba sprawdzająca. Kolumna z licencją bierze się z pliku, nie z raportu — po tym, co raport robi z CKEditorem, to nie jest formalność.

Taki załącznik zmienia trzy rzeczy, i to zanim zacznie się praca. Komponent płatny przestaje być niespodzianką z trzeciego miesiąca i staje się pozycją w kosztorysie albo powodem, by wybrać inny. Wymiana komponentu na etapie makiety kosztuje godziny, a po wdrożeniu — przepisanie modułu. Pytanie, kto kupuje licencję i na kogo ma być wystawiona, zapada raz.

Utrzymywanie tego załącznika jest pracą cykliczną, bo warunki licencji zmieniają się między wersjami — TinyMCE jest tego przykładem — a nie jednorazową czynnością przy oddaniu. To jedna z pozycji, które naturalnie wchodzą do stałej opłaty za utrzymanie; jeśli rozliczenia idą w tę stronę, przejście z pojedynczych zleceń na usługę abonamentową wymaga wpisania przeglądu licencji do zakresu, razem z częstotliwością i progiem, przy którym wraca się do klienta.

Zostaje jedna decyzja i ma ona postać wiersza w kosztorysie: licencja komercyjna komponentu jest kosztem po stronie klienta albo po stronie wykonawcy. Każda z tych dwóch odpowiedzi jest dobra. Brak odpowiedzi oznacza, że koszt spadnie na wykonawcę w momencie, w którym klient zapyta o warunki, a cena jest już podpisana.