n8n-nodes-pronote 0.1.2 → 0.2.1

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 CHANGED
@@ -1,14 +1,16 @@
1
1
  ## n8n PRONOTE nodes
2
2
 
3
- Custom n8n nodes and small agent helpers that talk to PRONOTE using the same Pawnote flow as the mobile app (mobile user-agent, device UUID, `loginCredentials` / `loginToken` + `createSessionHandle`).
3
+ Custom n8n nodes and small agent helpers that talk to PRONOTE using the Pawnote (mobile user-agent, device UUID, `loginCredentials` / `loginToken` + `createSessionHandle`).
4
4
 
5
5
  ### Available resources
6
6
  - Timetable: fetches the week for a reference date (defaults to today) and groups classes by day.
7
7
  - Tasks: homework for the week of a reference date, with an `onlyPending` toggle.
8
8
  - Grades: list available periods or pull an overview (student/class averages plus detailed grades per subject).
9
+ - **PRONOTE Tool (AI Agents)**: an `ai_tool` node that exposes timetable, tasks, grades, and grade-period listing to AI Agent nodes. The model can set `action`, `referenceDate` (ISO), `periodName`, `onlyPendingTasks`, and `includeGrades`.
10
+ - **PRONOTE Auth Helper**: prepares the mobile validation flow (InfoMobileApp + device UUID for Lycée Connecté/CAS) and validates `mdp` mobile tokens into a usable session/token. Also supports direct username/password login when CAS is not needed.
9
11
 
10
12
  ### Installation
11
- 1) `cd n8n-node-pronote`
13
+ 1) `cd n8n-nodes-pronote`
12
14
  2) Install deps: `npm install`
13
15
  3) Build: `npm run build`
14
16
  4) Copy/symlink the `dist` folder into your n8n custom nodes directory, or publish the package to a registry and install it in n8n.
@@ -21,4 +23,9 @@ Configure the **PRONOTE API** credentials in n8n:
21
23
  - Account kind: Student/Parent/Teacher (defaults to Student).
22
24
 
23
25
  ### Agent helpers
24
- See `agent-tools/pronoteTools.ts` for a thin wrapper you can drop into LLM agent stacks. It reuses the same login and fetch functions as the node.
26
+ See `agent-tools/pronoteTools.ts` for a thin wrapper you can drop into LLM agent stacks. It reuses the same login and fetch functions as the node. For no-code agent flows, drop the **PRONOTE Tool** node into an AI Agent graph and wire it to your agent; the model will decide which action to call and can set the reference date or period dynamically.
27
+
28
+ ### Auth helper flow (Lycée Connecté / CAS)
29
+ 1) Run **PRONOTE Auth Helper** with operation “Prepare Mobile Validation” and your instance URL. It returns a `deviceUUID` and the `InfoMobileApp.json` URL.
30
+ 2) Open that URL in a browser, approve the device in your CAS portal (e.g., Lycée Connecté). Capture the returned `login` and `mdp` (token).
31
+ 3) Run **PRONOTE Auth Helper** with operation “Validate Mobile Token”, providing the `instanceUrl`, `login`, `mdp`, and the same `deviceUUID`. The node outputs a verified token plus user details—use these in the PRONOTE credentials.
@@ -1,6 +1,6 @@
1
- import type { IExecuteFunctions, ILoadOptionsFunctions } from "n8n-workflow";
1
+ import type { IExecuteFunctions, ILoadOptionsFunctions, ISupplyDataFunctions } from "n8n-workflow";
2
2
  import { type PronoteAuthContext } from "../../shared/pawnoteClient";
3
- type Context = IExecuteFunctions | ILoadOptionsFunctions;
3
+ type Context = IExecuteFunctions | ILoadOptionsFunctions | ISupplyDataFunctions;
4
4
  export declare function getPronoteAuth(context: Context, itemIndex?: number): Promise<PronoteAuthContext>;
5
5
  export declare function resolveReferenceDate(raw?: string): Date;
6
6
  export {};
@@ -0,0 +1,5 @@
1
+ import { type IExecuteFunctions, type INodeExecutionData, type INodeType, type INodeTypeDescription } from "n8n-workflow";
2
+ export declare class PronoteAuthHelper implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PronoteAuthHelper = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const pawnote_1 = require("pawnote");
6
+ const n8n_workflow_1 = require("n8n-workflow");
7
+ const pawnoteClient_1 = require("../../shared/pawnoteClient");
8
+ class PronoteAuthHelper {
9
+ constructor() {
10
+ this.description = {
11
+ displayName: "PRONOTE Auth Helper",
12
+ name: "pronoteAuthHelper",
13
+ icon: "file:pronote.svg",
14
+ group: ["transform"],
15
+ version: 1,
16
+ description: "Assist with PRONOTE mobile/CAS token retrieval and validation",
17
+ subtitle: "={{ $parameter[\"operation\"] }}",
18
+ defaults: {
19
+ name: "PRONOTE Auth Helper",
20
+ },
21
+ inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
22
+ outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
23
+ outputNames: ["Result"],
24
+ properties: [
25
+ {
26
+ displayName: "Operation",
27
+ name: "operation",
28
+ type: "options",
29
+ options: [
30
+ {
31
+ name: "Prepare Mobile Validation",
32
+ value: "prepare",
33
+ description: "Generate device UUID and InfoMobileApp URL to start the mobile validation flow (Lycée Connecté/CAS)",
34
+ action: "Prepare mobile validation",
35
+ },
36
+ {
37
+ name: "Validate Mobile Token",
38
+ value: "validate",
39
+ description: "Validate an mdp/token obtained after mobile validation and return session details",
40
+ action: "Validate mobile token",
41
+ },
42
+ {
43
+ name: "Login with Credentials",
44
+ value: "loginCredentials",
45
+ description: "Direct username/password login (for instances without external SSO)",
46
+ action: "Login with credentials",
47
+ },
48
+ ],
49
+ default: "prepare",
50
+ },
51
+ {
52
+ displayName: "Instance URL",
53
+ name: "instanceUrl",
54
+ type: "string",
55
+ default: "",
56
+ placeholder: "https://example.index-education.net/pronote",
57
+ description: "Base URL of the PRONOTE instance",
58
+ },
59
+ {
60
+ displayName: "Device UUID",
61
+ name: "deviceUUID",
62
+ type: "string",
63
+ default: "",
64
+ description: "Optional persistent device UUID. Leave blank to auto-generate.",
65
+ displayOptions: {
66
+ show: {
67
+ operation: ["prepare", "validate", "loginCredentials"],
68
+ },
69
+ },
70
+ },
71
+ {
72
+ displayName: "Username",
73
+ name: "username",
74
+ type: "string",
75
+ default: "",
76
+ description: "PRONOTE username (or returned login from CAS). Required for validate/login.",
77
+ displayOptions: {
78
+ show: {
79
+ operation: ["validate", "loginCredentials"],
80
+ },
81
+ },
82
+ },
83
+ {
84
+ displayName: "Password",
85
+ name: "password",
86
+ type: "string",
87
+ typeOptions: {
88
+ password: true,
89
+ },
90
+ default: "",
91
+ description: "PRONOTE password (not used for CAS mobile tokens).",
92
+ displayOptions: {
93
+ show: {
94
+ operation: ["loginCredentials"],
95
+ },
96
+ },
97
+ },
98
+ {
99
+ displayName: "Mobile Token (mdp)",
100
+ name: "token",
101
+ type: "string",
102
+ typeOptions: {
103
+ password: true,
104
+ },
105
+ default: "",
106
+ description: "The mobile token (mdp) returned after CAS/mobile validation.",
107
+ displayOptions: {
108
+ show: {
109
+ operation: ["validate"],
110
+ },
111
+ },
112
+ },
113
+ {
114
+ displayName: "Account Kind",
115
+ name: "kind",
116
+ type: "options",
117
+ default: "STUDENT",
118
+ options: [
119
+ { name: "Student", value: "STUDENT" },
120
+ { name: "Parent", value: "PARENT" },
121
+ { name: "Teacher", value: "TEACHER" },
122
+ ],
123
+ description: "Account type for the session",
124
+ displayOptions: {
125
+ show: {
126
+ operation: ["validate", "loginCredentials"],
127
+ },
128
+ },
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ async execute() {
134
+ const items = this.getInputData();
135
+ const returnData = [];
136
+ for (let i = 0; i < items.length; i++) {
137
+ const operation = this.getNodeParameter("operation", i);
138
+ const rawInstance = this.getNodeParameter("instanceUrl", i);
139
+ const instanceUrl = (0, pawnote_1.cleanURL)(rawInstance);
140
+ if (!instanceUrl) {
141
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), "Instance URL is required.");
142
+ }
143
+ if (operation === "prepare") {
144
+ const deviceUUID = this.getNodeParameter("deviceUUID", i).trim() || (0, crypto_1.randomUUID)();
145
+ const infoMobileUrl = `${instanceUrl}/InfoMobileApp.json?id=0D264427-EEFC-4810-A9E9-346942A862A4`;
146
+ const mobileWebviewUrl = `${instanceUrl}/mobile.eleve.html?fd=1`;
147
+ returnData.push({
148
+ json: {
149
+ instanceUrl,
150
+ deviceUUID,
151
+ infoMobileUrl,
152
+ mobileWebviewUrl,
153
+ instructions: "Open InfoMobileApp.json in a browser, approve the device in the portal (e.g., Lycée Connecté/CAS). " +
154
+ "After validation, capture the returned login (username) and mdp (token), then run Validate Mobile Token.",
155
+ },
156
+ });
157
+ continue;
158
+ }
159
+ if (operation === "validate") {
160
+ const username = this.getNodeParameter("username", i).trim();
161
+ const token = this.getNodeParameter("token", i).trim();
162
+ const kindRaw = this.getNodeParameter("kind", i) || "STUDENT";
163
+ const deviceUUID = this.getNodeParameter("deviceUUID", i).trim() || (0, crypto_1.randomUUID)();
164
+ if (!username || !token) {
165
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), "Username and mobile token are required.");
166
+ }
167
+ const kind = normalizeKind(kindRaw);
168
+ const auth = await (0, pawnoteClient_1.createPronoteSession)({
169
+ instanceUrl,
170
+ username,
171
+ token,
172
+ deviceUUID,
173
+ kind,
174
+ authMode: "token",
175
+ });
176
+ returnData.push({
177
+ json: {
178
+ instanceUrl: auth.url,
179
+ deviceUUID: auth.deviceUUID,
180
+ username: auth.username,
181
+ token: auth.token,
182
+ kind: auth.kind,
183
+ userDisplayName: auth.session.user.name,
184
+ establishment: auth.session.user.resources[0]?.establishmentName,
185
+ className: auth.session.user.resources[0]?.className,
186
+ },
187
+ });
188
+ continue;
189
+ }
190
+ if (operation === "loginCredentials") {
191
+ const username = this.getNodeParameter("username", i).trim();
192
+ const password = this.getNodeParameter("password", i).trim();
193
+ const kindRaw = this.getNodeParameter("kind", i) || "STUDENT";
194
+ const deviceUUID = this.getNodeParameter("deviceUUID", i).trim() || (0, crypto_1.randomUUID)();
195
+ if (!username || !password) {
196
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), "Username and password are required.");
197
+ }
198
+ const kind = normalizeKind(kindRaw);
199
+ const auth = await (0, pawnoteClient_1.createPronoteSession)({
200
+ instanceUrl,
201
+ username,
202
+ password,
203
+ deviceUUID,
204
+ kind,
205
+ authMode: "credentials",
206
+ });
207
+ returnData.push({
208
+ json: {
209
+ instanceUrl: auth.url,
210
+ deviceUUID: auth.deviceUUID,
211
+ username: auth.username,
212
+ token: auth.token,
213
+ kind: auth.kind,
214
+ userDisplayName: auth.session.user.name,
215
+ establishment: auth.session.user.resources[0]?.establishmentName,
216
+ className: auth.session.user.resources[0]?.className,
217
+ },
218
+ });
219
+ continue;
220
+ }
221
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unsupported operation "${operation}".`);
222
+ }
223
+ return [returnData];
224
+ }
225
+ }
226
+ exports.PronoteAuthHelper = PronoteAuthHelper;
227
+ function normalizeKind(kind) {
228
+ if (typeof kind === "number") {
229
+ return kind;
230
+ }
231
+ const key = kind.toUpperCase();
232
+ return pawnote_1.AccountKind[key] ?? pawnote_1.AccountKind.STUDENT;
233
+ }
@@ -0,0 +1,5 @@
1
+ import { type INodeType, type INodeTypeDescription, type ISupplyDataFunctions, type SupplyData } from "n8n-workflow";
2
+ export declare class PronoteTool implements INodeType {
3
+ description: INodeTypeDescription;
4
+ supplyData(this: ISupplyDataFunctions): Promise<SupplyData>;
5
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PronoteTool = void 0;
4
+ const tools_1 = require("@langchain/core/tools");
5
+ const zod_1 = require("zod");
6
+ const n8n_workflow_1 = require("n8n-workflow");
7
+ const pawnoteClient_1 = require("../../shared/pawnoteClient");
8
+ const GenericFunctions_1 = require("./GenericFunctions");
9
+ class PronoteTool {
10
+ constructor() {
11
+ this.description = {
12
+ displayName: "PRONOTE Tool",
13
+ name: "pronoteTool",
14
+ icon: "file:pronote.svg",
15
+ group: ["transform"],
16
+ version: 1,
17
+ description: "Expose PRONOTE timetable, tasks, and grades to AI Agents",
18
+ subtitle: "={{ $parameter[\"toolDescription\"] || \"PRONOTE tool\" }}",
19
+ defaults: {
20
+ name: "PRONOTE Tool",
21
+ },
22
+ inputs: [],
23
+ outputs: [n8n_workflow_1.NodeConnectionTypes.AiTool],
24
+ outputNames: ["Tool"],
25
+ credentials: [
26
+ {
27
+ name: "pronoteApi",
28
+ required: true,
29
+ },
30
+ ],
31
+ properties: [
32
+ {
33
+ displayName: "Tool Description",
34
+ name: "toolDescription",
35
+ type: "string",
36
+ default: "Use this tool to read timetable, homework/tasks, or grade information from PRONOTE. " +
37
+ "Provide action: timetable, tasks, grades, or gradePeriods. " +
38
+ "Use referenceDate (ISO) for timetable/tasks; periodName for grades.",
39
+ description: "Optional guidance shown to the model about how to use this tool.",
40
+ typeOptions: {
41
+ rows: 3,
42
+ },
43
+ },
44
+ ],
45
+ };
46
+ }
47
+ async supplyData() {
48
+ try {
49
+ const { session } = await (0, GenericFunctions_1.getPronoteAuth)(this);
50
+ const tool = new tools_1.DynamicStructuredTool({
51
+ name: "pronote",
52
+ description: this.getNodeParameter("toolDescription", 0) ||
53
+ "Fetch timetable, homework/tasks, or grade information from PRONOTE. " +
54
+ "Actions: timetable, tasks, grades, gradePeriods. " +
55
+ "Use referenceDate (ISO date) to target a week for timetable/tasks. " +
56
+ "When action=grades, provide periodName (ID or label).",
57
+ schema: zod_1.z.object({
58
+ action: zod_1.z
59
+ .enum(["timetable", "tasks", "grades", "gradePeriods"])
60
+ .describe("What to retrieve from PRONOTE."),
61
+ referenceDate: zod_1.z
62
+ .string()
63
+ .optional()
64
+ .describe("ISO date (e.g. 2024-09-01). Used for timetable and tasks to select the week; defaults to today."),
65
+ periodName: zod_1.z
66
+ .string()
67
+ .optional()
68
+ .describe("Grade period name or ID. Used when action=grades."),
69
+ onlyPendingTasks: zod_1.z
70
+ .boolean()
71
+ .optional()
72
+ .describe("When action=tasks, if true only return undone tasks."),
73
+ includeGrades: zod_1.z
74
+ .boolean()
75
+ .optional()
76
+ .describe("When action=grades, if false only averages are returned."),
77
+ }),
78
+ func: async (input) => {
79
+ const safeStringify = (data) => JSON.stringify(data, null, 2);
80
+ switch (input.action) {
81
+ case "timetable": {
82
+ const referenceDate = (0, GenericFunctions_1.resolveReferenceDate)(input.referenceDate);
83
+ const timetable = await (0, pawnoteClient_1.fetchTimetable)(session, referenceDate);
84
+ return safeStringify(timetable);
85
+ }
86
+ case "tasks": {
87
+ const referenceDate = (0, GenericFunctions_1.resolveReferenceDate)(input.referenceDate);
88
+ const assignments = await (0, pawnoteClient_1.fetchAssignments)(session, referenceDate);
89
+ const filtered = input.onlyPendingTasks
90
+ ? assignments.filter((a) => !a.isDone)
91
+ : assignments;
92
+ return safeStringify(filtered);
93
+ }
94
+ case "gradePeriods": {
95
+ const periods = await (0, pawnoteClient_1.fetchGradePeriods)(session);
96
+ return safeStringify(periods);
97
+ }
98
+ case "grades": {
99
+ const includeGrades = input.includeGrades !== false;
100
+ const overview = await (0, pawnoteClient_1.fetchGrades)(session, input.periodName);
101
+ if (!includeGrades) {
102
+ return safeStringify({
103
+ period: overview.period,
104
+ studentOverall: overview.studentOverall,
105
+ classAverage: overview.classAverage,
106
+ subjects: overview.subjects.map((subject) => ({
107
+ id: subject.id,
108
+ name: subject.name,
109
+ studentAverage: subject.studentAverage,
110
+ classAverage: subject.classAverage,
111
+ maximum: subject.maximum,
112
+ minimum: subject.minimum,
113
+ outOf: subject.outOf,
114
+ })),
115
+ });
116
+ }
117
+ return safeStringify(overview);
118
+ }
119
+ default:
120
+ throw new Error(`Unsupported action: ${input["action"]}`);
121
+ }
122
+ },
123
+ });
124
+ return {
125
+ response: tool,
126
+ };
127
+ }
128
+ catch (error) {
129
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
130
+ }
131
+ }
132
+ }
133
+ exports.PronoteTool = PronoteTool;
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "n8n-nodes-pronote",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "n8n custom nodes and agent utilities to talk to PRONOTE via the Pawnote library.",
5
5
  "license": "MIT",
6
6
  "main": "dist/nodes/Pronote/Pronote.node.js",
7
7
  "types": "dist/nodes/Pronote/Pronote.node.d.ts",
8
+ "keywords": ["n8n-community-node-package", "n8n", "n8n-nodes", "pronote", "pawnote", "langchain"],
8
9
  "scripts": {
9
10
  "build": "tsc -p .",
10
11
  "clean": "rimraf dist"
@@ -13,7 +14,9 @@
13
14
  "dist"
14
15
  ],
15
16
  "dependencies": {
16
- "pawnote": "^1.6.2"
17
+ "@langchain/core": "^0.2.29",
18
+ "pawnote": "^1.6.2",
19
+ "zod": "^3.23.8"
17
20
  },
18
21
  "peerDependencies": {
19
22
  "n8n-core": "^1.0.0",
@@ -28,7 +31,9 @@
28
31
  },
29
32
  "n8n": {
30
33
  "nodes": [
31
- "dist/nodes/Pronote/Pronote.node.js"
34
+ "dist/nodes/Pronote/Pronote.node.js",
35
+ "dist/nodes/Pronote/PronoteTool.node.js",
36
+ "dist/nodes/Pronote/PronoteAuthHelper.node.js"
32
37
  ],
33
38
  "credentials": [
34
39
  "dist/credentials/PronoteApi.credentials.js"