CodeWorlds
Powrót do kolekcji
Przewodnik15 min czytaniaZespół CodeWorlds

Knock, warstwa powiadomień nad dostawcami

Knock zamienia jedno zdarzenie w wysyłkę mailem, SMS, push i Slackiem. Wersje 1.34.0 oraz 0.13.1, dwie różne licencje w jednej rodzinie i rachunek na przykładzie.

Knock, warstwa powiadomień nad dostawcami

Knock to warstwa między Twoim kodem a dostawcami wysyłki: jedno wywołanie workflows.trigger zamienia się w mail, SMS, push, wpis w kanale w aplikacji albo wiadomość na Slacku, zależnie od preferencji odbiorcy. Serwerowy pakiet @knocklabs/node ma wersję 1.34.0 z 16 lipca 2026 roku, przeglądarkowy @knocklabs/react wersję 0.13.1 z 4 sierpnia 2026 roku.

Co Knock robi ponad wysłanie maila

Resend i Postmark przyjmują żądanie HTTP i wysyłają jednego maila. Knock stoi piętro wyżej i nie wysyła niczego samodzielnie. Kanały konfiguruje się w panelu, podając poświadczenia własnego dostawcy: dla maila lista obejmuje Postmark, Resend, SendGrid, Amazon SES, Mailgun, Mailjet, Mandrill, MailerSend, SparkPost i zwykły SMTP, dla SMS między innymi Twilio, Vonage, Plivo, Telnyx, Sinch i MessageBird, dla push APNS, Firebase, Expo, OneSignal i Amazon SNS, dla czatu Slacka, Microsoft Teams, Discorda i WhatsAppa. To znaczy, że Knock nie zastępuje rachunku u dostawcy, tylko go dokłada.

Za co więc się płaci. Po pierwsze za rozstrzygnięcie, którymi kanałami wysłać dane zdarzenie do danego odbiorcy, czyli za zestaw preferencji trzymany po stronie Knocka. Po drugie za kroki, których dostawca maila nie ma: grupowanie wielu zdarzeń w jedną wiadomość, tłumienie zbyt częstych wysyłek i anulowanie zaplanowanej wysyłki, gdy przestała być potrzebna. Po trzecie za kanał w aplikacji: @knocklabs/client trzyma połączenie przez websocket, w zależnościach widać phoenix w wersji 1.8.5, więc feed aktualizuje się bez odpytywania serwera.

Knock przyjmuje też zdarzenia ze źródeł zewnętrznych, zamiast wyłącznie z Twojego kodu. Na liście integracji są Clerk, Stripe, PostHog, Segment, RudderStack, Hightouch, Census oraz Supabase. Dla części zespołów to jedyny powód wejścia w Knocka: powiadomienia da się wtedy podpiąć bez dokładania wywołań do aplikacji.

Rodzina pakietów i rozjazd licencji

Rodzina @knocklabs ma dwie licencje i to nie jest błąd odczytu. Sprawdzenie z trzech źródeł daje spójny, ale rozdwojony obraz.

Pakiety serwerowe są na Apache 2.0. @knocklabs/node 1.34.0 deklaruje Apache-2.0 w rejestrze npm, w repozytorium knocklabs/knock-node leży plik LICENSE z pełnym tekstem Apache License Version 2.0, a w opublikowanej paczce jest package/LICENSE z tym samym tekstem. Tak samo @knocklabs/mgmt 0.33.0 z 16 lipca 2026 roku oraz pythonowe knockapi 1.29.0, wgrane na PyPI tego samego dnia.

Pakiety przeglądarkowe i CLI są na MIT. @knocklabs/client 0.22.1, @knocklabs/react-core 0.15.1, @knocklabs/react 0.13.1, @knocklabs/react-native 0.11.1 i @knocklabs/expo 0.8.1 wyszły 4 sierpnia 2026 roku, @knocklabs/cli 1.2.3 trzynastego sierpnia. Wszystkie mają w rejestrze MIT, a w katalogu głównym monorepozytorium knocklabs/javascript leży LICENSE z tekstem MIT i notą „Copyright (c) 2021 Knock Labs, Inc.”.

Trzecie źródło, czyli zawartość paczek, pokazuje lukę. @knocklabs/client dołącza LICENSE z tekstem MIT. Natomiast @knocklabs/react, @knocklabs/react-core, @knocklabs/react-native i @knocklabs/expo nie zawierają żadnego pliku licencyjnego, bo pole files w ich package.json to ["dist", "README.md"]. W monorepozytorium tylko packages/client/LICENSE istnieje, ścieżki packages/react/LICENSE i pozostałe zwracają 404. Dla tych czterech pakietów jedynym dowodem licencji jest pole w metadanych npm plus plik w katalogu głównym repozytorium. Narzędzie, które skanuje treść paczki, nie znajdzie tam nic.

Czy podział jest celowy. Idzie dokładnie po granicy repozytoriów: knock-node, knock-python i knock-mgmt-node mają Apache 2.0, javascript i knock-cli mają MIT. Konsekwencja jest zbyt regularna na przypadek, więc czytam to jako świadome rozróżnienie serwer kontra klient, ale Knock nigdzie tego nie wyjaśnia w sposób, który dałbym się zweryfikować, i tak to oznaczam: to wniosek, nie deklaracja producenta.

Dla audytu wynika z tego kilka rzeczy. Zestawienie licencji zależności pokaże dwie pozycje dla jednego dostawcy, obie permisywne, więc konfliktu nie ma. Apache 2.0 niesie jednak wyraźne udzielenie patentu i wymóg zachowania not, czego MIT nie ma, więc reguła „cała rodzina X jest na MIT” będzie fałszywa. Do tego @knocklabs/node wozi w środku src/internal/qs/LICENSE.md z licencją BSD 3-Clause dla odgałęzienia biblioteki neoqs, a tej licencji nie widać w polu license w npm.

Dwa drobiazgi z tego samego przeglądu. @knocklabs/cli 1.2.3 przypina @knocklabs/mgmt dokładnie na 0.33.0, bez zakresu, więc pakiet na MIT ciągnie za sobą pakiet na Apache 2.0. A znacznik latest pakietu @knocklabs/types wskazuje wydanie kandydackie 0.1.5-rc-5.0 z 24 stycznia 2025 roku, podczas gdy stabilne 0.1.5 pochodzi z 13 listopada 2024. @knocklabs/client prosi o ^0.1.5, a ten zakres pomija wydania kandydackie, więc w drzewie wyląduje wersja stabilna. Kto zainstaluje @knocklabs/types po nazwie, dostanie kandydata.

Wyzwalanie przepływu z kodu

Instalacja i minimalne wywołanie wyglądają tak.

Code
Bash
npm install @knocklabs/node
npm install @knocklabs/react @knocklabs/client
npm install --global @knocklabs/cli

Klient serwerowy ma jedną zależność wykonawczą, jose w zakresie ^6.0.11, używaną do podpisywania tokenów użytkownika.

Code
TypeScript
import Knock from '@knocklabs/node'

const knock = new Knock({
  apiKey: process.env.KNOCK_API_KEY,
  timeout: 30_000,
  maxRetries: 2
})

const { workflow_run_id } = await knock.workflows.trigger('new-comment', {
  recipients: ['user_123', 'user_456'],
  actor: 'user_789',
  cancellation_key: 'comment_5521',
  tenant: 'acme',
  data: {
    comment_id: '5521',
    document_title: 'Plan na trzeci kwartał',
    body: 'Zerknij na sekcję ryzyk'
  },
  settings: { sandbox_mode: false, skip_delay: false }
})

await knock.workflows.cancel('new-comment', {
  cancellation_key: 'comment_5521',
  recipients: ['user_123']
})

Domyślne wartości warto znać, bo rzadko trafiają do przykładów w dokumentacji. apiKey bierze się ze zmiennej KNOCK_API_KEY, baseURL ze zmiennej KNOCK_BASE_URL z wartością zapasową https://api.knock.app, timeout wynosi minutę, maxRetries dwa, a logLevel czyta się z KNOCK_LOG i domyślnie jest ustawione na warn. Ponieważ przekroczenia czasu też są ponawiane, realne oczekiwanie bywa dłuższe niż sam timeout.

Ograniczenia są zapisane w typach paczki. Lista recipients mieści najwyżej tysiąc pozycji na jedno wyzwolenie. Ładunek data ma limit 10 MB, a pojedyncza wartość tekstowa dłuższa niż 1024 bajty zostaje przycięta w logach. Uruchomienia są asynchroniczne, odpowiedź niesie wyłącznie workflow_run_id. Pole cancellation_key trzeba podać w momencie wyzwolenia, inaczej późniejsze anulowanie jest niemożliwe, i powinno być unikalne, bo klucz współdzielony przez kilka żądań anuluje je hurtem.

Preferencje odbiorcy

To jest ta część, którą najdrożej odtworzyć samodzielnie: macierz zgód na kanał, kategorię i pojedynczy przepływ, razem z interfejsem do jej edycji.

Code
TypeScript
await knock.users.setPreferences('user_123', 'default', {
  channel_types: {
    email: true,
    sms: false,
    push: true,
    in_app_feed: true,
    chat: false,
    http: true
  },
  categories: {
    marketing: false,
    'product-updates': { channel_types: { email: true, push: false } }
  },
  workflows: {
    'new-comment': { channel_types: { email: false, in_app_feed: true } }
  }
})

const set = await knock.users.getPreferences('user_123', 'default')
const all = await knock.users.listPreferences('user_123')
await knock.users.unsetPreferences('user_123', 'marketing-only')

Typów kanałów jest sześć: chat, email, http, in_app_feed, push i sms. Każdy z trzech poziomów przyjmuje albo wartość logiczną, albo obiekt z własnym channel_types, a rozstrzygnięcie idzie od najbardziej szczegółowego, czyli ustawienie na poziomie przepływu wygrywa z kategorią, a kategoria z typem kanału. Drugi argument to identyfikator zestawu preferencji; default jest zwykłym wyborem, ale odbiorca może mieć ich kilka, co przydaje się przy oddzielnych zestawach dla różnych najemców. Po stronie Reacta @knocklabs/react-core udostępnia hook usePreferences, więc ekran ustawień nie wymaga własnego klienta HTTP.

Grupowanie i tłumienie powtórzeń

Najprostszy przykład, na którym widać sens dodatkowej warstwy. Dziesięć osób komentuje ten sam dokument w ciągu minuty. Bez warstwy dziesięć zdarzeń daje dziesięć maili i użytkownik wyłącza powiadomienia. Z krokiem batch dziesięć wyzwoleń zlewa się w jedno okno, szablon dostaje całą listę aktywności i wychodzi jeden mail z informacją o dziesięciu komentarzach.

Typy kroków dostępne w przepływie to channel, batch, delay, http_fetch, branch i throttle. Krok batch ma ustawienia batch_order, batch_window_type i batch_window; wartości podstawione przez generator to asc, sliding i okno trzydziestu sekund. Okno przesuwne przedłuża się przy każdym kolejnym zdarzeniu, więc seria komentarzy co dwadzieścia sekund trzyma paczkę otwartą. Krok delay przyjmuje delay_for z jednostką i wartością, http_fetch ma method i url i pozwala dociągnąć dane w trakcie uruchomienia, throttle ogranicza, jak często dany odbiorca w ogóle dostaje coś z tego przepływu, a branch rozgałęzia przepływ warunkowo.

Grupowanie zmienia też rachunek, bo Knock liczy wysłane wiadomości. Dziesięć wyzwoleń zwiniętych w jedną paczkę to jedna pozycja na fakturze, nie dziesięć.

I teraz uczciwie o drugiej stronie. Jeżeli wysyłasz reset hasła, potwierdzenie zamówienia i fakturę, każde na jednym kanale i każde natychmiast, to grupowanie, tłumienie i preferencje nie mają czego robić. Sam dostawca maila wystarczy, a Knock dokłada kolejną usługę na ścieżce dostarczenia, kolejny panel i kolejny rachunek. Próg opłacalności to mniej więcej moment, w którym pojawia się drugi kanał albo pierwszy powtarzalny strumień zdarzeń typu komentarz, wzmianka czy zmiana statusu.

Gdzie mieszka logika przepływu

Przepływy edytuje się w panelu Knocka, więc domyślnie decyzja o tym, kto co dostaje, mieszka poza Twoim repozytorium. Częściową odpowiedzią jest CLI.

Code
Bash
knock login
knock init
knock workflow pull new-comment
knock pull --knock-dir ./knock
knock push --knock-dir ./knock --commit -m "okno grupowania na piec minut"
knock commit --resource-type workflow --resource-id new-comment -m "poprawka szablonu"
knock commit promote --to production
knock branch create feature-digest

Polecenie knock workflow pull zapisuje katalog na przepływ, a w nim workflow.json oraz pliki szablonów. Klucze zakończone znakiem @ są wskaźnikami na pliki na dysku, więc treść maila zostaje osobnym plikiem .html, który da się sensownie porównać w żądaniu scalenia.

Code
JSON
{
  "name": "Nowy komentarz",
  "key": "new-comment",
  "steps": [
    {
      "ref": "batch_1",
      "type": "batch",
      "settings": {
        "batch_order": "asc",
        "batch_window_type": "sliding",
        "batch_window": { "unit": "seconds", "value": 30 }
      }
    },
    {
      "ref": "email_1",
      "type": "channel",
      "channel_key": "postmark-transactional",
      "template": {
        "settings": { "layout_key": "default" },
        "subject": "Nowe komentarze w {{ data.document_title }}",
        "html_body@": "email_1/html_body.html"
      }
    }
  ]
}

Poza przepływami CLI obsługuje układy graficzne, fragmenty szablonów, tłumaczenia, typy wiadomości, poradniki w aplikacji, audiencje oraz knock schema pull. Konfiguracja projektu leży w knock.json, tworzy ją knock init, a knock workflow generate-types wypuszcza typy dla ładunku data.

Ograniczenia trzeba znać, zanim wpiszesz to do procesu wydawania. Flagi --environment w knock push i knock commit mają dokładnie jedną dopuszczalną wartość, development, więc do środowiska produkcyjnego nie da się wypchnąć wprost; przechodzi się przez knock commit promote --to. Do tego Knock ma własne gałęzie, obsługiwane przez knock branch create, switch, merge, rebase, list, delete i exit, czyli drugi model rozgałęzień, który trzeba ręcznie trzymać w zgodzie z tym w gicie. Efekt jest taki, że przepływy da się wersjonować i przeglądać jako JSON, ale pełny obieg to pull, edycja, push, commit, promote, a nie zwykłe wdrożenie z Twojego serwera budującego.

Porównanie, które porządkuje ten wybór: w Inngest przepływ jest funkcją TypeScript w Twoim repozytorium i przechodzi zwykły przegląd kodu. Knock daje w zamian edytor i podgląd szablonów dla osób spoza zespołu inżynierskiego. To wymiana, nie przewaga jednej strony.

Cennik i rachunek na przykładzie

Jednostką rozliczeniową jest wiadomość, zdefiniowana na stronie cennika jako wiadomość skutecznie wysłana do jednego użytkownika na jednym kanale. Odbiorcy, przepływy, kanały i konta zespołu są bez limitu we wszystkich planach, więc liczy się wyłącznie ruch.

Plan Developer kosztuje 0 USD i mieści 10 000 wiadomości, 500 użytkowników widzących poradnik w aplikacji oraz 500 kredytów funkcji agenta, po których stawka wynosi 0,01 USD za kredyt. Logi trzymane są 30 dni, logowanie jednokrotne działa tylko przez Google. Plan Starter to 250 USD miesięcznie za 50 000 wiadomości, a każda kolejna kosztuje 0,005 USD; do tego 2500 użytkowników poradników po 0,05 USD za nadmiarowego i 2000 kredytów agenta po 0,01 USD. Starter zdejmuje też oznaczenia Knocka z komponentów. Enterprise jest wyceniany indywidualnie, ma retencję logów 90 dni i alternatywny model rozliczania według liczby unikalnych użytkowników powiadomionych w miesiącu.

Dla planu darmowego stawka za przekroczenie nie jest podana. Tabela mówi wyłącznie o przejściu na wyższy plan, więc dopóki Knock nie opublikuje ceny, traktuj limit 10 000 jako twardy.

Policzmy przykład z zadania. Dziesięć tysięcy użytkowników, każdy dostaje pięć powiadomień miesięcznie, średnio na dwóch kanałach. To 50 000 powiadomień i 100 000 wiadomości. Starter pokrywa 50 000, pozostałe 50 000 kosztuje 50 000 razy 0,005 USD, czyli 250 USD. Razem 250 plus 250, czyli 500 USD miesięcznie za sam Knock. Mailowa połowa ruchu, czyli 50 000 wiadomości, i tak musi wyjść przez dostawcę: plan Pro w Resendzie to 20 USD za 50 000 maili. Łącznie około 520 USD, z czego 500 USD to Knock.

Wariant tańszy dla porównania. Ci sami użytkownicy, trzy powiadomienia miesięcznie, jeden kanał, czyli 30 000 wiadomości. Mieści się w limicie Startera, więc 250 USD plus 20 USD za maile. Gdyby te same 30 000 maili wysyłać wprost przez Resend, rachunek wynosi 20 USD. Różnica 250 USD miesięcznie to cena preferencji, grupowania i feedu w aplikacji, i to jest liczba, do której trzeba przyłożyć własny przypadek. Przy dziesięciu tysiącach użytkowników i jednym powiadomieniu miesięcznie mieścisz się dokładnie w darmowym limicie 10 000 wiadomości, czyli na jego krawędzi.

Knock, dostawcy maila i Novu

NarzędzieWarstwaJednostka rozliczeniowaPlan darmowyPierwszy płatny próg
Knockorkiestracja nad dostawcamiwiadomość do jednego użytkownika na jednym kanale10 000 wiadomości250 USD za 50 000 wiadomości
Novuorkiestracja, także do samodzielnego hostowaniauruchomienie przepływu10 000 uruchomień, 20 przepływówod 30 USD za 30 000 uruchomień
Resenddostawca mailawysłany mail3000 maili, limit 100 dziennie20 USD za 50 000 maili
Postmarkdostawca mailawysłany mail100 maili miesięcznie15 USD od 10 000 maili

Różnica jednostki jest istotniejsza niż różnica ceny. Knock liczy wiadomości, więc powiadomienie wysłane trzema kanałami to trzy pozycje. Novu liczy uruchomienia przepływu, więc to samo powiadomienie na trzech kanałach jest jedną pozycją, za to plan darmowy ogranicza liczbę przepływów do dwudziestu, środowiska do dwóch, konta zespołu do trzech, a historię aktywności do doby. Novu ma też wariant do samodzielnego hostowania: w katalogu głównym repozytorium novuhq/novu leży plik LICENSE z tekstem MIT. Knock takiego wariantu nie oferuje, w cenniku nie ma pozycji o hostowaniu u siebie, a publikowane repozytoria to biblioteki klienckie, SDK i CLI, nie serwer.

Resend i Postmark są w innej kategorii i nie konkurują z Knockiem, tylko pod nim pracują. Twilio jest tym samym po stronie SMS: figuruje na liście dostawców SMS w Knocku, więc wysyłka SMS przez Knocka i tak kończy się na koncie w Twilio, a Knock dokłada rozstrzygnięcie preferencji i grupowanie. Jeśli zostaje Ci jeden kanał i proste powiadomienia transakcyjne, właściwym wyborem jest sam dostawca.

Po stronie interfejsu Knock ma gotowe komponenty, których dostawcy maila nie mają z definicji.

Code
TypeScript
'use client'

import {
  KnockProvider,
  KnockFeedProvider,
  NotificationIconButton,
  NotificationFeedPopover
} from '@knocklabs/react'
import '@knocklabs/react/dist/index.css'
import { useRef, useState } from 'react'

export function NotificationBell({ userId, userToken }) {
  const [isOpen, setIsOpen] = useState(false)
  const buttonRef = useRef(null)

  return (
    <KnockProvider
      apiKey={process.env.NEXT_PUBLIC_KNOCK_PUBLIC_API_KEY}
      user={{ id: userId }}
      userToken={userToken}
      enabled={Boolean(userId && userToken)}
    >
      <KnockFeedProvider feedId={process.env.NEXT_PUBLIC_KNOCK_FEED_ID}>
        <NotificationIconButton
          ref={buttonRef}
          onClick={() => setIsOpen(!isOpen)}
        />
        <NotificationFeedPopover
          buttonRef={buttonRef}
          isVisible={isOpen}
          onClose={() => setIsOpen(false)}
        />
      </KnockFeedProvider>
    </KnockProvider>
  )
}

KnockProvider przyjmuje między innymi apiKey, user, userToken, onUserTokenExpiring, timeBeforeExpirationInMs, host, i18n, logLevel, branch oraz enabled z wartością domyślną true. Starsza właściwość userId jest oznaczona jako przestarzała na rzecz user. Zależności równorzędne pakietu to React w wersjach 17, 18 i 19 razem z react-dom, a Next.js w zakresie od 13 do 16 oraz @tanstack/react-router w wersji 1 są w peerDependenciesMeta oznaczone jako opcjonalne. W zwykłych zależnościach siedzi własny system projektowy Knocka, pakiety @telegraph/*, w zakresach otwartych typu >=0.8.0, oraz lucide-react w zakresie ^0.544.0. Otwarte zakresy na bibliotekę komponentów oznaczają, że kolejne wydanie może wejść do budowania bez żadnej zmiany po Twojej stronie, więc plik blokady przestaje być formalnością.

Typowe błędy

Traktowanie Knocka jak dostawcy wysyłki. Knock zleca, a nie wysyła. Konto w Postmarku, Resendzie czy Twilio jest dalej potrzebne i płacisz dwa rachunki, co przy szacowaniu kosztu bywa pomijane.

Liczenie budżetu w powiadomieniach zamiast w wiadomościach. Jedno powiadomienie wysłane mailem, na push i do feedu to trzy pozycje na fakturze, a nie jedna.

Wyzwalanie bez cancellation_key. Klucza nie da się dopisać później, a bez niego workflows.cancel nie ma czego anulować, więc typowy scenariusz „nie wysyłaj przypomnienia, jeśli użytkownik już wszedł" przestaje być wykonalny.

Mylenie settings.sandbox_mode z ustawieniem kanału. Pole w wyzwoleniu nadpisuje tryb piaskownicy dla wszystkich kanałów w tym jednym uruchomieniu i nie zmienia konfiguracji samego kanału.

Edycja w panelu bez knock pull. Ktoś poprawia szablon w przeglądarce, repozytorium zostaje w tyle i przy odtwarzaniu środowiska wraca wersja sprzed poprawki. Jeśli decydujesz się trzymać przepływy w repozytorium, kierunek musi być jeden.

Instalacja bez pliku blokady, przy zależnościach @telegraph/* w zakresach otwartych. Do tego wpis „cała rodzina @knocklabs jest na MIT” w zestawieniu licencji będzie po prostu nieprawdziwy dla pakietów serwerowych.

FAQ

Czy Knock zastępuje Resend albo Postmark?

Nie. Knock jest warstwą nad dostawcami i wymaga podpięcia własnego konta u dostawcy w konfiguracji kanału. Postmark i Resend są na liście obsługiwanych dostawców maila, więc rachunki są dwa: za wiadomości u Knocka i za maile u dostawcy.

Czy przepływy da się trzymać w repozytorium?

Częściowo tak. @knocklabs/cli pobiera przepływy przez knock workflow pull do katalogów z plikami workflow.json i szablonami, a knock push wypycha je z powrotem. Ograniczenie polega na tym, że push i commit działają wyłącznie w środowisku development, a dalej idzie się przez knock commit promote --to.

Dlaczego dwa pakiety Knocka mają dwie różne licencje?

Bo pochodzą z różnych repozytoriów. @knocklabs/node z repozytorium knock-node jest na Apache 2.0, potwierdzonej w polu npm, w pliku LICENSE i w opublikowanej paczce. @knocklabs/react z monorepozytorium javascript jest na MIT, przy czym paczka nie zawiera pliku licencyjnego, bo pole files obejmuje tylko dist i README.md.

Ile kosztuje obsługa dziesięciu tysięcy użytkowników?

Przy pięciu powiadomieniach na użytkownika miesięcznie i dwóch kanałach wychodzi 100 000 wiadomości: 250 USD za plan Starter plus 250 USD za 50 000 wiadomości ponad limit, czyli 500 USD, do tego rachunek u dostawcy maila. Przy trzech powiadomieniach na jednym kanale mieścisz się w limicie Startera i płacisz 250 USD.

Kiedy Knock jest przerostem formy?

Gdy masz jeden kanał, powiadomienia transakcyjne wysyłane natychmiast i bez preferencji po stronie użytkownika. Wtedy grupowanie, tłumienie i rozstrzyganie preferencji nie mają zastosowania, a warstwa dokłada koszt, opóźnienie i logikę poza repozytorium.

Źródła: cennik Knocka, lista integracji, monorepozytorium javascript.

Czytaj dalej

Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie