@wilm-ai/wilma-client 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/client.d.ts +11 -2
- package/dist/client.js +5 -0
- package/dist/core.d.ts +3 -3
- package/dist/core.js +1 -1
- package/dist/session.d.ts +26 -1
- package/dist/session.js +51 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -50,6 +50,7 @@ Wilma allows one live session per account: a new login ends the previous one, an
|
|
|
50
50
|
- **Log in once** and use `client.forStudent(number)` for each child; they share the login.
|
|
51
51
|
- A session that Wilma ends (another login, or expiry) **logs in again by itself** on the next request — once, even when many requests notice at the same time, including the two-step verification step. A refused password stops further attempts, so a changed password can't lock the account.
|
|
52
52
|
- To continue a session in another process, save `client.exportSession()` (treat it like the password) and later call `WilmaClient.resume(profile, saved, onMfa)`. `client.onLogin(callback)` tells you when a new session should be saved.
|
|
53
|
+
- The password for `resume()` can be a function, e.g. one reading a keychain: `WilmaClient.resume({ baseUrl, username, password: () => readFromKeychain() }, saved)`. It's called only when Wilma has ended the session and a new login is needed, never while the saved one works, and once for many requests. If it fails, the request fails with `CredentialsUnavailableError` (with `reason` and `cause` from your error) and the next request tries again: unlike a password Wilma refuses, it never stops the client for good.
|
|
53
54
|
- `client.logout()` ends the session, as Wilma's own Log out does, so the parent can use Wilma in the browser. It never logs in: a session Wilma has already ended needs nothing, and it returns `false`. Afterwards the client and its `forStudent()` clients don't log in again by themselves. If Wilma can't be reached it throws and keeps the session, so it can be tried again.
|
|
54
55
|
|
|
55
56
|
### Any JavaScript runtime
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SharedSession, type SessionEvent } from "./session.js";
|
|
1
|
+
import { type PasswordSource, type SharedSession, type SessionEvent } from "./session.js";
|
|
2
2
|
import type { Exam, GradebookEntry, Invitation, InvitationListing, MessageRecipient, StudentDetails, LessonNote, LessonNoteSummary, Message, MessageFolder, NewsItem, NewsResource, OverviewData, PastExam, Printout, StudentInfo, UpcomingExam, WilmaProfile } from "./types.js";
|
|
3
3
|
import type { WilmaTransport } from "./transport.js";
|
|
4
4
|
export type MfaCallback = (formkey: string) => Promise<string>;
|
|
@@ -19,6 +19,10 @@ export interface ClientOptions {
|
|
|
19
19
|
*/
|
|
20
20
|
sharedSession?: SharedSession;
|
|
21
21
|
}
|
|
22
|
+
/** A profile for WilmaClient.resume(): the password may be a function called only for a new login. */
|
|
23
|
+
export type ResumeProfile = Omit<WilmaProfile, "password"> & {
|
|
24
|
+
password: PasswordSource;
|
|
25
|
+
};
|
|
22
26
|
export declare class WilmaClient {
|
|
23
27
|
private session;
|
|
24
28
|
private constructor();
|
|
@@ -27,8 +31,13 @@ export declare class WilmaClient {
|
|
|
27
31
|
* A client on a session saved earlier with exportSession(), without logging
|
|
28
32
|
* in. If Wilma has ended that session, the first request logs in again by
|
|
29
33
|
* itself. Returns null when the saved state has no session.
|
|
34
|
+
*
|
|
35
|
+
* The password may be a function (e.g. reading a keychain): it is called
|
|
36
|
+
* only when Wilma needs a new login, never while the saved session works.
|
|
37
|
+
* If it fails, the request fails with CredentialsUnavailableError and the
|
|
38
|
+
* next request tries again.
|
|
30
39
|
*/
|
|
31
|
-
static resume(profile:
|
|
40
|
+
static resume(profile: ResumeProfile, state: string, onMfaRequired?: MfaCallback, options?: ClientOptions): WilmaClient | null;
|
|
32
41
|
/**
|
|
33
42
|
* A client on a saved session without any credentials: it never logs in.
|
|
34
43
|
* When Wilma ends the session, requests fail with SessionEndedError. For
|
package/dist/client.js
CHANGED
|
@@ -47,6 +47,11 @@ export class WilmaClient {
|
|
|
47
47
|
* A client on a session saved earlier with exportSession(), without logging
|
|
48
48
|
* in. If Wilma has ended that session, the first request logs in again by
|
|
49
49
|
* itself. Returns null when the saved state has no session.
|
|
50
|
+
*
|
|
51
|
+
* The password may be a function (e.g. reading a keychain): it is called
|
|
52
|
+
* only when Wilma needs a new login, never while the saved session works.
|
|
53
|
+
* If it fails, the request fails with CredentialsUnavailableError and the
|
|
54
|
+
* next request tries again.
|
|
50
55
|
*/
|
|
51
56
|
static resume(profile, state, onMfaRequired, options = {}) {
|
|
52
57
|
const session = new WilmaSession(profile.baseUrl, {
|
package/dist/core.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export type { Exam, ExamGrade, GradebookEntry, HomeworkItem, Invitation, InvitationListing, InvitationSlot, InvitationSlotState, MessageRecipient, StudentDetails, LessonNote, LessonNoteSummary, Message, MessageReply, MessageFolder, Municipality, NewsItem, NewsResource, NewsResourceAuthContext, OverviewData, PastExam, Printout, ScheduleLesson, TenantDiscoveryResponse, TenantInfo, UpcomingExam, WilmaProfile, StudentInfo, } from "./types.js";
|
|
2
2
|
export { WilmaClient } from "./client.js";
|
|
3
|
-
export type { MfaCallback, ClientOptions } from "./client.js";
|
|
4
|
-
export { WilmaSession, AuthenticationError, MfaRequiredError, APIError, SessionEndedError } from "./session.js";
|
|
5
|
-
export type { SessionOptions, RequestInit, SharedSession, SessionEvent } from "./session.js";
|
|
3
|
+
export type { MfaCallback, ClientOptions, ResumeProfile } from "./client.js";
|
|
4
|
+
export { WilmaSession, AuthenticationError, MfaRequiredError, APIError, SessionEndedError, CredentialsUnavailableError } from "./session.js";
|
|
5
|
+
export type { SessionOptions, RequestInit, SharedSession, SessionEvent, PasswordSource } from "./session.js";
|
|
6
6
|
export { setDefaultTransport } from "./transport.js";
|
|
7
7
|
export type { WilmaTransport, WilmaRequest, WilmaReply } from "./transport.js";
|
|
8
8
|
export { DOWNLOAD_TIMEOUT_MS, isHtmlResponse, setExternalFetcher } from "./downloads.js";
|
package/dist/core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { WilmaClient } from "./client.js";
|
|
2
|
-
export { WilmaSession, AuthenticationError, MfaRequiredError, APIError, SessionEndedError } from "./session.js";
|
|
2
|
+
export { WilmaSession, AuthenticationError, MfaRequiredError, APIError, SessionEndedError, CredentialsUnavailableError } from "./session.js";
|
|
3
3
|
export { setDefaultTransport } from "./transport.js";
|
|
4
4
|
export { DOWNLOAD_TIMEOUT_MS, isHtmlResponse, setExternalFetcher } from "./downloads.js";
|
|
5
5
|
export { NetworkError, describeNetworkCode, extractCauseCode, wrapNetworkError } from "./network-error.js";
|
package/dist/session.d.ts
CHANGED
|
@@ -56,6 +56,24 @@ export declare class APIError extends Error {
|
|
|
56
56
|
export declare class SessionEndedError extends APIError {
|
|
57
57
|
constructor(message?: string);
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The password, or where to get it: a function called only when Wilma needs a
|
|
61
|
+
* new login (e.g. reading a keychain), not while a saved session works.
|
|
62
|
+
*/
|
|
63
|
+
export type PasswordSource = string | (() => Promise<string>);
|
|
64
|
+
/**
|
|
65
|
+
* Wilma needed a new login and the password couldn't be had (the function
|
|
66
|
+
* given as the password failed: a keychain locked, declined or missing the
|
|
67
|
+
* entry). Not a refusal by Wilma, so it never stops the client for good;
|
|
68
|
+
* the next request tries again. `reason` and `cause` come from the failure.
|
|
69
|
+
*/
|
|
70
|
+
export declare class CredentialsUnavailableError extends Error {
|
|
71
|
+
reason: string;
|
|
72
|
+
constructor(message: string, options?: {
|
|
73
|
+
reason?: string;
|
|
74
|
+
cause?: unknown;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
59
77
|
/**
|
|
60
78
|
* What happened to a client's session, for a host that shows it (e.g. a
|
|
61
79
|
* sign-in history): "login" (this client logged in), "adopted" (Wilma had
|
|
@@ -81,7 +99,7 @@ export declare class WilmaSession {
|
|
|
81
99
|
* fails with SessionEndedError. Returns false when the state holds no
|
|
82
100
|
* session to resume.
|
|
83
101
|
*/
|
|
84
|
-
resumeState(state: string, username?: string, password?:
|
|
102
|
+
resumeState(state: string, username?: string, password?: PasswordSource): boolean;
|
|
85
103
|
/** Called after every successful login (first or again). */
|
|
86
104
|
/** Share this login's session with the other WilmAI programs (see SharedSession). */
|
|
87
105
|
setShared(shared: SharedSession | undefined): void;
|
|
@@ -138,6 +156,13 @@ export declare class WilmaSession {
|
|
|
138
156
|
* waits for a fresh code.
|
|
139
157
|
*/
|
|
140
158
|
answerMfa(formkey: string, onMfa: (formkey: string) => Promise<string>): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* The password for a new login. A function given as the password is called
|
|
161
|
+
* here, only now; its answer replaces it (as a login does). If it fails, the
|
|
162
|
+
* session is "ended by Wilma" (never "logged out", never failed for good)
|
|
163
|
+
* and the error is a CredentialsUnavailableError.
|
|
164
|
+
*/
|
|
165
|
+
private resolvePassword;
|
|
141
166
|
/**
|
|
142
167
|
* A live session after Wilma ended this one, once for everyone sharing this
|
|
143
168
|
* login even if many requests notice at the same time: the newest session
|
package/dist/session.js
CHANGED
|
@@ -35,6 +35,20 @@ export class SessionEndedError extends APIError {
|
|
|
35
35
|
this.name = "SessionEndedError";
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Wilma needed a new login and the password couldn't be had (the function
|
|
40
|
+
* given as the password failed: a keychain locked, declined or missing the
|
|
41
|
+
* entry). Not a refusal by Wilma, so it never stops the client for good;
|
|
42
|
+
* the next request tries again. `reason` and `cause` come from the failure.
|
|
43
|
+
*/
|
|
44
|
+
export class CredentialsUnavailableError extends Error {
|
|
45
|
+
reason;
|
|
46
|
+
constructor(message, options = {}) {
|
|
47
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
48
|
+
this.name = "CredentialsUnavailableError";
|
|
49
|
+
this.reason = options.reason ?? "unavailable";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
38
52
|
/** How long Wilma may go silent (before answering, or mid-body) before a request fails. */
|
|
39
53
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
40
54
|
const MAX_REDIRECTS = 10;
|
|
@@ -268,6 +282,9 @@ export class WilmaSession {
|
|
|
268
282
|
const ended = holdsSession(auth.cookieJar, this.baseUrl) ? await this.endSession() : false;
|
|
269
283
|
auth.cookieJar.removeAllCookiesSync();
|
|
270
284
|
auth.loggedIn = false;
|
|
285
|
+
// Logged out on purpose: not "ended by Wilma", so the next request neither
|
|
286
|
+
// logs in again nor continues a session another program saved.
|
|
287
|
+
auth.endedByWilma = false;
|
|
271
288
|
auth.username = undefined;
|
|
272
289
|
auth.password = undefined;
|
|
273
290
|
return ended;
|
|
@@ -444,6 +461,36 @@ export class WilmaSession {
|
|
|
444
461
|
}
|
|
445
462
|
}
|
|
446
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* The password for a new login. A function given as the password is called
|
|
466
|
+
* here, only now; its answer replaces it (as a login does). If it fails, the
|
|
467
|
+
* session is "ended by Wilma" (never "logged out", never failed for good)
|
|
468
|
+
* and the error is a CredentialsUnavailableError.
|
|
469
|
+
*/
|
|
470
|
+
async resolvePassword() {
|
|
471
|
+
const auth = this.auth;
|
|
472
|
+
const source = auth.password;
|
|
473
|
+
if (typeof source !== "function")
|
|
474
|
+
return source ?? "";
|
|
475
|
+
try {
|
|
476
|
+
const password = await source();
|
|
477
|
+
if (typeof password !== "string" || !password)
|
|
478
|
+
throw new Error("No password was returned");
|
|
479
|
+
// logout() may have cleared it meanwhile: then it stays cleared.
|
|
480
|
+
if (auth.password === source)
|
|
481
|
+
auth.password = password;
|
|
482
|
+
return password;
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
auth.loggedIn = false;
|
|
486
|
+
auth.endedByWilma = true;
|
|
487
|
+
auth.onEvent?.("ended");
|
|
488
|
+
if (err instanceof CredentialsUnavailableError)
|
|
489
|
+
throw err;
|
|
490
|
+
const reason = typeof err?.reason === "string" ? err.reason : undefined;
|
|
491
|
+
throw new CredentialsUnavailableError(err instanceof Error ? err.message : String(err), { reason, cause: err });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
447
494
|
/**
|
|
448
495
|
* A live session after Wilma ended this one, once for everyone sharing this
|
|
449
496
|
* login even if many requests notice at the same time: the newest session
|
|
@@ -462,6 +509,9 @@ export class WilmaSession {
|
|
|
462
509
|
auth.onEvent?.("ended");
|
|
463
510
|
throw new SessionEndedError();
|
|
464
511
|
}
|
|
512
|
+
// Get the password before changing anything: if it can't be had, the
|
|
513
|
+
// session stays "ended by Wilma" and the next request tries again.
|
|
514
|
+
const password = await this.resolvePassword();
|
|
465
515
|
auth.loggedIn = false;
|
|
466
516
|
// Start from a clean jar: a leftover session cookie must not make a
|
|
467
517
|
// failed login look successful.
|
|
@@ -469,7 +519,7 @@ export class WilmaSession {
|
|
|
469
519
|
if (this.debug)
|
|
470
520
|
console.error("[wilmai] session cancelled or expired; logging in again");
|
|
471
521
|
try {
|
|
472
|
-
await this.login(auth.username,
|
|
522
|
+
await this.login(auth.username, password);
|
|
473
523
|
}
|
|
474
524
|
catch (err) {
|
|
475
525
|
if (err instanceof MfaRequiredError && auth.onMfa) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wilm-ai/wilma-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Unofficial client for Wilma, the Finnish school information system: login (with two-step verification), schedule, homework, exams, messages and news.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc -p tsconfig.json",
|
|
41
41
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
42
|
-
"test": "pnpm run build && node test/login.mjs && node test/logout.mjs && node test/core.mjs && node test/write.mjs && node test/parser-attendance.mjs && node test/parser-messages.mjs && node test/parser-news.mjs && node test/parser-schedule.mjs && node test/parser-students.mjs && node test/client-students.mjs && node test/client-schedule.mjs && node test/client-attendance.mjs && node test/client-news-resources.mjs && node test/network-error.mjs && node test/audit.mjs && node test/real-pages.mjs && node test/client-features.mjs",
|
|
42
|
+
"test": "pnpm run build && node test/login.mjs && node test/logout.mjs && node test/password-source.mjs && node test/core.mjs && node test/write.mjs && node test/parser-attendance.mjs && node test/parser-messages.mjs && node test/parser-news.mjs && node test/parser-schedule.mjs && node test/parser-students.mjs && node test/client-students.mjs && node test/client-schedule.mjs && node test/client-attendance.mjs && node test/client-news-resources.mjs && node test/network-error.mjs && node test/audit.mjs && node test/real-pages.mjs && node test/client-features.mjs",
|
|
43
43
|
"test:live": "tsc -p tsconfig.json && node test/live.spec.mjs",
|
|
44
44
|
"fixtures:anonymize": "node scripts/anonymize-fixture.mjs"
|
|
45
45
|
},
|