@dariusz/si-api (0.3.0)
Installation
@dariusz:registry=npm install @dariusz/si-api@0.3.0"@dariusz/si-api": "0.3.0"About this package
si-api
Klient Node.js do API portali hurtowych operatorów światłowodowych:
- Światłowód Inwestycje (
portal.swiatlowodinwestycje.pl) — klasaSiApiClient, - Orange BSA / Hurt Portal (
posh.hurt-orange.pl) — klasaOrangeBsaClient(patrz sekcja Orange BSA).
Oba portale dzielą ten sam interfejs danych (TM Forum API), więc OrangeBsaClient
dziedziczy po SiApiClient - różni się tylko logowaniem i bazami URL.
Backendy Światłowód Inwestycje:
- Portal —
https://portal.swiatlowodinwestycje.pl/portal-si-be - Bramka API —
https://apigateway-prd.swiatlowodinwestycje.pl/si/rest-api
Wymaga Node ≥ 18 (używa wbudowanego fetch). Brak zależności zewnętrznych (Orange BSA
korzysta dodatkowo z systemowego openssl do odczytu certyfikatu .p12). Moduł w formacie ESM.
Dokumentacja jest aktualizowana na bieżąco wraz z rozwojem modułu.
Jak działa logowanie
Ustalone na podstawie analizy ruchu (HAR portal.swiatlowodinwestycje.pl.har).
-
Żądanie logowania:
POST {PORTAL_BASE}/loginservices/users/login Content-Type: application/json;charset=UTF-8 body: {"username": "...", "password": "..."} -
Odpowiedź (HTTP 200):
{ "id": 305930, "username": "uzytkownik@firma.pl", "token": "TMj0e9nJon8yjB7ZCAh2PUREI0aEP7", "ukeId": "8465", "oaName": "PLAST_COM_SC", "accountStatus": 0, "oaId": 305922, "generateCert": 0 } -
Autoryzacja kolejnych żądań: zwrócony
tokenwysyłany jest w nagłówkutoken: <wartość>.- Działa zarówno dla portalu (
portal-si-be), jak i dla bramki (apigateway-prd .../si/rest-api). - Nie ma OAuth/Bearer - to prosty token w niestandardowym nagłówku
token.
- Działa zarówno dla portalu (
-
Wylogowanie:
PATCH {PORTAL_BASE}/loginservices/users/logout Content-Type: application/merge-patch+json;charset=UTF-8 token: <wartość> (bez body) -> 200 (pusta odpowiedź)Metoda
logout()wysyła to żądanie i czyścitoken/sessionw instancji.
Instalacja / uruchomienie
Projekt nie ma zależności — wystarczy Node ≥ 18.
Z prywatnego rejestru npm (Gitea)
Paczka jest publikowana w prywatnym rejestrze Gitea jako @dariusz/si-api.
U konsumenta utwórz .npmrc (nie commituj go) i zainstaluj:
@dariusz:registry=https://gitea.naurecki.pl/api/packages/dariusz/npm/
//gitea.naurecki.pl/api/packages/dariusz/npm/:_authToken=TWOJ_TOKEN
npm install @dariusz/si-api
Dzięki wpisowi @dariusz:registry tylko paczki z tego scope idą do Gitea, a
publiczne zależności dalej z npmjs.
Wymóg serwera: Gitea stoi za Apache, który dla scoped paczek (
%2Fw URL) musi mieć w vhościeAllowEncodedSlashes NoDecodeoraznocanonprzyProxyPass(skonfigurowane).
Publikacja nowej wersji (z katalogu projektu, mając .npmrc z tokenem):
npm version patch # podbij wersję
npm publish
Alternatywnie jako zależność git (poświadczenia w ~/.netrc):
npm install git+https://gitea.naurecki.pl/dariusz/si-api.git
Test logowania z linii poleceń
node bin/login.js <username> <password>
# lub przez zmienne środowiskowe:
SI_USERNAME=... SI_PASSWORD=... node bin/login.js
Użycie programistyczne
import { SiApiClient } from "./src/index.js";
const client = new SiApiClient();
await client.login("uzytkownik@firma.pl", "haslo");
// skróty do zbadanych endpointów:
const roles = await client.getUserRoles();
const orgs = await client.getOrganizations();
// generyczne żądanie do portalu:
const mapConf = await client.request("/extras/mapConf");
// generyczne żądanie do bramki API:
const spec = await client.request(
"/productCatalogManagement/v1/productSpecification",
{ base: "gateway", query: { fields: "id,name,productSpecificationType" } },
);
Konstruktor
new SiApiClient({ portalBase?, gatewayBase?, token? })
portalBase— domyślniehttps://portal.swiatlowodinwestycje.pl/portal-si-begatewayBase— domyślniehttps://apigateway-prd.swiatlowodinwestycje.pl/si/rest-apitoken— można podać istniejący token i pominąćlogin()
Metody
| Metoda | Opis |
|---|---|
login(username, password) |
Loguje, zapisuje token i session, zwraca obiekt sesji |
logout() |
Wylogowuje sesję (PATCH .../logout) i czyści token/session |
request(path, opts) |
Autoryzowane żądanie. opts: base ("portal"|"gateway"), method, query, body, headers |
getUserRoles() |
Role zalogowanego użytkownika (/services/users/roles/{id}) |
getOrganizations() |
Lista organizacji (/services/organizations) |
getProductSpecifications(opts) |
Katalog typów usług (TMF productSpecification) |
getProductOfferings(opts) |
Oferty produktowe (TMF productOffering) |
searchProducts(opts) |
Inwentarz usług klienta (TMF product) — obsługuje formularz wyszukiwania |
getProduct(id, opts) |
Szczegóły pojedynczej usługi (TMF product/{id}) |
startServiceTest(opts) |
Inicjuje telediagnostykę (pomiar) — tworzy zlecenie i id pomiaru |
listServiceTests(opts) |
Historia pomiarów (ekran „Zamówienia i zgłoszenia") |
getServiceTest(testId) |
Surowy ServiceTest po id (z testMeasure[]) |
getServiceTestResult(testId) |
Wynik uporządkowany: meta + metryki + werdykty |
Właściwości / błędy
client.token,client.session,client.isAuthenticatedSiApiError— rzucany przy błędach; pola.statusi.body
Zbadane endpointy (z HAR)
Portal (portal-si-be):
POST /loginservices/users/loginGET /services/users/roles/{userId}GET /services/organizationsGET /services/apiGET /extras/mapConf
Bramka (apigateway-prd .../si/rest-api), wszystkie wymagają nagłówka token:
| metoda | ścieżka | wymagany Accept |
uwagi |
|---|---|---|---|
GET |
/productCatalogManagement/v1/productSpecification |
— | katalog typów usług |
GET |
/productCatalogManagement/v1/productOffering |
— | oferty |
GET |
/productInventoryManagement/v1/product |
— | inwentarz (lista, filtry) |
GET |
/productInventoryManagement/v1/product/{id} |
application/hal+json (zalecane) |
szczegóły usługi |
POST |
/serviceTestManagement/v1/serviceTest |
application/json |
inicjalizacja pomiaru — inny Accept ⇒ 406 |
GET |
/serviceTestManagement/v1/serviceTest |
— | historia pomiarów (lista) |
GET |
/serviceTestManagement/v1/serviceTest/{id} |
application/hal+json |
wynik pomiaru — bez tego ⇒ 502 |
Negocjacja
Acceptjest tu nietypowo restrykcyjna. Bramka nie akceptuje*/*na końcówkachserviceTest: POST wymaga dokładnieapplication/json(inaczej406 code=62 "Not applicable"), a GET szczegółów wymaga dokładnieapplication/hal+json(inaczej502). Metody klienta ustawiają właściwe nagłówki automatycznie.
Katalog usług (TM Forum API)
Bramka udostępnia API w standardzie TM Forum (TMF). Katalog składa się z trzech warstw:
1. productSpecification — typy usług (katalog)
GET /productCatalogManagement/v1/productSpecification
Zwraca listę specyfikacji (typów usług). W środowisku produkcyjnym dostępne są m.in.:
| id | productSpecificationType | name |
|---|---|---|
DATA2 |
VLAN_BROADBAND |
DATA2 - VLAN N:1 w klasie C3 |
SLA |
ADDON |
Usługa SLA na usuwanie uszkodzeń |
ACCESS_TERMINAL |
DEVICE |
ONT |
POE_INJECTOR |
DEVICE |
Power Injector |
ADDITIONALTASK |
ADDITIONALTASK |
Czynność dodatkowa |
ACCESS |
PRODUCT |
Łącze dostępowe |
Z parametrem fields=...,productSpecCharacteristic każda specyfikacja zawiera charakterystyki —
to one definiują pola/opcje formularza. Dla DATA2 (VLAN_BROADBAND, BSA_N:1):
serviceOption(Opcja prędkości, słownik): 300M/50M, 600M/100M, 1G/300M, 2G/600MclassOfService(Klasa usługi):C3pduType(Rodzaj punktu styku):EthernetidPdu,vlan2pdu,vlan,customerVlan,remoteId,aoBundleId,freezePeriodStart/End— pola techniczne
Każda charakterystyka ma m.in.: name, description, valueType (Dictionary/String/Number),
isRequired, visible, oraz productSpecCharacteristicValue z dozwolonymi wartościami
(value, isDefault, allowedTechnology, np. FTTH).
const specs = await client.getProductSpecifications();
const data2 = await client.getProductSpecifications({
type: "VLAN_BROADBAND",
businessServiceType: "BSA_N:1",
withCharacteristics: true,
});
2. productOffering — oferty
GET /productCatalogManagement/v1/productOffering
Zwraca oferty powiązane ze specyfikacją, np. DATA2_OFFER w kategorii S-I ("Oferty S-I").
const offers = await client.getProductOfferings({
specificationType: "VLAN_BROADBAND",
businessServiceType: "BSA_N:1",
});
3. productInventoryManagement/v1/product — inwentarz (formularz wyszukiwania)
GET /productInventoryManagement/v1/product
Zwraca faktyczne usługi klienta. To zapytanie obsługuje formularz wyszukiwania w portalu. Parametry filtrowania (kropkowa notacja TMF):
| parametr | przykład | opis |
|---|---|---|
productSpecification.id |
DATA2 |
typ usługi |
productSpecification.productSpecificationType |
VLAN_BROADBAND |
kategoria specyfikacji |
businessServiceType |
BSA_N:1 |
typ usługi biznesowej |
characteristic.name |
serviceOption |
nazwa charakterystyki |
characteristic.value |
2G/600M |
wartość charakterystyki |
productRelationship.product.linkId |
026595759023 |
id łącza |
status |
active |
status usługi |
offset, limit |
0, 10 |
paginacja |
fields |
lista pól | które pola zwrócić |
Przykładowy rekord usługi:
{
"id": "300126169065",
"characteristic": [{ "name": "serviceOption", "value": "2G/600M" }],
"productRelationship": [{ "product": { "linkId": "026595759023", "addressName": "84-103 ŁEBCZ UL. WICHROWA 17/" } }],
"productSpecification": { "id": "DATA2", "productSpecificationType": "VLAN_BROADBAND" },
"relatedParty": [{ "id": "8465", "name": "PLASTCOM ... Sp. Jawna", "role": "owner" }],
"startDate": "2026-06-08T20:49:58+02:00",
"status": "active"
}
const products = await client.searchProducts({
specificationId: "DATA2",
specificationType: "VLAN_BROADBAND",
businessServiceType: "BSA_N:1",
characteristicName: "serviceOption",
offset: 0,
limit: 10,
});
Szczegóły usługi — product/{id}
GET /productInventoryManagement/v1/product/{id}
Portal wysyła nagłówki accept: application/hal+json oraz x_client_assent: TRUE
(metoda getProduct ustawia je domyślnie, ale nie są wymagane — sprawdzone).
Odpowiedź jest bogatsza niż rekord z listy — dochodzą:
| pole | przykład | opis |
|---|---|---|
isBundle |
false |
czy usługa jest pakietem |
isCustomerVisible |
true |
widoczność dla klienta |
productOffering |
{ id: "DATA2_OFFER", name: "oferta DATA2" } |
powiązana oferta |
productOrderItem[] |
orderId: "3988380887", orderItemAction: "add" |
referencja do zamówienia (orderHref → productOrderManagement) |
productRelationship[].type |
RELIES_ON |
typ relacji do produktu łącza |
productRelationship[].product.id |
300126169064 |
produkt łącza (osobny byt, href do jego szczegółów) |
productSpecification.name/version |
DATA2 - VLAN N:1 w klasie C3, 1 |
nazwa + wersja specyfikacji |
Uwaga: usługa (
300126169065) RELIES_ON produkt łącza (300126169064) — to dwa powiązane produkty TMF.linkId(026595759023) jest wspólnym identyfikatorem łącza.orderHrefwskazuje na inny host backendu:https://isi-cc.tp.pl/si/rest-api/....
const detail = await client.getProduct("300126169065");
Identyfikacja usług
Każda usługa (Product) jest identyfikowana wieloma kluczami:
| pole | przykład | znaczenie |
|---|---|---|
id |
300126169065 |
główny identyfikator usługi (TMF Product id) |
productRelationship[].product.linkId |
026595759023 |
id łącza dostępowego (fizyczne łącze) |
characteristic customerVlan |
35 |
VLAN w sieci OA (klienta) |
characteristic vlan |
2230 |
VLAN na OLT |
characteristic vlan2pdu |
1201 |
VLAN na punkcie styku |
characteristic idPdu |
GDA_SI_PLASTCOM_A |
identyfikator punktu styku |
relatedParty[].id |
8465 |
właściciel = ukeId operatora (z sesji logowania) |
Stan na 2026-06-16 (konto PLAST_COM_SC, ukeId 8465): 1 aktywna usługa — DATA2 VLAN N:1, opcja 2G/600M, pod adresem 84-103 Łebcz, ul. Wichrowa 17. Pozostałe typy specyfikacji (SLA, ONT, Power Injector, łącze ACCESS) nie mają pozycji w inwentarzu.
Telediagnostyka (serviceTestManagement)
Inicjalizacja pomiaru to pojedynczy POST, który tworzy zlecenie pomiaru
(state: "inprogress"). Pomiar trwa zwykle ~60–90 s, po czym state zmienia się na
completed, a wynik (zbiór metryk) odczytuje się osobnym GET po id. Wynik nie jest
plikiem binarnym — to JSON (patrz „Pobieranie wyników").
Przepływ w portalu (z HAR)
Przed pokazaniem formularza portal doczytuje produkty na łączu:
GET /product?id=300126169064→ACCESS(łącze, linkId026595759023)GET /product?id=300126169065,300126169066→DATA2(usługa) +ACCESS_TERMINAL(ONT)
Następnie wysyła zlecenie:
POST /serviceTestManagement/v1/serviceTest
Accept: application/json ← WYMAGANE (inny Accept, w tym */*, ⇒ 406 "Not applicable")
Content-Type: application/json;charset=UTF-8
token: <token z logowania>
Body:
{
"@type": "WHServiceTestV2",
"@baseType": "ServiceTest",
"name": "Pomiar z Portala",
"mode": "ONDEMAND",
"testSpecification": { "@referredType": "ServiceTestSpecification", "id": "TEST_COMPLEX" },
"relatedParty": [{ "@referredType": "Organization", "id": "8465", "role": "owner", "name": "PLAST_COM_SC" }],
"relatedProduct": { "@referredType": "Product", "id": "300126169065" }
}
relatedParty.id=ukeId,relatedParty.name=oaName— oba z sesji logowania.relatedProduct.id= id usługi (np. DATA2).
Odpowiedź 201:
{
"id": "000000002135910",
"href": "https://portal.swiatlowodinwestycje.pl/si/rest-api/necmplusrest/rest-api/serviceTestManagement/v1/serviceTest/000000002135910",
"name": "Pomiar z Portala",
"mode": "ONDEMAND",
"state": "inprogress",
"startDateTime": "2026-06-16T10:11:31Z",
"testSpecification": { "id": "TEST_COMPLEX" },
"relatedProduct": { "id": "300126169065" },
"relatedParty": [{ "id": "8465", "name": "PLAST_COM_SC", "role": "owner" }]
}
id to „ID pomiaru" widoczne w portalu. state startuje jako inprogress;
status/wynik można odpytywać przez href (= getServiceTest(id)).
Scenariusze testu (testSpecification.id)
| scenariusz | opis (z formularza) |
|---|---|
CHECK_IMPACT |
Sprawdzenie wystąpienia prac planowych i awarii masowych dla usługi |
GET_TRAFFIC |
Odczyt ruchu na porcie online |
TEST_COMPLEX |
Pomiar wszystkich usług na łączu |
TEST_GPON |
Telediagnostyka dla usług w technologii FTTH |
Stałe dostępne jako TEST_SCENARIOS.
import { SiApiClient, TEST_SCENARIOS } from "./src/index.js";
const client = new SiApiClient();
await client.login("uzytkownik@firma.pl", "haslo");
const test = await client.startServiceTest({
productId: "300126169065",
scenario: TEST_SCENARIOS.TEST_COMPLEX,
});
console.log("ID pomiaru:", test.id, "stan:", test.state);
// później: status/wynik
const status = await client.getServiceTest(test.id);
⚠️
startServiceTestrejestruje realne zlecenie pomiaru w systemie SI (tak jak kliknięcie „Wykonaj telediagnostykę" w portalu).
Pobieranie wyników („plików") diagnostyki
Ważne: wynik diagnostyki nie jest osobnym plikiem binarnym. Komunikat w portalu („Wynik pomiaru będzie dostępny wkrótce w pliku") odnosi się do raportu, który portal renderuje po stronie klienta z danych JSON. Backend zwraca wynik jako tablicę
testMeasure[]w szczegółach pomiaru. „Pobranie pliku" = pobranie tego JSON-a (GET poid) i ewentualny zapis/eksport po naszej stronie.
1. Historia pomiarów (lista):
GET /serviceTestManagement/v1/serviceTest
?testSpecification.id=TEST_GPON,GET_TRAFFIC,TEST_COMPLEX,CHECK_IMPACT
&fields=id,startDateTime,relatedProduct,testSpecification,state
&offset=0&limit=10&sort=-id
Zwraca listę pomiarów (najnowsze pierwsze): id, state, testSpecification.id, startDateTime.
2. Wynik pomiaru (szczegóły):
GET /serviceTestManagement/v1/serviceTest/{id}
Accept: application/hal+json ← WYMAGANE (bez tego backend zwraca 502)
Po zakończeniu state: "completed", jest endDateTime i testMeasure[] (dla TEST_COMPLEX ~131 metryk).
Każda metryka (testMeasure[]):
| pole | przykład |
|---|---|
metricName |
COLLECT_DATA.Dane.data.OLT_OntRxPower |
metricDescription |
Pomiar wszystkich usług na łączu |
value |
-16.946 dBm |
criticity |
OK / NOK / NONE |
interpretation |
[COMPLEX-02] Usługa działa niepoprawnie. (gdy istotne) |
captureDateTime |
2026-06-16T10:11:31Z |
Uwaga:
testMeasure[]bywa zakończone elementemnull— należy filtrować (getServiceTestResultrobi to automatycznie). Werdykty to metryki zcriticity≠NONE.
// historia
const history = await client.listServiceTests({ limit: 10 });
// wynik konkretnego pomiaru (uporządkowany)
const result = await client.getServiceTestResult("000000002135910");
console.log(result.state, result.scenario, result.measures.length);
for (const v of result.verdicts) {
console.log(`[${v.criticity}] ${v.metricName}: ${v.interpretation ?? v.value}`);
}
// zapis surowego wyniku do pliku JSON (po naszej stronie)
import { writeFile } from "node:fs/promises";
const raw = await client.getServiceTest("000000002135910");
await writeFile("pomiar-000000002135910.json", JSON.stringify(raw, null, 2));
Struktura metricName
metricName jest hierarchiczny, segmenty rozdzielone kropką:
<SCENARIUSZ>.<Sekcja czytelna>.<ścieżka.techniczna...>.<pole>
- pierwszy segment = grupa/źródło danych (
CHECK_IMPACT,GET_TRAFFIC,COLLECT_DATA,TEST_GPON,TEST_DATA_OUT,TEST_PPP2,TEST_COMPLEX_OUT, …), - drugi segment = czytelna nazwa sekcji (np.
Dane,Lokalizacja,VLAN,Awarie globalne,Uszkodzenie grupowe - brak sygnału), - ostatni segment = nazwa pola (np.
OLT_OntRxPower,rack,upFwdBytes). - Werdykt główny to jedyna metryka, której
metricNamejest równy nazwie scenariusza (np.metricName == "GET_TRAFFIC"); macriticity(OK/NOK) iinterpretation.
getServiceTestResult zwraca to spłaszczone: { id, state, scenario, startDateTime, endDateTime, productId, measures[], verdicts[] }, gdzie verdicts = metryki z criticity ≠ NONE.
Format wyniku per scenariusz
Zweryfikowane na realnych pomiarach (usługa DATA2 300126169065, łącze 026595759023):
| scenariusz | metryk | główne grupy (prefiks metricName) |
werdykt ([kod]) |
|---|---|---|---|
CHECK_IMPACT |
~5 | CHECK_IMPACT (awarie globalne, podejrzenia, parametry: eventCode) |
[EAI-001] brak awarii/prac planowych |
GET_TRAFFIC |
~13 | GET_TRAFFIC (Lokalizacja: rack/shelf/slot/port/ontId; VLAN: cVlan/sVlan; liczniki: up/dnFwdFrames/Bytes) |
[TRAFF-…] jest/brak ruchu |
TEST_GPON |
~103 | TEST_GPON (werdykt) + COLLECT_DATA (~100 pól OLT/ONT) |
[GPONONT-…] łącze i GPON OK |
TEST_COMPLEX |
~131 | COLLECT_DATA (~102), GET_TRAFFIC (~13), CHECK_IMPACT (5), TEST_GPON (1), TEST_DATA_OUT, TEST_PPP2, TEST_COMPLEX_OUT |
[COMPLEX-…] zbiorczy |
TEST_COMPLEXjest złożony — agreguje metryki pozostałych scenariuszy (każdy pod własnym prefiksem).TEST_PPP2zwracaGI-100 Usługa nie może być mierzona danym scenariuszem pomiarowym, gdy nie dotyczy danej usługi.
Sekcja COLLECT_DATA (TEST_GPON / TEST_COMPLEX)
Najbogatsza sekcja — surowe dane z OLT/ONT. Wybrane pola (COLLECT_DATA.Dane.data.*):
| pole | przykład | znaczenie |
|---|---|---|
OLT_OltName |
GD_LEBCZ_GPUCKA_A |
nazwa OLT |
OLT_OltDesc |
… NOKIA ISAM |
model/opis OLT |
OLT_OntSerialNumber |
ZTEGD63CE810 |
nr seryjny ONT |
OLT_UserMacAddress |
48:8F:5A:25:D7:EC |
adres MAC urządzenia klienta (pusty FAILED - nullSubTree, gdy brak świeżego ruchu) |
OLT_OntRxPower / OLT_OltRxPower |
-16.946 dBm / -20.4 dBm |
moc odbierana ONT/OLT |
OLT_OltTxPower / OLT_OntTxPower |
5.5 dBm / 2.472 dBm |
moc nadawana |
OLT_OntDistance |
1.7 km |
dystans ONT od OLT |
OLT_OntOperStatusDesc |
Up |
stan operacyjny ONT |
OLT_OntCurrAlarmsDesc |
No Defects |
bieżące alarmy ONT |
OLT_OntCVLAN / OLT_OntSVLAN |
35 / 2230 |
VLAN klienta / VLAN usługi |
OLT_CustomerId |
300126169065 |
id usługi (= Product id) |
VLANCur1DayUpBytes / …DnBytes |
35: 1244300 |
wolumen ruchu (bieżąca doba), per VLAN |
VLANPrev1DayUpBytes / …DnBytes |
35: 5853355226 |
wolumen ruchu (poprzednia doba) |
Adres MAC pojawia się tylko w
TEST_GPONiTEST_COMPLEX(przezCOLLECT_DATA), i tylko gdy w oknie pomiaru był ruch.CHECK_IMPACTiGET_TRAFFICnie zawierają MAC.
Orange BSA (Hurt Portal)
Portal posh.hurt-orange.pl udostępnia ten sam interfejs danych (TM Forum API) co
Światłowód Inwestycje, ale logowanie jest inne i wymaga certyfikatu klienta (mTLS).
Bazy URL:
- Portal —
https://posh.hurt-orange.pl/portal-opl-be - Bramka API —
https://apigateway-prd.orange.pl/wh/rest-api(zasoby w wersji v2) - SSO —
https://sso.hurt-orange.pl/sso-sc/api/loginservices
Logowanie (mTLS + SSO + OAuth code)
Certyfikat klienta jest drugim składnikiem uwierzytelnienia (twoFAType=CERT) i jest
wymagany tylko przy logowaniu SSO. Po uzyskaniu tokena bramka i portal działają
z samym nagłówkiem token: (jak w SI).
Przepływ (zbadany z HAR + testy na żywo):
POST {SSO}/users/login{username,password}— z certyfikatem klienta →{ code, ukeId, oaName, twoFAType:"CERT", ... }+Set-Cookie: ssoCookie.POST {SSO}/verifyCookie(cookiessoCookie) →{ code }— świeży, jednorazowy kod autoryzacyjny powiązany z sesją SSO.POST {PORTAL}/services/tokens/authorize{grantType:"authorization_code", code}— z certyfikatem oraz literalnym nagłówkiemtoken: "undefined"(bez niego portal zwraca400 BAD_REQUEST) →{ token, ukeId, oaName, csrfToken, ... }.
Certyfikat .p12
Plik .p12 z certyfikatem klienta (CN użytkownika, wystawca Orange Polska Hurt Portal).
Uwagi techniczne (obsłużone automatycznie przez loadOrangeCert):
- p12 jest w starym formacie (3DES) — Node/OpenSSL 3 nie wczyta go jako
pfx, dlatego PEM wyciągany jest przezopenssl ... -legacy, - certyfikat jest podpisany SHA1 — agent TLS używa
ciphers="DEFAULT@SECLEVEL=0", - wysyłany jest tylko liść (
-clcerts), bez łańcucha CA (CA ma za słaby podpis).
Użycie
import { OrangeBsaClient } from "@dariusz/si-api/orange";
// Certyfikat podajesz dopiero przy logowaniu (zalecane):
const client = new OrangeBsaClient();
await client.login("uzytkownik", "haslo", {
p12Path: "/sciezka/ORANGE_certificate_xxx.p12",
passphrase: "haslo-do-p12",
});
const roles = await client.getUserRoles(); // /aaservices/users/roles/{userId}
const specs = await client.getProductSpecifications(); // bramka v2
const inv = await client.searchProducts({ specificationId: "BITSTREAML2" });
await client.logout();
Certyfikat można też podać już w konstruktorze - wtedy login() nie wymaga 3. argumentu:
const client = new OrangeBsaClient({ p12Path: "...", passphrase: "..." });
await client.login("uzytkownik", "haslo");
API OrangeBsaClient
Klasa dziedziczy po SiApiClient - poniżej różnice i elementy specyficzne dla Orange.
new OrangeBsaClient(opts?) - wszystkie pola opcjonalne:
| pole | domyślnie | opis |
|---|---|---|
certPem |
— | gotowy PEM (klucz prywatny + certyfikat klienta) |
p12Path |
— | albo ścieżka do .p12 (PEM wyciągnie openssl) |
passphrase |
— | hasło do .p12 |
token |
— | istniejący token (pomija logowanie) |
ssoBase |
ORANGE_SSO_BASE |
baza SSO loginservices |
portalBase |
ORANGE_PORTAL_BASE |
baza portalu (portal-opl-be) |
gatewayBase |
ORANGE_GATEWAY_BASE |
baza bramki (wh/rest-api) |
Ustawia automatycznie apiVersion: "v2" oraz rolesPath: "/aaservices/users/roles".
Metody specyficzne / nadpisane:
| metoda | opis |
|---|---|
login(username, password, cert?) |
pełny przepływ mTLS+SSO+authorize; cert = {certPem} lub {p12Path, passphrase} (jeśli nie podano w konstruktorze) |
setCert(certPem) |
ustawia/zmienia certyfikat klienta (buduje agenta mTLS) |
getUserRoles() |
jak w SI, ale po userId (nie wewnętrznym id sesji) |
logout() |
dziedziczone z SI (PATCH /loginservices/users/logout) |
Funkcje pomocnicze (eksport z @dariusz/si-api/orange):
| funkcja | opis |
|---|---|
loadOrangeCert(p12Path, passphrase) |
zwraca PEM (klucz + liść) z .p12; używa openssl ... -legacy -clcerts |
ORANGE_PORTAL_BASE, ORANGE_GATEWAY_BASE, ORANGE_SSO_BASE |
stałe baz URL |
Metody danych wspólne z SiApiClient (działają na bramce v2):
getProductSpecifications, getProductOfferings, searchProducts, getProduct,
getOrganizations, startServiceTest, listServiceTests, getServiceTest,
getServiceTestResult. Opis parametrów - patrz sekcje
Katalog usług i Telediagnostyka.
Zweryfikowane na żywo (11/11)
Logowanie, getUserRoles, getOrganizations (1550 organizacji), getProductSpecifications
(z filtrem i withCharacteristics), getProductOfferings, searchProducts, getProduct,
listServiceTests, getServiceTestResult (pomiar completed z metrykami i werdyktem),
logout. Samego startServiceTest nie wyzwalano (tworzy realne zlecenie pomiaru), ale
używa tej samej, sprawdzonej ścieżki v2.
Pułapka portalu: Angular wysyła
content-type: application/json;charset=UTF-8na każdym żądaniu (też GET) - bez tego portal zwraca415(np. na/services/organizations).OrangeBsaClientustawia ten nagłówek domyślnie.
Narzędzia CLI (bin/)
| skrypt | działanie |
|---|---|
node bin/login.js [user] [pass] |
test logowania SI (lub ENV SI_USERNAME/SI_PASSWORD) |
node bin/orange-login.js <user> <pass> <p12Path> [p12Pass] |
test logowania Orange BSA (lub ENV ORANGE_USERNAME/ORANGE_PASSWORD/ORANGE_P12/ORANGE_P12_PASS) |
node bin/run-all-tests.js [productId] |
inicjuje 4 scenariusze, czeka na ukończenie, zapisuje results/<scenario>-<id>.json |
node bin/render-html.js |
generuje wizualizacje HTML z results/*.json → results/html/ (raport per pomiar + index.html) |
run-all-tests.js używa domyślnie productId=300126169065 i danych z ENV (fallback do
zaszytych). Polling co 5 s, timeout 5 min; toleruje przejściowe 502 podczas liczenia.
Struktura projektu
si-api/
├── README.md # ta dokumentacja (aktualizowana na bieżąco)
├── package.json
├── src/
│ ├── index.js # eksporty publiczne (SiApiClient, OrangeBsaClient, *_BASE, ...)
│ ├── client.js # SiApiClient, SiApiError, stałe baz URL i scenariuszy
│ └── orange.js # OrangeBsaClient (mTLS+SSO), loadOrangeCert
├── bin/
│ ├── login.js # CLI do testu logowania SI
│ ├── orange-login.js # CLI do testu logowania Orange BSA (mTLS)
│ ├── run-all-tests.js # inicjacja 4 scenariuszy + polling + zapis JSON
│ └── render-html.js # generator wizualizacji HTML z wyników
└── results/ # wyniki pomiarów (generowane)
├── <scenario>-<id>.json
└── html/ # raporty HTML + index.html