@wilm-ai/wilma-client 1.6.0 → 1.7.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 +48 -1
- package/dist/client.d.ts +76 -4
- package/dist/client.js +114 -9
- package/dist/core.d.ts +16 -0
- package/dist/core.js +11 -0
- package/dist/downloads.d.ts +11 -0
- package/dist/downloads.js +21 -0
- package/dist/external-fetch.d.ts +2 -8
- package/dist/external-fetch.js +2 -6
- package/dist/index.d.ts +3 -11
- package/dist/index.js +14 -9
- package/dist/internal.d.ts +3 -0
- package/dist/internal.js +2 -0
- package/dist/parsers/attendance.d.ts +7 -2
- package/dist/parsers/attendance.js +16 -6
- package/dist/parsers/exams.d.ts +10 -1
- package/dist/parsers/exams.js +91 -0
- package/dist/parsers/forms.d.ts +40 -0
- package/dist/parsers/forms.js +93 -0
- package/dist/parsers/html-text.d.ts +4 -3
- package/dist/parsers/html-text.js +41 -10
- package/dist/parsers/invitations.d.ts +13 -0
- package/dist/parsers/invitations.js +121 -0
- package/dist/parsers/messages.js +4 -0
- package/dist/parsers/news.js +8 -4
- package/dist/parsers/overview.js +1 -0
- package/dist/parsers/recipients.d.ts +16 -0
- package/dist/parsers/recipients.js +170 -0
- package/dist/parsers/schedule.js +7 -2
- package/dist/parsers/students.js +8 -2
- package/dist/session.d.ts +102 -10
- package/dist/session.js +241 -34
- package/dist/timeouts.d.ts +17 -2
- package/dist/timeouts.js +23 -14
- package/dist/transport-node.d.ts +2 -0
- package/dist/transport-node.js +19 -0
- package/dist/transport.d.ts +21 -0
- package/dist/transport.js +19 -0
- package/dist/types.d.ts +111 -0
- package/dist/write.d.ts +143 -0
- package/dist/write.js +427 -0
- package/package.json +10 -2
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Who a parent can write to, and the child's class and supervisors.
|
|
3
|
+
*
|
|
4
|
+
* Seen 2026-10-07 (Helsinki, Wilma 2.36):
|
|
5
|
+
* - the child's front page has a "Huollettavasi tiedot" panel of
|
|
6
|
+
* `.margin-bottom-inline` blocks, each a `<strong>Label:</strong><br>` and
|
|
7
|
+
* its value: the name, the class, the class teacher and other supervisors
|
|
8
|
+
* (each followed by a message link `compose?r_teacher=<id>`), and links to
|
|
9
|
+
* message all supervisors or all the child's teachers;
|
|
10
|
+
* - `/messages/recipients` lists recipients as `button.recipient-block` with
|
|
11
|
+
* the compose page in `data-source`, grouped in panels (the child, the other
|
|
12
|
+
* guardians, all own teachers; staff and teachers per school tab). Disabled
|
|
13
|
+
* buttons (no data-source) can't be written to. "Omat opettajat" (own
|
|
14
|
+
* teachers) one by one loads from the panel link's data-source.
|
|
15
|
+
*/
|
|
16
|
+
import * as cheerio from "cheerio";
|
|
17
|
+
const clean = (text) => text.replace(/\s+/g, " ").trim();
|
|
18
|
+
/** A compose link as a path on this Wilma, or null for anything else. */
|
|
19
|
+
export function composePath(href, pageUrl) {
|
|
20
|
+
if (!href)
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const page = new URL(pageUrl);
|
|
24
|
+
const url = new URL(href, pageUrl);
|
|
25
|
+
if (url.origin !== page.origin || !/^(\/![^/]+)?\/messages\/compose$/.test(url.pathname))
|
|
26
|
+
return null;
|
|
27
|
+
return url.pathname + url.search;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** What a compose link writes to, from its r_ parameters (a repeated or mixed one: several people). */
|
|
34
|
+
export function recipientKind(path) {
|
|
35
|
+
const keys = [...new URL(path, "https://wilma.invalid").searchParams.keys()].filter((k) => k.startsWith("r_"));
|
|
36
|
+
if (keys.length > 1)
|
|
37
|
+
return "group";
|
|
38
|
+
switch (keys[0]) {
|
|
39
|
+
case "r_teacher":
|
|
40
|
+
return "teacher";
|
|
41
|
+
case "r_personnel":
|
|
42
|
+
return "staff";
|
|
43
|
+
case "r_student":
|
|
44
|
+
return "student";
|
|
45
|
+
case "r_guardian":
|
|
46
|
+
return "guardian";
|
|
47
|
+
case "r_ownteachers":
|
|
48
|
+
case "r_groupteachers":
|
|
49
|
+
return "group";
|
|
50
|
+
default:
|
|
51
|
+
return "other";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The child's class and supervisors from their front page (`/!<n>/`); empty fields when the panel isn't there. */
|
|
55
|
+
export function parseStudentDetailsHtml(html, pageUrl) {
|
|
56
|
+
const $ = cheerio.load(html);
|
|
57
|
+
const details = { name: null, className: null, supervisors: [], messageGroups: [] };
|
|
58
|
+
// The panel whose blocks carry labels in <strong>, with a class or a supervisor's message link.
|
|
59
|
+
const panel = $(".panel-body")
|
|
60
|
+
.filter((_, p) => $(p).find(".margin-bottom-inline > strong").length > 0)
|
|
61
|
+
.filter((_, p) => /luokka|klass|class/i.test($(p).find(".margin-bottom-inline > strong").text()) || $(p).find("a[href*='r_teacher']").length > 0)
|
|
62
|
+
.first();
|
|
63
|
+
if (!panel.length)
|
|
64
|
+
return details;
|
|
65
|
+
const group = clean(panel.children("h1, h2").first().text());
|
|
66
|
+
panel.find(".margin-bottom-inline").each((_, block) => {
|
|
67
|
+
const label = clean($(block).children("strong").first().text()).replace(/:$/, "");
|
|
68
|
+
if (!label)
|
|
69
|
+
return;
|
|
70
|
+
const entries = blockEntries($, block, pageUrl);
|
|
71
|
+
if (/^(nimi|namn|name)$/i.test(label))
|
|
72
|
+
details.name = entries[0]?.text ?? null;
|
|
73
|
+
else if (/^(luokka|klass|class)$/i.test(label))
|
|
74
|
+
details.className = entries[0]?.text ?? null;
|
|
75
|
+
else if (entries.some((e) => e.link) || /ohjaaja|valvoja|handledare|föreståndare|supervisor|teacher|tutor/i.test(label)) {
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const recipient = entry.link ? { name: entry.text, group, school: null, kind: recipientKind(entry.link), composePath: entry.link } : null;
|
|
78
|
+
details.supervisors.push({ name: entry.text, role: label, recipient });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
panel.find("a.show-text[href], .margin-top a[href], .margin-top-inline a[href]").each((_, a) => {
|
|
83
|
+
const path = composePath($(a).attr("href"), pageUrl);
|
|
84
|
+
if (!path || details.messageGroups.some((g) => g.composePath === path))
|
|
85
|
+
return;
|
|
86
|
+
const name = clean($(a).text()) || clean($(a).attr("title") ?? "");
|
|
87
|
+
details.messageGroups.push({ name, group, school: null, kind: recipientKind(path), composePath: path });
|
|
88
|
+
});
|
|
89
|
+
return details;
|
|
90
|
+
}
|
|
91
|
+
/** A block's values: text up to each <br>, with the message link that follows a name. */
|
|
92
|
+
function blockEntries($, block, pageUrl) {
|
|
93
|
+
const entries = [];
|
|
94
|
+
let text = "";
|
|
95
|
+
let link = null;
|
|
96
|
+
const flush = () => {
|
|
97
|
+
const value = clean(text);
|
|
98
|
+
if (value)
|
|
99
|
+
entries.push({ text: value, link });
|
|
100
|
+
text = "";
|
|
101
|
+
link = null;
|
|
102
|
+
};
|
|
103
|
+
$(block)
|
|
104
|
+
.contents()
|
|
105
|
+
.each((_, node) => {
|
|
106
|
+
if (node.type === "text") {
|
|
107
|
+
text += node.data;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const el = $(node);
|
|
111
|
+
const tag = node.tagName?.toLowerCase();
|
|
112
|
+
if (tag === "strong")
|
|
113
|
+
return;
|
|
114
|
+
if (tag === "br")
|
|
115
|
+
return flush();
|
|
116
|
+
const href = el.is("a") ? el.attr("href") : el.find("a[href]").first().attr("href");
|
|
117
|
+
const path = composePath(href, pageUrl);
|
|
118
|
+
if (path)
|
|
119
|
+
link = path;
|
|
120
|
+
else
|
|
121
|
+
text += el.text();
|
|
122
|
+
});
|
|
123
|
+
flush();
|
|
124
|
+
return entries;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The recipients page (`/messages/recipients`), or a list it loads (own
|
|
128
|
+
* teachers). Only recipients Wilma lets the parent write to are returned.
|
|
129
|
+
*/
|
|
130
|
+
export function parseRecipientsHtml(html, pageUrl, defaults = {}) {
|
|
131
|
+
const $ = cheerio.load(html);
|
|
132
|
+
const tabNames = new Map();
|
|
133
|
+
$("a[href^='#'][data-toggle='tab'], .nav-tabs a[href^='#'], ul.nav a[href^='#']").each((_, a) => {
|
|
134
|
+
tabNames.set(($(a).attr("href") ?? "").slice(1), clean($(a).text()));
|
|
135
|
+
});
|
|
136
|
+
const out = [];
|
|
137
|
+
$(".recipient-block").each((_, button) => {
|
|
138
|
+
const el = $(button);
|
|
139
|
+
if (el.attr("disabled") !== undefined)
|
|
140
|
+
return;
|
|
141
|
+
const path = composePath(el.attr("data-source"), pageUrl);
|
|
142
|
+
if (!path || out.some((r) => r.composePath === path))
|
|
143
|
+
return;
|
|
144
|
+
const panel = el.closest(".panel");
|
|
145
|
+
const group = clean(panel.find(".panel-title").first().text()) || defaults.group || "";
|
|
146
|
+
const tab = el.closest(".tab-pane").attr("id");
|
|
147
|
+
const school = tab && tabNames.has(tab) && !/common/i.test(tab) ? tabNames.get(tab) : null;
|
|
148
|
+
out.push({ name: clean(el.text()), group, school, kind: recipientKind(path), composePath: path });
|
|
149
|
+
});
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/** Lists the recipients page loads on demand (own teachers one by one): [panel title, path]. */
|
|
153
|
+
export function recipientListSources(html, pageUrl) {
|
|
154
|
+
const $ = cheerio.load(html);
|
|
155
|
+
const page = new URL(pageUrl);
|
|
156
|
+
return $("a.disclosure[data-source]")
|
|
157
|
+
.map((_, a) => {
|
|
158
|
+
try {
|
|
159
|
+
const url = new URL($(a).attr("data-source"), pageUrl);
|
|
160
|
+
if (url.origin !== page.origin || !/\/messages\/recipients\//.test(url.pathname))
|
|
161
|
+
return null;
|
|
162
|
+
return [[clean($(a).text()), url.pathname + url.search]];
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
.get()
|
|
169
|
+
.filter(Boolean);
|
|
170
|
+
}
|
package/dist/parsers/schedule.js
CHANGED
|
@@ -98,7 +98,12 @@ export function parseTimetableJson(payload) {
|
|
|
98
98
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date))
|
|
99
99
|
continue;
|
|
100
100
|
for (const module of lesson.modules ?? []) {
|
|
101
|
-
|
|
101
|
+
// The caption is the course code, sometimes followed by the name
|
|
102
|
+
// ("BI_ Biologia", or just "yRUB1.1" in upper secondary schools); the
|
|
103
|
+
// name alone is in courseName. Lessons without a course (lunch, a
|
|
104
|
+
// class teacher's hour) have only a caption.
|
|
105
|
+
const caption = (module.caption ?? "").trim();
|
|
106
|
+
const subject = (module.courseName ?? "").trim() || caption;
|
|
102
107
|
const teachers = module.teachers ?? [];
|
|
103
108
|
const rooms = (module.rooms ?? []).map((room) => (room.abbreviation ?? room.caption ?? "").trim()).filter(Boolean);
|
|
104
109
|
lessons.push({
|
|
@@ -107,7 +112,7 @@ export function parseTimetableJson(payload) {
|
|
|
107
112
|
start,
|
|
108
113
|
end,
|
|
109
114
|
subject,
|
|
110
|
-
subjectCode: firstSubjectToken(
|
|
115
|
+
subjectCode: firstSubjectToken(caption),
|
|
111
116
|
teacher: teachers.map((t) => [t.lastname, t.firstname].filter(Boolean).join(" ")).join(", "),
|
|
112
117
|
teacherCode: teachers.map((t) => t.abbreviation ?? "").filter(Boolean).join(", "),
|
|
113
118
|
groupId: Number(module.id) || 0,
|
package/dist/parsers/students.js
CHANGED
|
@@ -53,15 +53,21 @@ function roleName(rec, fallback) {
|
|
|
53
53
|
}
|
|
54
54
|
return fallback;
|
|
55
55
|
}
|
|
56
|
-
function collectStudent(students, studentNumber, name) {
|
|
56
|
+
function collectStudent(students, studentNumber, name, role) {
|
|
57
57
|
if (!students.has(studentNumber)) {
|
|
58
58
|
students.set(studentNumber, {
|
|
59
59
|
studentNumber,
|
|
60
60
|
name,
|
|
61
61
|
href: `/!${studentNumber}/`,
|
|
62
|
+
...(role ? { role } : {}),
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
/** A guardian's role for a child, or a student's own login; anything else unknown. */
|
|
67
|
+
function studentRole(type) {
|
|
68
|
+
const value = typeof type === "string" ? type.toLowerCase() : "";
|
|
69
|
+
return value === "guardian" ? "guardian" : value === "student" ? "student" : undefined;
|
|
70
|
+
}
|
|
65
71
|
/**
|
|
66
72
|
* Parse GET /api/v1/accounts/me/roles.
|
|
67
73
|
* Accepts `{ payload: AccountRole[] }` or a raw array.
|
|
@@ -98,7 +104,7 @@ export function parseStudentsFromAccountsRoles(data) {
|
|
|
98
104
|
if (!name) {
|
|
99
105
|
continue;
|
|
100
106
|
}
|
|
101
|
-
collectStudent(students, studentNumber, name);
|
|
107
|
+
collectStudent(students, studentNumber, name, studentRole(roleType(rec)));
|
|
102
108
|
}
|
|
103
109
|
return [...students.values()];
|
|
104
110
|
}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type WilmaTransport } from "./transport.js";
|
|
2
|
+
/** What a request may carry (a subset of fetch's RequestInit that every transport can send). */
|
|
3
|
+
export interface RequestInit {
|
|
4
|
+
method?: string;
|
|
5
|
+
headers?: Record<string, string | undefined> | Headers | [string, string][];
|
|
6
|
+
body?: string;
|
|
7
|
+
redirect?: "follow" | "manual";
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export interface SessionOptions {
|
|
11
|
+
studentNumber?: string | null;
|
|
12
|
+
debug?: boolean;
|
|
13
|
+
/** How requests are sent (default: the transport set by the entry point; undici on Node). */
|
|
14
|
+
transport?: WilmaTransport;
|
|
15
|
+
/**
|
|
16
|
+
* Log in again by itself when Wilma ends the session (default true, with
|
|
17
|
+
* the credentials of the last login). Set false where a background login
|
|
18
|
+
* would be unwelcome — every login ends the parent's other Wilma sessions:
|
|
19
|
+
* requests then fail with SessionEndedError and the caller decides.
|
|
20
|
+
*/
|
|
21
|
+
relogin?: boolean;
|
|
22
|
+
/** Where the WilmAI programs on one computer keep this login's session (see SharedSession). */
|
|
23
|
+
shared?: SharedSession;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The session of one Wilma login, kept where every WilmAI program on the
|
|
27
|
+
* computer finds it (the CLI, assistants' connectors, the desktop app).
|
|
28
|
+
* Wilma allows one live session per account, so programs that each log in
|
|
29
|
+
* keep ending each other's sessions. With a shared session, a client whose
|
|
30
|
+
* session Wilma ended first continues the newest one another program saved,
|
|
31
|
+
* and logs in only when that one has ended too; after its own login it saves
|
|
32
|
+
* the session for the others.
|
|
33
|
+
*/
|
|
34
|
+
export interface SharedSession {
|
|
35
|
+
/** The newest saved session (exportState() format), or null. */
|
|
36
|
+
load(): Promise<string | null>;
|
|
37
|
+
/** Save this client's session (after it logged in). */
|
|
38
|
+
save(state: string): Promise<void>;
|
|
39
|
+
}
|
|
2
40
|
export declare class AuthenticationError extends Error {
|
|
3
41
|
constructor(message: string);
|
|
4
42
|
}
|
|
@@ -10,26 +48,45 @@ export declare class APIError extends Error {
|
|
|
10
48
|
status: number;
|
|
11
49
|
constructor(message: string, status: number);
|
|
12
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Wilma ended the session (another login on the account, or it expired) and
|
|
53
|
+
* this client doesn't log in again by itself (no saved credentials, or
|
|
54
|
+
* `relogin: false`). Nothing was sent; log in again and retry.
|
|
55
|
+
*/
|
|
56
|
+
export declare class SessionEndedError extends APIError {
|
|
57
|
+
constructor(message?: string);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* What happened to a client's session, for a host that shows it (e.g. a
|
|
61
|
+
* sign-in history): "login" (this client logged in), "adopted" (Wilma had
|
|
62
|
+
* ended its session and it continued the one another WilmAI program saved),
|
|
63
|
+
* "ended" (Wilma ended it and the client may not, or could not, get a live
|
|
64
|
+
* one back by itself).
|
|
65
|
+
*/
|
|
66
|
+
export type SessionEvent = "login" | "adopted" | "ended";
|
|
13
67
|
export declare class WilmaSession {
|
|
14
68
|
private baseUrl;
|
|
15
69
|
private auth;
|
|
16
70
|
private studentNumber?;
|
|
17
71
|
private debug;
|
|
18
|
-
constructor(baseUrl: string, opts?:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
});
|
|
72
|
+
constructor(baseUrl: string, opts?: SessionOptions);
|
|
73
|
+
/** The address of this Wilma, e.g. "https://example.inschool.fi". */
|
|
74
|
+
get origin(): string;
|
|
22
75
|
/** The login's cookies, so another process can resume the session (see resumeState). */
|
|
23
76
|
exportState(): string;
|
|
24
77
|
/**
|
|
25
78
|
* Continue a session saved by exportState() instead of logging in. If Wilma
|
|
26
79
|
* has ended it meanwhile, the first request notices (401/403 or the login
|
|
27
|
-
* page) and logs in again with these credentials
|
|
28
|
-
* state holds no
|
|
80
|
+
* page) and logs in again with these credentials; without credentials it
|
|
81
|
+
* fails with SessionEndedError. Returns false when the state holds no
|
|
82
|
+
* session to resume.
|
|
29
83
|
*/
|
|
30
|
-
resumeState(state: string, username
|
|
84
|
+
resumeState(state: string, username?: string, password?: string): boolean;
|
|
31
85
|
/** Called after every successful login (first or again). */
|
|
86
|
+
/** Share this login's session with the other WilmAI programs (see SharedSession). */
|
|
87
|
+
setShared(shared: SharedSession | undefined): void;
|
|
32
88
|
onLogin(callback: (() => void) | undefined): void;
|
|
89
|
+
onSessionEvent(callback: ((event: SessionEvent) => void) | undefined): void;
|
|
33
90
|
/** Used to answer the two-step verification challenge when the session has to log in again. */
|
|
34
91
|
setMfaCallback(onMfa?: (formkey: string) => Promise<string>): void;
|
|
35
92
|
/**
|
|
@@ -42,6 +99,18 @@ export declare class WilmaSession {
|
|
|
42
99
|
getPrefixedPath(path: string): string;
|
|
43
100
|
login(username: string, password: string): Promise<void>;
|
|
44
101
|
submitMfaCode(formkey: string, otpCode: string): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* End this login's session in Wilma, as its "Log out" button does: the
|
|
104
|
+
* logout form on the front page posts its formkey to /logout. Never logs
|
|
105
|
+
* in; a session Wilma has already ended needs nothing. The session is
|
|
106
|
+
* shared with every per-student session made from this one, and afterwards
|
|
107
|
+
* none of them logs in again by itself (call login() for a new session).
|
|
108
|
+
* Returns true when a live session was ended, false when there was none.
|
|
109
|
+
* If Wilma can't be reached, throws and keeps the session, so logging out
|
|
110
|
+
* can be tried again.
|
|
111
|
+
*/
|
|
112
|
+
logout(): Promise<boolean>;
|
|
113
|
+
private endSession;
|
|
45
114
|
private getLoginFormFields;
|
|
46
115
|
/**
|
|
47
116
|
* An authenticated request to this Wilma. Follows redirects within Wilma
|
|
@@ -69,13 +138,36 @@ export declare class WilmaSession {
|
|
|
69
138
|
* waits for a fresh code.
|
|
70
139
|
*/
|
|
71
140
|
answerMfa(formkey: string, onMfa: (formkey: string) => Promise<string>): Promise<void>;
|
|
72
|
-
/**
|
|
73
|
-
|
|
141
|
+
/**
|
|
142
|
+
* A live session after Wilma ended this one, once for everyone sharing this
|
|
143
|
+
* login even if many requests notice at the same time: the newest session
|
|
144
|
+
* another program saved (no login), else a login when this client may log
|
|
145
|
+
* in again. Without either, SessionEndedError.
|
|
146
|
+
*/
|
|
147
|
+
private recover;
|
|
148
|
+
/** Continue the session another program saved, if it differs from ours and Wilma still accepts it. */
|
|
149
|
+
private adoptShared;
|
|
150
|
+
/** After a login: tell the host (onLogin) and save the session for the other programs. */
|
|
151
|
+
private loggedInNow;
|
|
152
|
+
/** Changes whenever the session behind this client changes (a login, or another program's session continued). */
|
|
153
|
+
get sessionMark(): number;
|
|
74
154
|
get(path: string, init?: RequestInit, opts?: {
|
|
75
155
|
externalRedirect?: "error" | "return";
|
|
76
156
|
timeoutMs?: number;
|
|
77
157
|
}): Promise<Response>;
|
|
78
158
|
post(path: string, init?: RequestInit): Promise<Response>;
|
|
159
|
+
/**
|
|
160
|
+
* Send a form that changes something in Wilma (a reply, a sign-up, an
|
|
161
|
+
* absence report): one POST, never repeated. Unlike request(), it doesn't
|
|
162
|
+
* log in again and retry when the session has ended — after a new login
|
|
163
|
+
* the form's key is stale, and a write must not be sent twice — it fails
|
|
164
|
+
* with SessionEndedError instead (nothing was sent; prepare the form
|
|
165
|
+
* again). Redirects within Wilma are followed; the answer is returned for
|
|
166
|
+
* the caller to check.
|
|
167
|
+
*/
|
|
168
|
+
submit(path: string, form: URLSearchParams | Record<string, string> | [string, string][], opts?: {
|
|
169
|
+
headers?: Record<string, string>;
|
|
170
|
+
}): Promise<Response>;
|
|
79
171
|
private getLoginToken;
|
|
80
172
|
/**
|
|
81
173
|
* One request to this Wilma, following redirects within Wilma hop by hop
|