Skip to content

Commit 3b90ddf

Browse files
Support API backend selection (#96)
1 parent fec075f commit 3b90ddf

40 files changed

Lines changed: 3049 additions & 504 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ coverage/
55
.env
66
.env.integration
77
.env.integration.local
8+
.env.integration.*.local
89
.DS_Store
910
.claude/
1011
.serena/

AGENTS.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ Guidance for coding agents working in this repository.
44

55
## Project summary
66

7-
- This is a TypeScript SDK and CLI for the Librus family portal flow.
8-
- The supported path is `portal.librus.pl` authentication plus child-scoped `https://api.librus.pl/3.0` requests.
7+
- This is a TypeScript SDK and CLI for Librus family portal and Gateway API 2.0 flows.
8+
- Supported backends are:
9+
- `api_v3`: `portal.librus.pl` authentication plus child-scoped `https://api.librus.pl/3.0` requests.
10+
- `gateway_api_20`: school-issued login/password authentication plus cookie-backed `https://synergia.librus.pl/gateway/api/2.0` requests.
11+
- `api_v3` is multi-child. `gateway_api_20` is already scoped to one account and must not pretend to support Portal-style child switching.
912
- Do not reintroduce the legacy `synergia.librus.pl` HTML-scraping approach.
1013
- The package exposes both an SDK entrypoint and a CLI binary named `librus`.
1114

@@ -49,6 +52,9 @@ npm run cli -- grades list --child <id-or-login>
4952
## Environment and secrets
5053

5154
- Runtime credentials come from `LIBRUS_PORTAL_EMAIL` and `LIBRUS_PORTAL_PASSWORD`.
55+
- Gateway API 2.0 credentials come from `LIBRUS_GATEWAY_LOGIN` and `LIBRUS_GATEWAY_PASSWORD`.
56+
- Backend selection uses `LIBRUS_API_BACKEND=api_v3|gateway_api_20` when explicit; otherwise complete Portal credentials win, then complete Gateway credentials, then `api_v3` default errors.
57+
- `LIBRUS_CHILD` is an optional default child selector for `api_v3` only. CLI `--child` wins over `LIBRUS_CHILD`; explicit `--child` is unsupported for `gateway_api_20`.
5258
- `LIBRUS_EMAIL` and `LIBRUS_PASSWORD` are kept as compatibility fallbacks.
5359
- Never hardcode credentials, tokens, or session cookies.
5460
- Keep CLI and SDK errors secret-safe. Do not leak passwords, bearer tokens, or raw cookie values in output, logs, tests, or thrown error messages.
@@ -60,7 +66,8 @@ npm run cli -- grades list --child <id-or-login>
6066
- Preserve the JSON-first CLI behavior: normal results go to stdout, structured errors go to stderr, and commands should return non-zero exit codes on failure.
6167
- Follow the existing class boundaries:
6268
- `PortalClient` handles portal login/session concerns.
63-
- `LibrusSession` orchestrates login, child discovery, and child selection.
69+
- `GatewayApi20AuthClient` handles Gateway API 2.0 login/session concerns.
70+
- `LibrusSession` orchestrates backend selection, login, child discovery, and child selection.
6471
- `SynergiaApiClient` handles authenticated API calls for a specific child account.
6572
- Keep compatibility with Node 22+. Node 22 is the supported runtime floor for
6673
SDK, CLI, CI, and dependency updates.
@@ -73,7 +80,7 @@ npm run cli -- grades list --child <id-or-login>
7380

7481
## Change guidance
7582

76-
- Preserve the current portal-based flow unless the task explicitly requires architectural change.
83+
- Preserve existing `api_v3` behavior unless the task explicitly requires architectural change, and keep `gateway_api_20` feature support limited to what the upstream JSON API actually provides.
7784
- All code and documentation changes should be made outside `master`. Prefer one worktree per active feature and one branch per mergeable change. Prefer branches cut from an up-to-date `master`, but allow short-lived dependent branches when the work is intentionally stacked.
7885
- Keep public SDK exports intentional and update `README.md` when user-facing CLI or SDK behavior changes.
7986
- Update `CHANGELOG.md` for notable user-facing changes.

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- Gateway API 2.0 login/password authentication through
12+
`LIBRUS_GATEWAY_LOGIN` and `LIBRUS_GATEWAY_PASSWORD`, including
13+
`LibrusSession.fromGatewayCredentials()` and CLI child-scoped commands that
14+
can run without `--child` in `gateway_api_20`.
15+
- Public API backend selection through `LIBRUS_API_BACKEND`,
16+
`LibrusSessionOptions.apiBackend`, and `LibrusSession.getApiBackend()`.
17+
- Optional `LIBRUS_CHILD` default child selection for `api_v3` CLI commands.
18+
19+
### Changed
20+
21+
- `LIBRUS_EMAIL` and `LIBRUS_PASSWORD` remain documented as deprecated
22+
Portal-only compatibility fallbacks. Use `LIBRUS_PORTAL_EMAIL` and
23+
`LIBRUS_PORTAL_PASSWORD` for `api_v3`, or `LIBRUS_GATEWAY_LOGIN` and
24+
`LIBRUS_GATEWAY_PASSWORD` for `gateway_api_20`.
25+
- Deprecated compatibility aliases now emit Node deprecation warnings when
26+
used.
27+
928
## [0.6.2] - 2026-05-17
1029

1130
### Fixed

README.md

Lines changed: 156 additions & 56 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "librus-sdk",
33
"version": "0.6.2",
4-
"description": "TypeScript SDK and CLI for the Librus family portal flow.",
4+
"description": "TypeScript SDK and CLI for Librus family portal and Gateway API 2.0 flows.",
55
"author": "Andrey Koltsov",
66
"repository": {
77
"type": "git",
@@ -42,14 +42,26 @@
4242
"report:integration": "npm run build && node --env-file=.env.integration.local test/integration/report-all.mjs",
4343
"report:integration:cli": "npm run build && node --env-file=.env.integration.local test/integration/report-cli.mjs",
4444
"report:integration:sdk": "npm run build && node --env-file=.env.integration.local test/integration/report-sdk.mjs",
45+
"report:integration:gateway-api-20": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.gateway-api-20.local test/integration/report-all.mjs",
46+
"report:integration:gateway-api-20:cli": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.gateway-api-20.local test/integration/report-cli.mjs",
47+
"report:integration:gateway-api-20:sdk": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.gateway-api-20.local test/integration/report-sdk.mjs",
48+
"report:integration:synergia": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.synergia.local test/integration/report-all.mjs",
49+
"report:integration:synergia:cli": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.synergia.local test/integration/report-cli.mjs",
50+
"report:integration:synergia:sdk": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.synergia.local test/integration/report-sdk.mjs",
4551
"validate": "npm run lint && npm run format:check && npm run build && npm run test:coverage && npm run pack:check",
4652
"prepublishOnly": "npm run validate",
4753
"prepare": "husky",
4854
"test": "vitest run",
4955
"test:coverage": "vitest run --coverage",
5056
"test:integration": "npm run build && node --env-file=.env.integration.local --test test/integration/*.integration.test.mjs",
5157
"test:integration:cli": "npm run build && node --env-file=.env.integration.local --test test/integration/cli.integration.test.mjs",
52-
"test:integration:sdk": "npm run build && node --env-file=.env.integration.local --test test/integration/sdk.integration.test.mjs"
58+
"test:integration:sdk": "npm run build && node --env-file=.env.integration.local --test test/integration/sdk.integration.test.mjs",
59+
"test:integration:gateway-api-20": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.gateway-api-20.local --test test/integration/*.integration.test.mjs",
60+
"test:integration:gateway-api-20:cli": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.gateway-api-20.local --test test/integration/cli.integration.test.mjs",
61+
"test:integration:gateway-api-20:sdk": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.gateway-api-20.local --test test/integration/sdk.integration.test.mjs",
62+
"test:integration:synergia": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.synergia.local --test test/integration/*.integration.test.mjs",
63+
"test:integration:synergia:cli": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.synergia.local --test test/integration/cli.integration.test.mjs",
64+
"test:integration:synergia:sdk": "npm run build && LIBRUS_API_BACKEND=gateway_api_20 node --env-file=.env.integration.synergia.local --test test/integration/sdk.integration.test.mjs"
5365
},
5466
"files": [
5567
"dist",

scripts/verify-published-package.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export function fetchPublishedPackageMetadata(packageSpec) {
8080
}
8181
}
8282

83+
/* v8 ignore next -- covered by release workflow smoke usage instead of unit import. */
8384
if (import.meta.url === pathToFileURL(resolve(process.argv[1] ?? "")).href) {
8485
const [, , packageName, packageVersion] = process.argv;
8586

src/cli/commands/announcements.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { Command } from "commander";
33
import type { CliContext } from "./common.js";
44
import {
55
addFormatOption,
6+
addChildOption,
67
configureCommand,
8+
createChildApiClient,
79
type CliFormatOptions,
8-
writeChildScopedOutput,
10+
writeOptionalChildScopedOutput,
911
} from "./common.js";
1012

1113
export function createAnnouncementsCommand(context: CliContext): Command {
@@ -14,38 +16,44 @@ export function createAnnouncementsCommand(context: CliContext): Command {
1416
context,
1517
);
1618
const list = configureCommand(
17-
addFormatOption(
18-
new Command("list").description("List school announcements"),
19+
addChildOption(
20+
addFormatOption(
21+
new Command("list").description("List school announcements"),
22+
),
1923
),
2024
context,
2125
);
2226
const get = configureCommand(
23-
addFormatOption(
24-
new Command("get").description("Get a school announcement by id"),
27+
addChildOption(
28+
addFormatOption(
29+
new Command("get").description("Get a school announcement by id"),
30+
),
2531
),
2632
context,
2733
);
2834

29-
list.requiredOption("--child <id-or-login>", "Child account id or login");
30-
list.action(async (options: CliFormatOptions & { child: string }) => {
35+
list.action(async (options: CliFormatOptions & { child?: string }) => {
3136
const session = context.createSession();
32-
const child = await session.resolveChild(options.child);
33-
const client = await session.forChild(child);
37+
const { child, client } = await createChildApiClient(
38+
session,
39+
options.child,
40+
);
3441
const data = await client.listSchoolNotices();
3542

36-
writeChildScopedOutput(context, options.format, child, data);
43+
writeOptionalChildScopedOutput(context, options.format, child, data);
3744
});
3845

39-
get.requiredOption("--child <id-or-login>", "Child account id or login");
4046
get.requiredOption("--id <id>", "Announcement id");
4147
get.action(
42-
async (options: CliFormatOptions & { child: string; id: string }) => {
48+
async (options: CliFormatOptions & { child?: string; id: string }) => {
4349
const session = context.createSession();
44-
const child = await session.resolveChild(options.child);
45-
const client = await session.forChild(child);
50+
const { child, client } = await createChildApiClient(
51+
session,
52+
options.child,
53+
);
4654
const data = await client.getSchoolNotice(options.id);
4755

48-
writeChildScopedOutput(context, options.format, child, data);
56+
writeOptionalChildScopedOutput(context, options.format, child, data);
4957
},
5058
);
5159

src/cli/commands/attendance.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { Command } from "commander";
33
import type { CliContext } from "./common.js";
44
import {
55
addFormatOption,
6+
addChildOption,
67
configureCommand,
8+
createChildApiClient,
79
type CliFormatOptions,
8-
writeChildScopedOutput,
10+
writeOptionalChildScopedOutput,
911
} from "./common.js";
1012

1113
export function createAttendanceCommand(context: CliContext): Command {
@@ -14,20 +16,23 @@ export function createAttendanceCommand(context: CliContext): Command {
1416
context,
1517
);
1618
const list = configureCommand(
17-
addFormatOption(
18-
new Command("list").description("List attendances for a child"),
19+
addChildOption(
20+
addFormatOption(
21+
new Command("list").description("List attendances for a child"),
22+
),
1923
),
2024
context,
2125
);
2226

23-
list.requiredOption("--child <id-or-login>", "Child account id or login");
24-
list.action(async (options: CliFormatOptions & { child: string }) => {
27+
list.action(async (options: CliFormatOptions & { child?: string }) => {
2528
const session = context.createSession();
26-
const child = await session.resolveChild(options.child);
27-
const client = await session.forChild(child);
29+
const { child, client } = await createChildApiClient(
30+
session,
31+
options.child,
32+
);
2833
const data = await client.getAttendances();
2934

30-
writeChildScopedOutput(context, options.format, child, data);
35+
writeOptionalChildScopedOutput(context, options.format, child, data);
3136
});
3237

3338
attendance.addCommand(list);

0 commit comments

Comments
 (0)