moodle-cli 0.8.0 → 0.9.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.
@@ -3582,7 +3582,7 @@ var require_parse = __commonJS({
3582
3582
  var whitespace = /* @__PURE__ */ new Set([9, 10, 12, 13, 32]);
3583
3583
  var ZERO = "0".charCodeAt(0);
3584
3584
  var NINE = "9".charCodeAt(0);
3585
- function parse7(formula) {
3585
+ function parse8(formula) {
3586
3586
  formula = formula.trim().toLowerCase();
3587
3587
  if (formula === "even") {
3588
3588
  return [2, 0];
@@ -3634,7 +3634,7 @@ var require_parse = __commonJS({
3634
3634
  }
3635
3635
  }
3636
3636
  }
3637
- exports.parse = parse7;
3637
+ exports.parse = parse8;
3638
3638
  }
3639
3639
  });
3640
3640
 
@@ -5084,7 +5084,7 @@ var require_html = __commonJS({
5084
5084
  }).join("");
5085
5085
  }
5086
5086
  set innerHTML(content) {
5087
- const r = parse7(content, this._parseOptions);
5087
+ const r = parse8(content, this._parseOptions);
5088
5088
  const nodes = r.childNodes.length ? r.childNodes : [new text_1.default(content, this)];
5089
5089
  resetParent(nodes, this);
5090
5090
  resetParent(this.childNodes, null);
@@ -5095,7 +5095,7 @@ var require_html = __commonJS({
5095
5095
  content = [content];
5096
5096
  } else if (typeof content == "string") {
5097
5097
  options = Object.assign(Object.assign({}, this._parseOptions), options);
5098
- const r = parse7(content, options);
5098
+ const r = parse8(content, options);
5099
5099
  content = r.childNodes.length ? r.childNodes : [new text_1.default(r.innerHTML, this)];
5100
5100
  }
5101
5101
  resetParent(this.childNodes, null);
@@ -5109,7 +5109,7 @@ var require_html = __commonJS({
5109
5109
  if (node2 instanceof node_1.default) {
5110
5110
  return [node2];
5111
5111
  } else if (typeof node2 == "string") {
5112
- const r = parse7(node2, this._parseOptions);
5112
+ const r = parse8(node2, this._parseOptions);
5113
5113
  return r.childNodes.length ? r.childNodes : [new text_1.default(node2, this)];
5114
5114
  }
5115
5115
  return [];
@@ -5493,7 +5493,7 @@ var require_html = __commonJS({
5493
5493
  if (arguments.length < 2) {
5494
5494
  throw new Error("2 arguments required");
5495
5495
  }
5496
- const p = parse7(html, this._parseOptions);
5496
+ const p = parse8(html, this._parseOptions);
5497
5497
  if (where === "afterend") {
5498
5498
  this.after(...p.childNodes);
5499
5499
  } else if (where === "afterbegin") {
@@ -5639,7 +5639,7 @@ var require_html = __commonJS({
5639
5639
  }
5640
5640
  /** Clone this Node */
5641
5641
  clone() {
5642
- return parse7(this.toString(), this._parseOptions).firstChild;
5642
+ return parse8(this.toString(), this._parseOptions).firstChild;
5643
5643
  }
5644
5644
  };
5645
5645
  exports.default = HTMLElement2;
@@ -5841,7 +5841,7 @@ var require_html = __commonJS({
5841
5841
  return stack;
5842
5842
  }
5843
5843
  exports.base_parse = base_parse;
5844
- function parse7(data, options = {}) {
5844
+ function parse8(data, options = {}) {
5845
5845
  const stack = base_parse(data, options);
5846
5846
  const [root] = stack;
5847
5847
  while (stack.length > 1) {
@@ -5869,7 +5869,7 @@ var require_html = __commonJS({
5869
5869
  }
5870
5870
  return root;
5871
5871
  }
5872
- exports.parse = parse7;
5872
+ exports.parse = parse8;
5873
5873
  function resolveInsertable(insertable) {
5874
5874
  return insertable.map((val) => {
5875
5875
  if (typeof val === "string") {
@@ -5937,18 +5937,18 @@ var require_dist = __commonJS({
5937
5937
  var parse_1 = __importDefault(require_parse2());
5938
5938
  var valid_1 = __importDefault(require_valid());
5939
5939
  exports.valid = valid_1.default;
5940
- function parse7(data, options = {}) {
5940
+ function parse8(data, options = {}) {
5941
5941
  return (0, parse_1.default)(data, options);
5942
5942
  }
5943
- exports.default = parse7;
5944
- exports.parse = parse7;
5945
- parse7.parse = parse_1.default;
5946
- parse7.HTMLElement = html_1.default;
5947
- parse7.CommentNode = comment_1.default;
5948
- parse7.valid = valid_1.default;
5949
- parse7.Node = node_1.default;
5950
- parse7.TextNode = text_1.default;
5951
- parse7.NodeType = type_1.default;
5943
+ exports.default = parse8;
5944
+ exports.parse = parse8;
5945
+ parse8.parse = parse_1.default;
5946
+ parse8.HTMLElement = html_1.default;
5947
+ parse8.CommentNode = comment_1.default;
5948
+ parse8.valid = valid_1.default;
5949
+ parse8.Node = node_1.default;
5950
+ parse8.TextNode = text_1.default;
5951
+ parse8.NodeType = type_1.default;
5952
5952
  }
5953
5953
  });
5954
5954
 
@@ -6292,20 +6292,20 @@ function createOAuthRouter(options) {
6292
6292
  async function handleAuthorizationCodeGrant(form) {
6293
6293
  const code = form.get("code") ?? "";
6294
6294
  const key = `${CODE_PREFIX}${await digestBearerToken(code)}`;
6295
- const record2 = code ? await storage.get(key) : void 0;
6296
- if (record2) await storage.delete(key);
6297
- if (!record2 || record2.expiresAt <= now()) {
6295
+ const record3 = code ? await storage.get(key) : void 0;
6296
+ if (record3) await storage.delete(key);
6297
+ if (!record3 || record3.expiresAt <= now()) {
6298
6298
  return oauthError(400, "invalid_grant", "The authorization code is invalid or expired.");
6299
6299
  }
6300
- if (form.get("client_id") !== record2.clientId) {
6300
+ if (form.get("client_id") !== record3.clientId) {
6301
6301
  return oauthError(400, "invalid_grant", "The authorization code was issued to another client.");
6302
6302
  }
6303
6303
  const redirectUri = form.get("redirect_uri");
6304
- if (redirectUri !== null && redirectUri !== record2.redirectUri) {
6304
+ if (redirectUri !== null && redirectUri !== record3.redirectUri) {
6305
6305
  return oauthError(400, "invalid_grant", "The redirect URI does not match the authorization request.");
6306
6306
  }
6307
6307
  const verifier = form.get("code_verifier") ?? "";
6308
- if (!/^[A-Za-z0-9._~-]{43,128}$/u.test(verifier) || await pkceChallenge(verifier) !== record2.codeChallenge) {
6308
+ if (!/^[A-Za-z0-9._~-]{43,128}$/u.test(verifier) || await pkceChallenge(verifier) !== record3.codeChallenge) {
6309
6309
  return oauthError(400, "invalid_grant", "The PKCE code verifier is invalid.");
6310
6310
  }
6311
6311
  const requestedResource = normalizeResource(form.get("resource"));
@@ -6313,9 +6313,9 @@ function createOAuthRouter(options) {
6313
6313
  return oauthError(400, "invalid_target", "The requested resource is not hosted by this server.");
6314
6314
  }
6315
6315
  return issueTokens({
6316
- clientId: record2.clientId,
6316
+ clientId: record3.clientId,
6317
6317
  resource: resourceUrl,
6318
- scope: record2.scope,
6318
+ scope: record3.scope,
6319
6319
  family: createSecret()
6320
6320
  });
6321
6321
  }
@@ -6325,33 +6325,33 @@ function createOAuthRouter(options) {
6325
6325
  const requestedResource = normalizeResource(form.get("resource"));
6326
6326
  if (requestedResource === void 0) return oauthError(400, "invalid_target", "The requested resource is not hosted by this server.");
6327
6327
  const digest = await digestBearerToken(refreshToken);
6328
- const record2 = await storage.get(`${REFRESH_PREFIX}${digest}`);
6329
- if (!record2) {
6328
+ const record3 = await storage.get(`${REFRESH_PREFIX}${digest}`);
6329
+ if (!record3) {
6330
6330
  const consumed = await storage.get(`${CONSUMED_REFRESH_PREFIX}${digest}`);
6331
6331
  if (consumed) await revokeFamily(consumed.family);
6332
6332
  return oauthError(400, "invalid_grant", "The refresh token is invalid or expired.");
6333
6333
  }
6334
6334
  await storage.delete(`${REFRESH_PREFIX}${digest}`);
6335
- if (record2.resource !== resourceUrl) return oauthError(400, "invalid_grant", "The grant must be authorized again for this resource.");
6336
- if (record2.expiresAt <= now()) return oauthError(400, "invalid_grant", "The refresh token is invalid or expired.");
6335
+ if (record3.resource !== resourceUrl) return oauthError(400, "invalid_grant", "The grant must be authorized again for this resource.");
6336
+ if (record3.expiresAt <= now()) return oauthError(400, "invalid_grant", "The refresh token is invalid or expired.");
6337
6337
  const clientId = form.get("client_id");
6338
- if (clientId !== null && clientId !== record2.clientId) {
6338
+ if (clientId !== null && clientId !== record3.clientId) {
6339
6339
  return oauthError(400, "invalid_grant", "The refresh token was issued to another client.");
6340
6340
  }
6341
6341
  const requestedScope = form.get("scope");
6342
- if (requestedScope && requestedScope.split(/\s+/u).some((scope) => scope && scope !== record2.scope)) {
6342
+ if (requestedScope && requestedScope.split(/\s+/u).some((scope) => scope && scope !== record3.scope)) {
6343
6343
  return oauthError(400, "invalid_scope", "The requested scope exceeds the original grant.");
6344
6344
  }
6345
6345
  await storage.put(`${CONSUMED_REFRESH_PREFIX}${digest}`, {
6346
- ...record2,
6346
+ ...record3,
6347
6347
  expiresAt: now() + CONSUMED_REFRESH_RETENTION_MS
6348
6348
  });
6349
6349
  await prune();
6350
6350
  return issueTokens({
6351
- clientId: record2.clientId,
6352
- resource: record2.resource,
6353
- scope: record2.scope,
6354
- family: record2.family
6351
+ clientId: record3.clientId,
6352
+ resource: record3.resource,
6353
+ scope: record3.scope,
6354
+ family: record3.family
6355
6355
  });
6356
6356
  }
6357
6357
  async function handleRevoke(request) {
@@ -6363,8 +6363,8 @@ function createOAuthRouter(options) {
6363
6363
  if (token) {
6364
6364
  const digest = await digestBearerToken(token);
6365
6365
  for (const prefix of [ACCESS_PREFIX, REFRESH_PREFIX]) {
6366
- const record2 = await storage.get(`${prefix}${digest}`);
6367
- if (record2) await revokeFamily(record2.family);
6366
+ const record3 = await storage.get(`${prefix}${digest}`);
6367
+ if (record3) await revokeFamily(record3.family);
6368
6368
  }
6369
6369
  }
6370
6370
  return new Response(null, { status: 200, headers: { "cache-control": "no-store" } });
@@ -6424,8 +6424,8 @@ function createOAuthRouter(options) {
6424
6424
  async revokeClients(clientId) {
6425
6425
  await storage.delete(PAIRING_KEY);
6426
6426
  for (const prefix of [CLIENT_PREFIX, CODE_PREFIX, ACCESS_PREFIX, REFRESH_PREFIX, CONSUMED_REFRESH_PREFIX]) {
6427
- for (const [key, record2] of await storage.list({ prefix })) {
6428
- if (!clientId || record2.clientId === clientId) await storage.delete(key);
6427
+ for (const [key, record3] of await storage.list({ prefix })) {
6428
+ if (!clientId || record3.clientId === clientId) await storage.delete(key);
6429
6429
  }
6430
6430
  }
6431
6431
  },
@@ -6442,14 +6442,14 @@ function createOAuthRouter(options) {
6442
6442
  async verifyAccessToken(token, resource) {
6443
6443
  if (!token) return null;
6444
6444
  const key = `${ACCESS_PREFIX}${await digestBearerToken(token)}`;
6445
- const record2 = await storage.get(key);
6446
- if (!record2) return null;
6447
- if (record2.expiresAt <= now()) {
6445
+ const record3 = await storage.get(key);
6446
+ if (!record3) return null;
6447
+ if (record3.expiresAt <= now()) {
6448
6448
  await storage.delete(key);
6449
6449
  return null;
6450
6450
  }
6451
- if (record2.resource !== resourceUrl || resource !== resourceUrl) return null;
6452
- return { clientId: record2.clientId, scope: record2.scope };
6451
+ if (record3.resource !== resourceUrl || resource !== resourceUrl) return null;
6452
+ return { clientId: record3.clientId, scope: record3.scope };
6453
6453
  }
6454
6454
  };
6455
6455
  }
@@ -8529,18 +8529,18 @@ var validateAsync = async (schema2, value, _ctx) => {
8529
8529
  return result.issues.length === 0;
8530
8530
  };
8531
8531
  var _encode = (_Err) => {
8532
- const parse7 = _parse(_Err);
8532
+ const parse8 = _parse(_Err);
8533
8533
  const fn = (schema2, value, _ctx, _params) => {
8534
8534
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
8535
- return parse7(schema2, value, ctx, finalizeParams(fn, _params));
8535
+ return parse8(schema2, value, ctx, finalizeParams(fn, _params));
8536
8536
  };
8537
8537
  return fn;
8538
8538
  };
8539
8539
  var encode = /* @__PURE__ */ _encode($ZodRealError);
8540
8540
  var _decode = (_Err) => {
8541
- const parse7 = _parse(_Err);
8541
+ const parse8 = _parse(_Err);
8542
8542
  const fn = (schema2, value, _ctx, _params) => {
8543
- return parse7(schema2, value, _ctx, finalizeParams(fn, _params));
8543
+ return parse8(schema2, value, _ctx, finalizeParams(fn, _params));
8544
8544
  };
8545
8545
  return fn;
8546
8546
  };
@@ -8934,8 +8934,8 @@ var $ZodCheckMaxSize = /* @__PURE__ */ $constructor("$ZodCheckMaxSize", (inst, d
8934
8934
  (_a3 = inst._zod.def).when ?? (_a3.when = _whenHasSize);
8935
8935
  inst._zod.check = (payload) => {
8936
8936
  const input3 = payload.value;
8937
- const size = input3.size;
8938
- if (size <= def.maximum)
8937
+ const size2 = input3.size;
8938
+ if (size2 <= def.maximum)
8939
8939
  return;
8940
8940
  payload.issues.push({
8941
8941
  origin: getSizableOrigin(input3),
@@ -8954,8 +8954,8 @@ var $ZodCheckMinSize = /* @__PURE__ */ $constructor("$ZodCheckMinSize", (inst, d
8954
8954
  (_a3 = inst._zod.def).when ?? (_a3.when = _whenHasSize);
8955
8955
  inst._zod.check = (payload) => {
8956
8956
  const input3 = payload.value;
8957
- const size = input3.size;
8958
- if (size >= def.minimum)
8957
+ const size2 = input3.size;
8958
+ if (size2 >= def.minimum)
8959
8959
  return;
8960
8960
  payload.issues.push({
8961
8961
  origin: getSizableOrigin(input3),
@@ -8974,10 +8974,10 @@ var $ZodCheckSizeEquals = /* @__PURE__ */ $constructor("$ZodCheckSizeEquals", (i
8974
8974
  (_a3 = inst._zod.def).when ?? (_a3.when = _whenHasSize);
8975
8975
  inst._zod.check = (payload) => {
8976
8976
  const input3 = payload.value;
8977
- const size = input3.size;
8978
- if (size === def.size)
8977
+ const size2 = input3.size;
8978
+ if (size2 === def.size)
8979
8979
  return;
8980
- const tooBig = size > def.size;
8980
+ const tooBig = size2 > def.size;
8981
8981
  payload.issues.push({
8982
8982
  origin: getSizableOrigin(input3),
8983
8983
  ...tooBig ? { code: "too_big", maximum: def.size } : { code: "too_small", minimum: def.size },
@@ -21713,11 +21713,11 @@ function _minSize(minimum, params) {
21713
21713
  });
21714
21714
  }
21715
21715
  // @__NO_SIDE_EFFECTS__
21716
- function _size(size, params) {
21716
+ function _size(size2, params) {
21717
21717
  return new $ZodCheckSizeEquals({
21718
21718
  check: "size_equals",
21719
21719
  ...normalizeParams(params),
21720
- size
21720
+ size: size2
21721
21721
  });
21722
21722
  }
21723
21723
  // @__NO_SIDE_EFFECTS__
@@ -23274,8 +23274,8 @@ function rewriteKeyNames(ctx) {
23274
23274
  bySchema.set(entry.schema, entry);
23275
23275
  }
23276
23276
  const rewrites = /* @__PURE__ */ new Map();
23277
- for (const record2 of pendingRecords.get(ctx) ?? []) {
23278
- const seen = ctx.seen.get(record2);
23277
+ for (const record3 of pendingRecords.get(ctx) ?? []) {
23278
+ const seen = ctx.seen.get(record3);
23279
23279
  const names = (seen?.def ?? seen?.schema)?.propertyNames;
23280
23280
  if (!names || names === true || rewrites.has(names))
23281
23281
  continue;
@@ -24992,8 +24992,8 @@ var ZodFile = /* @__PURE__ */ $constructor("ZodFile", (inst, def) => {
24992
24992
  $ZodFile.init(inst, def);
24993
24993
  ZodType.init(inst, def);
24994
24994
  inst._zod.processJSONSchema = (ctx, json2, params) => fileProcessor(inst, ctx, json2, params);
24995
- inst.min = (size, params) => inst.check(_minSize(size, params));
24996
- inst.max = (size, params) => inst.check(_maxSize(size, params));
24995
+ inst.min = (size2, params) => inst.check(_minSize(size2, params));
24996
+ inst.max = (size2, params) => inst.check(_maxSize(size2, params));
24997
24997
  inst.mime = (types, params) => inst.check(_mime(Array.isArray(types) ? types : [types], params));
24998
24998
  });
24999
24999
  function file(params) {
@@ -26452,7 +26452,7 @@ function stringValue(value) {
26452
26452
  }
26453
26453
 
26454
26454
  // src/version.ts
26455
- var VERSION = "0.8.0";
26455
+ var VERSION = "0.9.0";
26456
26456
 
26457
26457
  // src/worker/http.ts
26458
26458
  var HEALTH_PATH = "/healthz";
@@ -27057,6 +27057,9 @@ var FUNC_GET_UNREAD_CONVERSATION_COUNTS = "core_message_get_unread_conversation_
27057
27057
  var FUNC_GET_DISCUSSION_POSTS = "mod_forum_get_discussion_posts";
27058
27058
  var DEFAULT_SESSION_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
27059
27059
 
27060
+ // src/moodle-assign-core.ts
27061
+ var import_node_html_parser3 = __toESM(require_dist(), 1);
27062
+
27060
27063
  // src/html-utils.ts
27061
27064
  var import_node_html_parser = __toESM(require_dist(), 1);
27062
27065
  function htmlToStructuredContent(html, baseUrl) {
@@ -27126,262 +27129,6 @@ function decodeHtml(value) {
27126
27129
  return value.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
27127
27130
  }
27128
27131
 
27129
- // src/parsers.ts
27130
- function schema(parser) {
27131
- return { parse: parser };
27132
- }
27133
- var UserInfoSchema = schema(parseUserInfo);
27134
- var CourseSchema = schema(parseCourse);
27135
- var CoursesSchema = schema(parseCourses);
27136
- var ActivitySchema = schema(parseActivity);
27137
- var SectionSchema = schema(parseSection);
27138
- var CourseContentsSchema = schema(parseCourseContents);
27139
- var TodoItemSchema = schema(parseTodoItem);
27140
- function parseUserInfo(value) {
27141
- const data = asRecord(value);
27142
- return {
27143
- userid: numberValue(data.userid),
27144
- username: stringValue2(data.username),
27145
- fullname: stringValue2(data.fullname),
27146
- sitename: stringValue2(data.sitename),
27147
- siteurl: stringValue2(data.siteurl),
27148
- lang: stringValue2(data.lang),
27149
- ...data.timezone ? { timezone: String(data.timezone) } : {}
27150
- };
27151
- }
27152
- function parseCourse(value, nowSeconds = Math.floor(Date.now() / 1e3)) {
27153
- const data = asRecord(value);
27154
- const course = {
27155
- id: numberValue(data.id),
27156
- shortname: stringValue2(data.shortname),
27157
- fullname: stringValue2(data.fullname),
27158
- category: numberValue(data.category),
27159
- visible: booleanValue(data.visible, true),
27160
- startdate: numberValue(data.startdate)
27161
- };
27162
- const enddate = numberValue(data.enddate);
27163
- if (enddate > 0) {
27164
- course.enddate = enddate;
27165
- }
27166
- return course;
27167
- }
27168
- function parseCourses(value) {
27169
- return asArray(value).map((item) => parseCourse(item));
27170
- }
27171
- function parseActivity(value) {
27172
- const data = asRecord(value);
27173
- return {
27174
- id: numberValue(data.id),
27175
- name: stringValue2(data.name),
27176
- modname: stringValue2(data.modname),
27177
- url: stringValue2(data.url),
27178
- visible: booleanValue(data.visible, true),
27179
- description: stringValue2(data.description),
27180
- ...data.completiondata && typeof data.completiondata === "object" ? { completion: numberValue(asRecord(data.completiondata).state) } : {},
27181
- ...Array.isArray(data.contents) ? { file_entries: data.contents.filter((f) => asRecord(f).fileurl).map((f) => ({ name: stringValue2(asRecord(f).filename), url: stringValue2(asRecord(f).fileurl), requires_authentication: true })) } : {}
27182
- };
27183
- }
27184
- function parseSection(value) {
27185
- const data = asRecord(value);
27186
- return {
27187
- id: numberValue(data.id),
27188
- name: stringValue2(data.name),
27189
- section: numberValue(data.section),
27190
- visible: booleanValue(data.visible, true),
27191
- summary: stringValue2(data.summary),
27192
- ...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
27193
- activities: asArray(data.modules).map((item) => parseActivity(item))
27194
- };
27195
- }
27196
- function parseCourseContents(value) {
27197
- return asArray(value).map((item) => parseSection(item));
27198
- }
27199
- function parseCourseFormatState(value, baseUrl) {
27200
- const state = asRecord(parseJsonValue(value));
27201
- const activities = /* @__PURE__ */ new Map();
27202
- const activitiesBySection = /* @__PURE__ */ new Map();
27203
- for (const item of asArray(state.cm)) {
27204
- const data = asRecord(item);
27205
- const id2 = numberValue(data.id);
27206
- const sectionId = stringValue2(data.sectionid);
27207
- const module = stringValue2(data.module) || stringValue2(data.plugin).replace(/^mod_/u, "") || stringValue2(data.modname).toLowerCase();
27208
- const activity = {
27209
- id: id2,
27210
- name: htmlText(data.name, baseUrl),
27211
- modname: module.toLowerCase(),
27212
- url: stringValue2(data.url) ? resolveUrl(baseUrl, stringValue2(data.url)) : "",
27213
- visible: booleanValue(data.visible, true) && booleanValue(data.uservisible, true) && !booleanValue(data.stealth),
27214
- description: htmlText(data.content ?? data.description, baseUrl),
27215
- ...data.completionstate !== void 0 && data.completionstate !== null ? { completion: numberValue(data.completionstate) } : {}
27216
- };
27217
- activities.set(String(id2), activity);
27218
- const sectionActivities = activitiesBySection.get(sectionId) ?? [];
27219
- sectionActivities.push(activity);
27220
- activitiesBySection.set(sectionId, sectionActivities);
27221
- }
27222
- return asArray(state.section).map((item) => {
27223
- const data = asRecord(item);
27224
- const id2 = numberValue(data.id);
27225
- const hasActivityList = Array.isArray(data.cmlist);
27226
- const listedActivities = asArray(data.cmlist).map((activityId) => activities.get(stringValue2(activityId))).filter((activity) => activity !== void 0);
27227
- return {
27228
- id: id2,
27229
- name: htmlText(data.title || data.rawtitle, baseUrl),
27230
- section: numberValue(data.section ?? data.number),
27231
- visible: booleanValue(data.visible, true),
27232
- summary: htmlText(data.summary, baseUrl),
27233
- ...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
27234
- activities: hasActivityList ? listedActivities : activitiesBySection.get(String(id2)) ?? []
27235
- };
27236
- });
27237
- }
27238
- function parseTodoItem(value) {
27239
- const data = asRecord(value);
27240
- const course = asRecord(data.course);
27241
- const action = asRecord(data.action);
27242
- const progress = course.progress;
27243
- return {
27244
- id: numberValue(data.id),
27245
- name: stringValue2(data.name),
27246
- activity_name: stringValue2(data.activityname),
27247
- modname: stringValue2(data.modulename),
27248
- course_id: numberValue(course.id),
27249
- course_name: stringValue2(course.fullname),
27250
- due_at: numberValue(data.timesort) || numberValue(data.timestart),
27251
- overdue: booleanValue(data.overdue),
27252
- actionable: booleanValue(action.actionable),
27253
- action_name: stringValue2(action.name),
27254
- action_url: stringValue2(action.url),
27255
- url: stringValue2(data.url),
27256
- event_type: stringValue2(data.eventtype),
27257
- course_progress: typeof progress === "number" ? progress : void 0
27258
- };
27259
- }
27260
- function parseTodoItems(value) {
27261
- return asArray(value).map((item) => parseTodoItem(item));
27262
- }
27263
- function parseAlertNotification(value) {
27264
- const data = asRecord(value);
27265
- return {
27266
- id: numberValue(data.id),
27267
- subject: stringValue2(data.subject),
27268
- short_subject: stringValue2(data.shortenedsubject),
27269
- event_type: stringValue2(data.eventtype),
27270
- component: stringValue2(data.component),
27271
- created_at: numberValue(data.timecreated),
27272
- created_pretty: stringValue2(data.timecreatedpretty),
27273
- read: booleanValue(data.read),
27274
- context_url: stringValue2(data.contexturl),
27275
- context_name: stringValue2(data.contexturlname)
27276
- };
27277
- }
27278
- function parseAlertSummary(notificationsData, countsData, unreadCountsData) {
27279
- const notificationsRecord = asRecord(notificationsData);
27280
- const counts2 = asRecord(countsData);
27281
- const unreadCounts = asRecord(unreadCountsData);
27282
- const types = asRecord(counts2.types);
27283
- const unreadTypes = asRecord(unreadCounts.types);
27284
- const notifications = asArray(notificationsRecord.notifications).map((item) => parseAlertNotification(item));
27285
- return {
27286
- notifications,
27287
- notification_count: notifications.length,
27288
- unread_notification_count: notifications.filter((notification) => !notification.read).length,
27289
- starred_message_count: numberValue(counts2.favourites),
27290
- direct_message_count: numberValue(types["1"]),
27291
- group_message_count: numberValue(types["2"]),
27292
- self_message_count: numberValue(types["3"]),
27293
- unread_starred_message_count: numberValue(unreadCounts.favourites),
27294
- unread_direct_message_count: numberValue(unreadTypes["1"]),
27295
- unread_group_message_count: numberValue(unreadTypes["2"]),
27296
- unread_self_message_count: numberValue(unreadTypes["3"])
27297
- };
27298
- }
27299
- function parseForumPostAuthor(value) {
27300
- const data = asRecord(value);
27301
- const urls = asRecord(data.urls);
27302
- return {
27303
- id: numberValue(data.id),
27304
- fullname: stringValue2(data.fullname),
27305
- profile_url: stringValue2(urls.profile),
27306
- profile_image_url: stringValue2(urls.profileimage)
27307
- };
27308
- }
27309
- function parseForumPost(value, baseUrl = "") {
27310
- const data = asRecord(value);
27311
- const urls = asRecord(data.urls);
27312
- const messageHtml = stringValue2(data.message);
27313
- const structured = htmlToStructuredContent(messageHtml, stringValue2(urls.view || urls.discuss) || baseUrl);
27314
- return {
27315
- id: numberValue(data.id),
27316
- discussion_id: numberValue(data.discussionid),
27317
- subject: stringValue2(data.subject),
27318
- message_html: messageHtml,
27319
- message_text: structured.text,
27320
- image_urls: structured.image_urls,
27321
- links: structured.links,
27322
- tables: structured.tables,
27323
- author: parseForumPostAuthor(data.author),
27324
- parent_id: numberValue(data.parentid),
27325
- time_created: numberValue(data.timecreated),
27326
- time_modified: numberValue(data.timemodified),
27327
- created_pretty: "",
27328
- unread: booleanValue(data.unread),
27329
- is_deleted: booleanValue(data.isdeleted),
27330
- is_private_reply: booleanValue(data.isprivatereply),
27331
- url: stringValue2(urls.view || urls.viewisolated),
27332
- reply_url: stringValue2(urls.reply)
27333
- };
27334
- }
27335
- function parseForumDiscussion(value, discussionId, baseUrl = "") {
27336
- const data = asRecord(value);
27337
- const posts = asArray(data.posts).map((item) => parseForumPost(item, baseUrl));
27338
- return {
27339
- id: discussionId,
27340
- subject: posts[0]?.subject ?? "",
27341
- course_id: numberValue(data.courseid),
27342
- forum_id: numberValue(data.forumid),
27343
- group_id: numberValue(data.groupid),
27344
- group_name: stringValue2(data.groupname),
27345
- url: posts[0]?.url ? posts[0].url.split("#", 1)[0] : "",
27346
- posts
27347
- };
27348
- }
27349
- function asRecord(value) {
27350
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
27351
- }
27352
- function asArray(value) {
27353
- return Array.isArray(value) ? value : [];
27354
- }
27355
- function stringValue2(value) {
27356
- return typeof value === "string" ? value : value == null ? "" : String(value);
27357
- }
27358
- function numberValue(value) {
27359
- if (typeof value === "number" && Number.isFinite(value)) {
27360
- return value;
27361
- }
27362
- if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
27363
- return Number(value);
27364
- }
27365
- return 0;
27366
- }
27367
- function booleanValue(value, defaultValue = false) {
27368
- if (value === void 0 || value === null) {
27369
- return defaultValue;
27370
- }
27371
- return Boolean(value);
27372
- }
27373
- function parseJsonValue(value) {
27374
- if (typeof value !== "string") return value;
27375
- try {
27376
- return JSON.parse(value);
27377
- } catch {
27378
- return {};
27379
- }
27380
- }
27381
- function htmlText(value, baseUrl) {
27382
- return htmlToStructuredContent(stringValue2(value), baseUrl).text;
27383
- }
27384
-
27385
27132
  // src/scraper.ts
27386
27133
  var import_node_html_parser2 = __toESM(require_dist(), 1);
27387
27134
  function parseMoodleErrorHtml(html) {
@@ -27413,8 +27160,8 @@ function parseMoodleErrorHtml(html) {
27413
27160
  function parsePageContext(html, baseUrl) {
27414
27161
  const root = (0, import_node_html_parser2.parse)(html);
27415
27162
  const config2 = parseMoodleConfig(html);
27416
- const sesskey = stringValue3(config2.sesskey).trim();
27417
- const userid = numberValue2(config2.userId) || numberValue2(root.querySelector("[data-user-id]")?.getAttribute("data-user-id"));
27163
+ const sesskey = stringValue2(config2.sesskey).trim();
27164
+ const userid = numberValue(config2.userId) || numberValue(root.querySelector("[data-user-id]")?.getAttribute("data-user-id"));
27418
27165
  if (!sesskey || !userid) {
27419
27166
  throw new Error("Session appears invalid: could not load authenticated Moodle context");
27420
27167
  }
@@ -27426,8 +27173,8 @@ function parsePageContext(html, baseUrl) {
27426
27173
  fullname: cleanNodeText(root.querySelector(".userfullname")),
27427
27174
  sitename: extractSitename(root),
27428
27175
  siteurl: baseUrl,
27429
- ...config2.timezone ? { timezone: stringValue3(config2.timezone) } : {},
27430
- lang: stringValue3(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
27176
+ ...config2.timezone ? { timezone: stringValue2(config2.timezone) } : {},
27177
+ lang: stringValue2(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
27431
27178
  }
27432
27179
  };
27433
27180
  }
@@ -27967,7 +27714,7 @@ function numberQueryValue(href, key) {
27967
27714
  return null;
27968
27715
  }
27969
27716
  }
27970
- function numberValue2(value) {
27717
+ function numberValue(value) {
27971
27718
  if (typeof value === "number" && Number.isFinite(value)) {
27972
27719
  return value;
27973
27720
  }
@@ -27976,7 +27723,7 @@ function numberValue2(value) {
27976
27723
  }
27977
27724
  return 0;
27978
27725
  }
27979
- function stringValue3(value) {
27726
+ function stringValue2(value) {
27980
27727
  return typeof value === "string" ? value : value == null ? "" : String(value);
27981
27728
  }
27982
27729
  function fileEntry(name, url2, baseUrl) {
@@ -27990,6 +27737,619 @@ function unique(items) {
27990
27737
  return [...new Set(items)];
27991
27738
  }
27992
27739
 
27740
+ // src/moodle-assign-core.ts
27741
+ async function submitAssignmentFiles(deps, request) {
27742
+ const id2 = request.activityId;
27743
+ if (!Number.isSafeInteger(id2) || id2 <= 0) throw deps.usage("The assignment id must be a positive integer.");
27744
+ if (!request.files.length && !request.final) throw deps.usage("Give at least one file to upload, or use --final to submit the existing draft.");
27745
+ const seen = /* @__PURE__ */ new Set();
27746
+ for (const file3 of request.files) {
27747
+ if (!file3.name || /[\\/]/u.test(file3.name)) throw deps.usage(`'${file3.name}' is not a plain file name.`);
27748
+ if (seen.has(file3.name.toLowerCase())) throw deps.usage(`'${file3.name}' is given twice; Moodle keeps one file per name.`);
27749
+ seen.add(file3.name.toLowerCase());
27750
+ }
27751
+ const viewUrl = `${deps.baseUrl}${ASSIGN_VIEW_PATH}?id=${id2}`;
27752
+ request.onProgress?.("Reading the assignment");
27753
+ const before = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
27754
+ const form = parseSubmissionForm(await pageText(deps, `${viewUrl}&action=editsubmission`), deps);
27755
+ const draft = await listDraftFiles(deps, form);
27756
+ const removed = request.replace ? draft.map((file3) => file3.name) : [];
27757
+ const kept = request.replace ? [] : draft.filter((file3) => !seen.has(file3.name.toLowerCase()));
27758
+ checkLimits(deps, form, kept, request.files);
27759
+ let statement = form.statement;
27760
+ let confirm;
27761
+ if (request.final && !statement) {
27762
+ confirm = parseConfirmForm(await pageText(deps, `${viewUrl}&action=submit`), deps);
27763
+ statement = confirm.statement;
27764
+ }
27765
+ if (statement && !request.acceptStatement) {
27766
+ throw deps.usage(`Moodle requires you to accept this statement: "${statement}"`, "Re-run with --accept-statement once you agree.");
27767
+ }
27768
+ const limits = describeLimits(form);
27769
+ const uploads = request.files.map((file3) => ({ name: file3.name, bytes: file3.bytes.byteLength, ...file3.path ? { path: file3.path } : {} }));
27770
+ if (request.dryRun) {
27771
+ return {
27772
+ ...before,
27773
+ action: "planned",
27774
+ files: draft.map((file3) => ({ name: file3.name, bytes: file3.bytes })),
27775
+ uploads,
27776
+ removed,
27777
+ limits,
27778
+ ...statement ? { statement, statement_accepted: true } : {},
27779
+ checked_at: timestamp(deps)
27780
+ };
27781
+ }
27782
+ if (removed.length) {
27783
+ request.onProgress?.(`Removing ${removed.join(", ")}`);
27784
+ await deleteDraftFiles(deps, form, draft);
27785
+ }
27786
+ const storedNames = [];
27787
+ for (const [index, file3] of request.files.entries()) {
27788
+ request.onProgress?.(`Uploading ${file3.name} (${index + 1}/${request.files.length})`);
27789
+ storedNames.push(await uploadDraftFile(deps, form, file3));
27790
+ }
27791
+ if (storedNames.length || removed.length) {
27792
+ request.onProgress?.("Saving the submission");
27793
+ const savedHtml = await postForm(deps, form.action, [...form.fields, ...form.statement ? [["submissionstatement", "1"]] : [], ["submitbutton", "Save changes"]]);
27794
+ if (savedHtml !== null) throw deps.fail(`Moodle did not save the submission: ${noticesOf(savedHtml) || "it returned the edit form again without a reason"}`);
27795
+ }
27796
+ request.onProgress?.("Reading the receipt");
27797
+ let receipt = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
27798
+ const listed = new Set(receipt.files.map((file3) => file3.name.toLowerCase()));
27799
+ const missing = storedNames.filter((name) => !listed.has(name.toLowerCase()));
27800
+ if (missing.length) throw deps.fail(`Moodle saved the submission but its page does not list ${missing.join(", ")}; check the assignment in a browser before submitting.`);
27801
+ let action = "saved";
27802
+ if (isSubmitted(receipt.submission_status)) action = "submitted";
27803
+ else if (request.final) {
27804
+ request.onProgress?.("Submitting for grading");
27805
+ confirm ??= parseConfirmForm(await pageText(deps, `${viewUrl}&action=submit`), deps);
27806
+ const errorHtml = await postForm(deps, confirm.action, [...confirm.fields, ...confirm.statement ? [["submissionstatement", "1"]] : [], ["submitbutton", "Continue"]]);
27807
+ if (errorHtml !== null) throw deps.fail(`Moodle did not submit the assignment for grading: ${noticesOf(errorHtml) || "it returned the confirmation page again without a reason"}`);
27808
+ receipt = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
27809
+ if (!isSubmitted(receipt.submission_status)) throw deps.fail(`Moodle accepted the confirmation but still reports "${receipt.submission_status || "no status"}"; check the assignment in a browser.`);
27810
+ action = "submitted";
27811
+ }
27812
+ return {
27813
+ ...receipt,
27814
+ action,
27815
+ uploads,
27816
+ removed,
27817
+ limits,
27818
+ ...statement ? { statement, statement_accepted: true } : {},
27819
+ checked_at: timestamp(deps)
27820
+ };
27821
+ }
27822
+ function parseSubmissionForm(html, deps) {
27823
+ const root = (0, import_node_html_parser3.parse)(html);
27824
+ const form = formWithAction(root, "savesubmission");
27825
+ if (!form) {
27826
+ const notice = noticesOf(html);
27827
+ if (root.querySelectorAll("input[type=submit], button").some((button) => /begin assignment/iu.test(cleanText(button.getAttribute("value") ?? button.textContent)))) {
27828
+ throw deps.fail("This is a timed assignment; start it in a browser before uploading files.");
27829
+ }
27830
+ throw deps.fail(notice ? `Moodle is not accepting a submission: ${notice}` : "Moodle did not show a submission form for this assignment.");
27831
+ }
27832
+ const itemid = form.querySelector("input[name=files_filemanager]")?.getAttribute("value")?.trim() ?? "";
27833
+ if (!itemid) throw deps.fail("This assignment does not accept file uploads.");
27834
+ const options = filemanagerOptions(html, itemid);
27835
+ if (!options) throw deps.fail("Moodle did not describe the file upload area for this assignment.");
27836
+ const fields2 = formFields(form);
27837
+ const sesskey = fields2.find(([name]) => name === "sesskey")?.[1] ?? "";
27838
+ if (!sesskey) throw deps.fail("The submission form has no session key.");
27839
+ const picker = record2(options.filepicker);
27840
+ const repositories = (Array.isArray(picker.repositories) ? picker.repositories : Object.values(record2(picker.repositories))).map(record2);
27841
+ const upload = repositories.find((repo) => repo.type === "upload");
27842
+ if (!upload || upload.id === void 0) throw deps.fail("The site does not allow direct file uploads for this assignment.");
27843
+ const accepted = options.accepted_types;
27844
+ const acceptedTypes = Array.isArray(accepted) ? accepted.map(String).filter(Boolean) : accepted === void 0 || accepted === "*" ? "*" : [String(accepted)];
27845
+ return {
27846
+ action: resolveUrl(deps.baseUrl, form.getAttribute("action") || `${deps.baseUrl}${ASSIGN_VIEW_PATH}`),
27847
+ fields: fields2,
27848
+ sesskey,
27849
+ itemid,
27850
+ clientId: String(options.client_id ?? ""),
27851
+ contextId: String(record2(options.context).id ?? ""),
27852
+ repoId: String(upload.id),
27853
+ author: String(picker.author ?? ""),
27854
+ license: String(picker.defaultlicense ?? ""),
27855
+ maxBytes: integer2(options.maxbytes),
27856
+ areaMaxBytes: integer2(options.areamaxbytes),
27857
+ maxFiles: integer2(options.maxfiles),
27858
+ acceptedTypes: acceptedTypes.length === 1 && acceptedTypes[0] === "*" ? "*" : acceptedTypes,
27859
+ ...statementOf(form)
27860
+ };
27861
+ }
27862
+ function parseConfirmForm(html, deps) {
27863
+ const root = (0, import_node_html_parser3.parse)(html);
27864
+ const form = formWithAction(root, "confirmsubmit");
27865
+ if (!form) {
27866
+ const notice = noticesOf(html);
27867
+ throw deps.fail(notice ? `Moodle is not accepting a submission for grading: ${notice}` : "Moodle did not show the submit-for-grading confirmation.");
27868
+ }
27869
+ return { action: resolveUrl(deps.baseUrl, form.getAttribute("action") || `${deps.baseUrl}${ASSIGN_VIEW_PATH}`), fields: formFields(form), ...statementOf(form) };
27870
+ }
27871
+ function parseReceiptPage(html, activityId, baseUrl) {
27872
+ const page2 = parseAssignmentHtml(html, activityId, baseUrl);
27873
+ const root = (0, import_node_html_parser3.parse)(html);
27874
+ const files = /* @__PURE__ */ new Map();
27875
+ const add = (link) => {
27876
+ const name = cleanText(link.textContent);
27877
+ const href = link.getAttribute("href") ?? "";
27878
+ if (name && !files.has(name.toLowerCase())) files.set(name.toLowerCase(), { name, ...href ? { url: resolveUrl(baseUrl, href) } : {} });
27879
+ };
27880
+ for (const link of root.querySelectorAll(".fileuploadsubmission a[href]")) add(link);
27881
+ for (const link of tableCell(root, "File submissions")?.querySelectorAll("a[href]") ?? []) add(link);
27882
+ return {
27883
+ id: activityId,
27884
+ name: page2.name,
27885
+ ...page2.course_id ? { unit_id: page2.course_id } : {},
27886
+ url: page2.url,
27887
+ submission_status: page2.submission_status,
27888
+ grading_status: page2.grading_status,
27889
+ due: page2.due_pretty || cleanText(tableCell(root, "Due date")?.textContent),
27890
+ time_remaining: page2.time_remaining,
27891
+ last_modified: cleanText(tableCell(root, "Last modified")?.textContent),
27892
+ files: [...files.values()]
27893
+ };
27894
+ }
27895
+ function noticesOf(html) {
27896
+ const root = (0, import_node_html_parser3.parse)(html);
27897
+ const texts = [];
27898
+ for (const node2 of root.querySelectorAll(".alert, .invalid-feedback, .form-control-feedback, .error, [data-fieldtype] .text-danger")) {
27899
+ for (const junk of node2.querySelectorAll("button, .close")) junk.remove();
27900
+ const text = cleanText(node2.textContent);
27901
+ if (text && !texts.includes(text)) texts.push(text);
27902
+ }
27903
+ return texts.join(" ");
27904
+ }
27905
+ function formWithAction(root, action) {
27906
+ for (const form of root.querySelectorAll("form")) {
27907
+ if (form.querySelectorAll("input[name=action]").some((input3) => input3.getAttribute("value") === action)) return form;
27908
+ }
27909
+ return null;
27910
+ }
27911
+ function formFields(form) {
27912
+ const fields2 = [];
27913
+ for (const element of form.querySelectorAll("input, textarea, select")) {
27914
+ const name = element.getAttribute("name");
27915
+ if (!name) continue;
27916
+ const tag = element.tagName.toLowerCase();
27917
+ if (tag === "textarea") {
27918
+ fields2.push([name, element.textContent]);
27919
+ continue;
27920
+ }
27921
+ if (tag === "select") {
27922
+ const options = element.querySelectorAll("option");
27923
+ const chosen = options.find((option) => option.hasAttribute("selected")) ?? options[0];
27924
+ if (chosen) fields2.push([name, chosen.getAttribute("value") ?? cleanText(chosen.textContent)]);
27925
+ continue;
27926
+ }
27927
+ const type = (element.getAttribute("type") ?? "text").toLowerCase();
27928
+ if (["submit", "button", "image", "file", "reset"].includes(type)) continue;
27929
+ if ((type === "checkbox" || type === "radio") && !element.hasAttribute("checked")) continue;
27930
+ fields2.push([name, element.getAttribute("value") ?? (type === "checkbox" ? "on" : "")]);
27931
+ }
27932
+ return fields2;
27933
+ }
27934
+ function statementOf(form) {
27935
+ const box = form.querySelector("input[name=submissionstatement]");
27936
+ if (!box) return {};
27937
+ const id2 = box.getAttribute("id");
27938
+ const label = (id2 ? form.querySelector(`label[for="${id2}"]`) : null) ?? box.closest("label") ?? box.parentNode?.querySelector("label") ?? null;
27939
+ const text = cleanText(label?.textContent).replace(/\s*Required\s*$/u, "").trim();
27940
+ return { statement: text || "Submission statement" };
27941
+ }
27942
+ function filemanagerOptions(html, itemid) {
27943
+ const pattern = /M\.form_filemanager\.init\(\s*Y\s*,\s*/gu;
27944
+ let match;
27945
+ while (match = pattern.exec(html)) {
27946
+ const json2 = balancedObject(html, match.index + match[0].length);
27947
+ if (!json2) continue;
27948
+ try {
27949
+ const options = JSON.parse(json2);
27950
+ if (isRecord5(options) && String(options.itemid) === itemid) return options;
27951
+ } catch {
27952
+ }
27953
+ }
27954
+ return null;
27955
+ }
27956
+ function balancedObject(text, start) {
27957
+ if (text[start] !== "{") return null;
27958
+ let depth = 0;
27959
+ let quoted = false;
27960
+ for (let index = start; index < text.length; index += 1) {
27961
+ const char = text[index];
27962
+ if (quoted) {
27963
+ if (char === "\\") index += 1;
27964
+ else if (char === '"') quoted = false;
27965
+ } else if (char === '"') quoted = true;
27966
+ else if (char === "{") depth += 1;
27967
+ else if (char === "}") {
27968
+ depth -= 1;
27969
+ if (depth === 0) return text.slice(start, index + 1);
27970
+ }
27971
+ }
27972
+ return null;
27973
+ }
27974
+ function tableCell(root, label) {
27975
+ for (const row of root.querySelectorAll("tr")) {
27976
+ const cells = row.querySelectorAll("th, td");
27977
+ if (cells.length > 1 && cleanText(cells[0].textContent) === label) return cells[1];
27978
+ }
27979
+ return null;
27980
+ }
27981
+ async function pageText(deps, url2) {
27982
+ return (await deps.request(url2)).text();
27983
+ }
27984
+ async function postForm(deps, action, fields2) {
27985
+ const response = await deps.request(action, {
27986
+ method: "POST",
27987
+ headers: { "content-type": "application/x-www-form-urlencoded" },
27988
+ body: new URLSearchParams(fields2).toString()
27989
+ });
27990
+ const html = await response.text();
27991
+ return landedOnView(response.url) ? null : html;
27992
+ }
27993
+ function landedOnView(url2) {
27994
+ try {
27995
+ const parsed = new URL(url2);
27996
+ return parsed.pathname.endsWith(ASSIGN_VIEW_PATH) && (parsed.searchParams.get("action") ?? "view") === "view";
27997
+ } catch {
27998
+ return false;
27999
+ }
28000
+ }
28001
+ async function draftAjax(deps, form, action, params) {
28002
+ const body = new URLSearchParams({ sesskey: form.sesskey, client_id: form.clientId, itemid: form.itemid, ...params });
28003
+ const response = await deps.request(`${deps.baseUrl}/repository/draftfiles_ajax.php?action=${action}`, {
28004
+ method: "POST",
28005
+ headers: { "content-type": "application/x-www-form-urlencoded" },
28006
+ body: body.toString()
28007
+ }, { allowErrorStatus: true });
28008
+ return jsonOf(deps, response, `draft file ${action}`);
28009
+ }
28010
+ async function listDraftFiles(deps, form) {
28011
+ const data = record2(await draftAjax(deps, form, "list", { filepath: "/" }));
28012
+ const list2 = Array.isArray(data.list) ? data.list.map(record2) : [];
28013
+ return list2.filter((item) => item.type !== "folder").map((item) => ({ name: String(item.filename ?? item.fullname ?? ""), path: String(item.filepath ?? "/"), bytes: integer2(item.size) })).filter((item) => item.name);
28014
+ }
28015
+ async function deleteDraftFiles(deps, form, files) {
28016
+ const selected = JSON.stringify(files.map((file3) => ({ filename: file3.name, filepath: file3.path })));
28017
+ const result = await draftAjax(deps, form, "deleteselected", { selected });
28018
+ if (result === false) throw deps.fail("Moodle did not remove the existing submission files.");
28019
+ }
28020
+ async function uploadDraftFile(deps, form, file3) {
28021
+ const body = new FormData();
28022
+ body.set("sesskey", form.sesskey);
28023
+ body.set("client_id", form.clientId);
28024
+ body.set("repo_id", form.repoId);
28025
+ body.set("itemid", form.itemid);
28026
+ body.set("env", "filemanager");
28027
+ body.set("ctx_id", form.contextId);
28028
+ body.set("title", file3.name);
28029
+ body.set("author", form.author);
28030
+ body.set("license", form.license);
28031
+ body.set("savepath", "/");
28032
+ body.set("maxbytes", String(form.maxBytes));
28033
+ body.set("areamaxbytes", String(form.areaMaxBytes));
28034
+ for (const type of form.acceptedTypes === "*" ? ["*"] : form.acceptedTypes) body.append("accepted_types[]", type);
28035
+ body.set("overwrite", "1");
28036
+ body.set("repo_upload_file", new Blob([Uint8Array.from(file3.bytes)]), file3.name);
28037
+ const response = await deps.request(`${deps.baseUrl}/repository/repository_ajax.php?action=upload`, { method: "POST", body }, { allowErrorStatus: true });
28038
+ const data = record2(await jsonOf(deps, response, `upload of ${file3.name}`));
28039
+ if (typeof data.error === "string" && data.error) throw deps.fail(`Moodle refused ${file3.name}: ${data.error}`, typeof data.errorcode === "string" ? data.errorcode : void 0);
28040
+ if (data.event === "fileexists") throw deps.fail(`Moodle reports ${file3.name} already exists and did not overwrite it.`);
28041
+ const stored = typeof data.file === "string" && data.file ? data.file : file3.name;
28042
+ if (!data.url && !data.id && !data.file) throw deps.fail(`Moodle did not confirm the upload of ${file3.name}.`);
28043
+ return stored;
28044
+ }
28045
+ async function jsonOf(deps, response, step) {
28046
+ const text = await response.text();
28047
+ try {
28048
+ return JSON.parse(text);
28049
+ } catch {
28050
+ const notice = noticesOf(text);
28051
+ throw deps.fail(`Moodle did not answer the ${step} with JSON (HTTP ${response.status})${notice ? `: ${notice}` : ""}`);
28052
+ }
28053
+ }
28054
+ function checkLimits(deps, form, kept, files) {
28055
+ if (form.maxFiles > 0 && kept.length + files.length > form.maxFiles) {
28056
+ throw deps.usage(`This assignment allows ${form.maxFiles} file${form.maxFiles === 1 ? "" : "s"}; the submission would hold ${kept.length + files.length}.`, kept.length ? "Use --replace to drop the existing files first." : void 0);
28057
+ }
28058
+ for (const file3 of files) {
28059
+ if (form.maxBytes > 0 && file3.bytes.byteLength > form.maxBytes) throw deps.usage(`${file3.name} is ${size(file3.bytes.byteLength)}; the limit is ${size(form.maxBytes)}.`);
28060
+ if (form.acceptedTypes !== "*" && form.acceptedTypes.every((type) => type.startsWith(".")) && !form.acceptedTypes.some((type) => file3.name.toLowerCase().endsWith(type.toLowerCase()))) {
28061
+ throw deps.usage(`${file3.name} is not an accepted type; allowed: ${form.acceptedTypes.join(", ")}.`);
28062
+ }
28063
+ }
28064
+ const total = kept.reduce((sum, file3) => sum + file3.bytes, 0) + files.reduce((sum, file3) => sum + file3.bytes.byteLength, 0);
28065
+ if (form.areaMaxBytes > 0 && total > form.areaMaxBytes) throw deps.usage(`The submission would total ${size(total)}; the limit is ${size(form.areaMaxBytes)}.`, kept.length ? "Use --replace to drop the existing files first." : void 0);
28066
+ }
28067
+ function describeLimits(form) {
28068
+ return {
28069
+ ...form.maxBytes > 0 ? { max_bytes: form.maxBytes } : {},
28070
+ ...form.maxFiles > 0 ? { max_files: form.maxFiles } : {},
28071
+ ...form.areaMaxBytes > 0 ? { area_max_bytes: form.areaMaxBytes } : {},
28072
+ ...form.acceptedTypes !== "*" ? { accepted_types: form.acceptedTypes } : {}
28073
+ };
28074
+ }
28075
+ function isSubmitted(status) {
28076
+ return /\bsubmitted\b/iu.test(status) && !/\bnot submitted\b/iu.test(status);
28077
+ }
28078
+ function size(bytes) {
28079
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
28080
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
28081
+ return `${bytes} B`;
28082
+ }
28083
+ function timestamp(deps) {
28084
+ return (deps.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
28085
+ }
28086
+ function integer2(value) {
28087
+ const number4 = Number(value);
28088
+ return Number.isSafeInteger(number4) ? number4 : 0;
28089
+ }
28090
+ function isRecord5(value) {
28091
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28092
+ }
28093
+ function record2(value) {
28094
+ return isRecord5(value) ? value : {};
28095
+ }
28096
+
28097
+ // src/parsers.ts
28098
+ function schema(parser) {
28099
+ return { parse: parser };
28100
+ }
28101
+ var UserInfoSchema = schema(parseUserInfo);
28102
+ var CourseSchema = schema(parseCourse);
28103
+ var CoursesSchema = schema(parseCourses);
28104
+ var ActivitySchema = schema(parseActivity);
28105
+ var SectionSchema = schema(parseSection);
28106
+ var CourseContentsSchema = schema(parseCourseContents);
28107
+ var TodoItemSchema = schema(parseTodoItem);
28108
+ function parseUserInfo(value) {
28109
+ const data = asRecord(value);
28110
+ return {
28111
+ userid: numberValue2(data.userid),
28112
+ username: stringValue3(data.username),
28113
+ fullname: stringValue3(data.fullname),
28114
+ sitename: stringValue3(data.sitename),
28115
+ siteurl: stringValue3(data.siteurl),
28116
+ lang: stringValue3(data.lang),
28117
+ ...data.timezone ? { timezone: String(data.timezone) } : {}
28118
+ };
28119
+ }
28120
+ function parseCourse(value, nowSeconds = Math.floor(Date.now() / 1e3)) {
28121
+ const data = asRecord(value);
28122
+ const course = {
28123
+ id: numberValue2(data.id),
28124
+ shortname: stringValue3(data.shortname),
28125
+ fullname: stringValue3(data.fullname),
28126
+ category: numberValue2(data.category),
28127
+ visible: booleanValue(data.visible, true),
28128
+ startdate: numberValue2(data.startdate)
28129
+ };
28130
+ const enddate = numberValue2(data.enddate);
28131
+ if (enddate > 0) {
28132
+ course.enddate = enddate;
28133
+ }
28134
+ return course;
28135
+ }
28136
+ function parseCourses(value) {
28137
+ return asArray(value).map((item) => parseCourse(item));
28138
+ }
28139
+ function parseActivity(value) {
28140
+ const data = asRecord(value);
28141
+ return {
28142
+ id: numberValue2(data.id),
28143
+ name: stringValue3(data.name),
28144
+ modname: stringValue3(data.modname),
28145
+ url: stringValue3(data.url),
28146
+ visible: booleanValue(data.visible, true),
28147
+ description: stringValue3(data.description),
28148
+ ...data.completiondata && typeof data.completiondata === "object" ? { completion: numberValue2(asRecord(data.completiondata).state) } : {},
28149
+ ...Array.isArray(data.contents) ? { file_entries: data.contents.filter((f) => asRecord(f).fileurl).map((f) => ({ name: stringValue3(asRecord(f).filename), url: stringValue3(asRecord(f).fileurl), requires_authentication: true })) } : {}
28150
+ };
28151
+ }
28152
+ function parseSection(value) {
28153
+ const data = asRecord(value);
28154
+ return {
28155
+ id: numberValue2(data.id),
28156
+ name: stringValue3(data.name),
28157
+ section: numberValue2(data.section),
28158
+ visible: booleanValue(data.visible, true),
28159
+ summary: stringValue3(data.summary),
28160
+ ...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
28161
+ activities: asArray(data.modules).map((item) => parseActivity(item))
28162
+ };
28163
+ }
28164
+ function parseCourseContents(value) {
28165
+ return asArray(value).map((item) => parseSection(item));
28166
+ }
28167
+ function parseCourseFormatState(value, baseUrl) {
28168
+ const state = asRecord(parseJsonValue(value));
28169
+ const activities = /* @__PURE__ */ new Map();
28170
+ const activitiesBySection = /* @__PURE__ */ new Map();
28171
+ for (const item of asArray(state.cm)) {
28172
+ const data = asRecord(item);
28173
+ const id2 = numberValue2(data.id);
28174
+ const sectionId = stringValue3(data.sectionid);
28175
+ const module = stringValue3(data.module) || stringValue3(data.plugin).replace(/^mod_/u, "") || stringValue3(data.modname).toLowerCase();
28176
+ const activity = {
28177
+ id: id2,
28178
+ name: htmlText(data.name, baseUrl),
28179
+ modname: module.toLowerCase(),
28180
+ url: stringValue3(data.url) ? resolveUrl(baseUrl, stringValue3(data.url)) : "",
28181
+ visible: booleanValue(data.visible, true) && booleanValue(data.uservisible, true) && !booleanValue(data.stealth),
28182
+ description: htmlText(data.content ?? data.description, baseUrl),
28183
+ ...data.completionstate !== void 0 && data.completionstate !== null ? { completion: numberValue2(data.completionstate) } : {}
28184
+ };
28185
+ activities.set(String(id2), activity);
28186
+ const sectionActivities = activitiesBySection.get(sectionId) ?? [];
28187
+ sectionActivities.push(activity);
28188
+ activitiesBySection.set(sectionId, sectionActivities);
28189
+ }
28190
+ return asArray(state.section).map((item) => {
28191
+ const data = asRecord(item);
28192
+ const id2 = numberValue2(data.id);
28193
+ const hasActivityList = Array.isArray(data.cmlist);
28194
+ const listedActivities = asArray(data.cmlist).map((activityId) => activities.get(stringValue3(activityId))).filter((activity) => activity !== void 0);
28195
+ return {
28196
+ id: id2,
28197
+ name: htmlText(data.title || data.rawtitle, baseUrl),
28198
+ section: numberValue2(data.section ?? data.number),
28199
+ visible: booleanValue(data.visible, true),
28200
+ summary: htmlText(data.summary, baseUrl),
28201
+ ...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
28202
+ activities: hasActivityList ? listedActivities : activitiesBySection.get(String(id2)) ?? []
28203
+ };
28204
+ });
28205
+ }
28206
+ function parseTodoItem(value) {
28207
+ const data = asRecord(value);
28208
+ const course = asRecord(data.course);
28209
+ const action = asRecord(data.action);
28210
+ const progress = course.progress;
28211
+ return {
28212
+ id: numberValue2(data.id),
28213
+ name: stringValue3(data.name),
28214
+ activity_name: stringValue3(data.activityname),
28215
+ modname: stringValue3(data.modulename),
28216
+ course_id: numberValue2(course.id),
28217
+ course_name: stringValue3(course.fullname),
28218
+ due_at: numberValue2(data.timesort) || numberValue2(data.timestart),
28219
+ overdue: booleanValue(data.overdue),
28220
+ actionable: booleanValue(action.actionable),
28221
+ action_name: stringValue3(action.name),
28222
+ action_url: stringValue3(action.url),
28223
+ url: stringValue3(data.url),
28224
+ event_type: stringValue3(data.eventtype),
28225
+ course_progress: typeof progress === "number" ? progress : void 0
28226
+ };
28227
+ }
28228
+ function parseTodoItems(value) {
28229
+ return asArray(value).map((item) => parseTodoItem(item));
28230
+ }
28231
+ function parseAlertNotification(value) {
28232
+ const data = asRecord(value);
28233
+ return {
28234
+ id: numberValue2(data.id),
28235
+ subject: stringValue3(data.subject),
28236
+ short_subject: stringValue3(data.shortenedsubject),
28237
+ event_type: stringValue3(data.eventtype),
28238
+ component: stringValue3(data.component),
28239
+ created_at: numberValue2(data.timecreated),
28240
+ created_pretty: stringValue3(data.timecreatedpretty),
28241
+ read: booleanValue(data.read),
28242
+ context_url: stringValue3(data.contexturl),
28243
+ context_name: stringValue3(data.contexturlname)
28244
+ };
28245
+ }
28246
+ function parseAlertSummary(notificationsData, countsData, unreadCountsData) {
28247
+ const notificationsRecord = asRecord(notificationsData);
28248
+ const counts2 = asRecord(countsData);
28249
+ const unreadCounts = asRecord(unreadCountsData);
28250
+ const types = asRecord(counts2.types);
28251
+ const unreadTypes = asRecord(unreadCounts.types);
28252
+ const notifications = asArray(notificationsRecord.notifications).map((item) => parseAlertNotification(item));
28253
+ return {
28254
+ notifications,
28255
+ notification_count: notifications.length,
28256
+ unread_notification_count: notifications.filter((notification) => !notification.read).length,
28257
+ starred_message_count: numberValue2(counts2.favourites),
28258
+ direct_message_count: numberValue2(types["1"]),
28259
+ group_message_count: numberValue2(types["2"]),
28260
+ self_message_count: numberValue2(types["3"]),
28261
+ unread_starred_message_count: numberValue2(unreadCounts.favourites),
28262
+ unread_direct_message_count: numberValue2(unreadTypes["1"]),
28263
+ unread_group_message_count: numberValue2(unreadTypes["2"]),
28264
+ unread_self_message_count: numberValue2(unreadTypes["3"])
28265
+ };
28266
+ }
28267
+ function parseForumPostAuthor(value) {
28268
+ const data = asRecord(value);
28269
+ const urls = asRecord(data.urls);
28270
+ return {
28271
+ id: numberValue2(data.id),
28272
+ fullname: stringValue3(data.fullname),
28273
+ profile_url: stringValue3(urls.profile),
28274
+ profile_image_url: stringValue3(urls.profileimage)
28275
+ };
28276
+ }
28277
+ function parseForumPost(value, baseUrl = "") {
28278
+ const data = asRecord(value);
28279
+ const urls = asRecord(data.urls);
28280
+ const messageHtml = stringValue3(data.message);
28281
+ const structured = htmlToStructuredContent(messageHtml, stringValue3(urls.view || urls.discuss) || baseUrl);
28282
+ return {
28283
+ id: numberValue2(data.id),
28284
+ discussion_id: numberValue2(data.discussionid),
28285
+ subject: stringValue3(data.subject),
28286
+ message_html: messageHtml,
28287
+ message_text: structured.text,
28288
+ image_urls: structured.image_urls,
28289
+ links: structured.links,
28290
+ tables: structured.tables,
28291
+ author: parseForumPostAuthor(data.author),
28292
+ parent_id: numberValue2(data.parentid),
28293
+ time_created: numberValue2(data.timecreated),
28294
+ time_modified: numberValue2(data.timemodified),
28295
+ created_pretty: "",
28296
+ unread: booleanValue(data.unread),
28297
+ is_deleted: booleanValue(data.isdeleted),
28298
+ is_private_reply: booleanValue(data.isprivatereply),
28299
+ url: stringValue3(urls.view || urls.viewisolated),
28300
+ reply_url: stringValue3(urls.reply)
28301
+ };
28302
+ }
28303
+ function parseForumDiscussion(value, discussionId, baseUrl = "") {
28304
+ const data = asRecord(value);
28305
+ const posts = asArray(data.posts).map((item) => parseForumPost(item, baseUrl));
28306
+ return {
28307
+ id: discussionId,
28308
+ subject: posts[0]?.subject ?? "",
28309
+ course_id: numberValue2(data.courseid),
28310
+ forum_id: numberValue2(data.forumid),
28311
+ group_id: numberValue2(data.groupid),
28312
+ group_name: stringValue3(data.groupname),
28313
+ url: posts[0]?.url ? posts[0].url.split("#", 1)[0] : "",
28314
+ posts
28315
+ };
28316
+ }
28317
+ function asRecord(value) {
28318
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
28319
+ }
28320
+ function asArray(value) {
28321
+ return Array.isArray(value) ? value : [];
28322
+ }
28323
+ function stringValue3(value) {
28324
+ return typeof value === "string" ? value : value == null ? "" : String(value);
28325
+ }
28326
+ function numberValue2(value) {
28327
+ if (typeof value === "number" && Number.isFinite(value)) {
28328
+ return value;
28329
+ }
28330
+ if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
28331
+ return Number(value);
28332
+ }
28333
+ return 0;
28334
+ }
28335
+ function booleanValue(value, defaultValue = false) {
28336
+ if (value === void 0 || value === null) {
28337
+ return defaultValue;
28338
+ }
28339
+ return Boolean(value);
28340
+ }
28341
+ function parseJsonValue(value) {
28342
+ if (typeof value !== "string") return value;
28343
+ try {
28344
+ return JSON.parse(value);
28345
+ } catch {
28346
+ return {};
28347
+ }
28348
+ }
28349
+ function htmlText(value, baseUrl) {
28350
+ return htmlToStructuredContent(stringValue3(value), baseUrl).text;
28351
+ }
28352
+
27993
28353
  // src/moodle-forum-core.ts
27994
28354
  var ForumModule = class {
27995
28355
  baseUrl;
@@ -28124,13 +28484,13 @@ var ForumModule = class {
28124
28484
  }
28125
28485
  };
28126
28486
  function shouldFallbackForumAjax(error62) {
28127
- if (!isRecord5(error62)) {
28487
+ if (!isRecord6(error62)) {
28128
28488
  return false;
28129
28489
  }
28130
28490
  const code = typeof error62.moodleErrorCode === "string" ? error62.moodleErrorCode : "";
28131
28491
  return code === "servicenotavailable" || code === "accessexception" || error62 instanceof Error && error62.message.includes("Web service is not available");
28132
28492
  }
28133
- function isRecord5(value) {
28493
+ function isRecord6(value) {
28134
28494
  return typeof value === "object" && value !== null && !Array.isArray(value);
28135
28495
  }
28136
28496
 
@@ -28262,9 +28622,9 @@ async function searchForumContent(source, query, options = {}) {
28262
28622
  hits.sort(sortBy === "recent" ? sortRecent : sortRelevant);
28263
28623
  return hits.slice(0, options.limit ?? 20).map(([, hit]) => includePostText ? hit : { ...hit, snippet: "" });
28264
28624
  }
28265
- async function inParallel(items, size, run) {
28625
+ async function inParallel(items, size2, run) {
28266
28626
  const results = [];
28267
- for (let index = 0; index < items.length; index += size) results.push(...await Promise.all(items.slice(index, index + size).map(run)));
28627
+ for (let index = 0; index < items.length; index += size2) results.push(...await Promise.all(items.slice(index, index + size2).map(run)));
28268
28628
  return results;
28269
28629
  }
28270
28630
  function normalizeQuery(value) {
@@ -28450,7 +28810,7 @@ var MoodleClientCore = class {
28450
28810
  try {
28451
28811
  if (this.unavailable.has(FUNC_GET_SITE_INFO) && this.userInfo?.fullname) return this.userInfo;
28452
28812
  const data = await this.call(FUNC_GET_SITE_INFO);
28453
- if (isRecord6(data) && "userid" in data) {
28813
+ if (isRecord7(data) && "userid" in data) {
28454
28814
  const info = parseUserInfo(data);
28455
28815
  this.sesskey = typeof data.sesskey === "string" ? data.sesskey : this.sesskey;
28456
28816
  this.userid = info.userid;
@@ -28549,9 +28909,9 @@ var MoodleClientCore = class {
28549
28909
  let type = "";
28550
28910
  try {
28551
28911
  const data = await this.call(FUNC_GET_COURSE_MODULE, { cmid: id2 });
28552
- const module = isRecord6(data) && isRecord6(data.cm) ? data.cm : data;
28553
- type = isRecord6(module) && typeof module.modname === "string" ? module.modname : "";
28554
- courseId = isRecord6(module) && typeof module.course === "number" ? module.course : void 0;
28912
+ const module = isRecord7(data) && isRecord7(data.cm) ? data.cm : data;
28913
+ type = isRecord7(module) && typeof module.modname === "string" ? module.modname : "";
28914
+ courseId = isRecord7(module) && typeof module.course === "number" ? module.course : void 0;
28555
28915
  } catch (error62) {
28556
28916
  if (!this.errors.isApi(error62) || error62.moodleErrorCode !== "servicenotavailable") {
28557
28917
  throw error62;
@@ -28595,14 +28955,14 @@ var MoodleClientCore = class {
28595
28955
  } else {
28596
28956
  data = await this.call(FUNC_GET_ACTION_EVENTS, { ...window, limittononsuspendedevents: true });
28597
28957
  }
28598
- const events = isRecord6(data) && Array.isArray(data.events) ? data.events : [];
28958
+ const events = isRecord7(data) && Array.isArray(data.events) ? data.events : [];
28599
28959
  for (const item of parseTodoItems(events)) if (!seen.has(item.id)) {
28600
28960
  seen.add(item.id);
28601
28961
  items.push(item);
28602
28962
  }
28603
28963
  if (events.length < batchSize) break;
28604
28964
  const last = events.at(-1);
28605
- const next = isRecord6(last) && typeof last.id === "number" ? last.id : void 0;
28965
+ const next = isRecord7(last) && typeof last.id === "number" ? last.id : void 0;
28606
28966
  if (!next || next === aftereventid) throw this.errors.api("Moodle repeated a calendar page; refine the date window.");
28607
28967
  aftereventid = next;
28608
28968
  }
@@ -28769,8 +29129,18 @@ var MoodleClientCore = class {
28769
29129
  async getFolder(id2) {
28770
29130
  return parseFolderHtml(await this.get(FOLDER_VIEW_PATH, { id: id2 }), id2, this.baseUrl);
28771
29131
  }
28772
- async requestAbsolute(url2, init = {}) {
28773
- return this.requestAbsoluteInternal(url2, init, true);
29132
+ async requestAbsolute(url2, init = {}, options = {}) {
29133
+ return this.requestAbsoluteInternal(url2, init, true, Boolean(options.allowErrorStatus));
29134
+ }
29135
+ /** Uploads files into an assignment and reads the receipt back from the site. */
29136
+ async submitAssignment(request) {
29137
+ await this.ensureSession();
29138
+ return submitAssignmentFiles({
29139
+ baseUrl: this.baseUrl,
29140
+ request: (url2, init, options) => this.requestAbsolute(url2, init, options),
29141
+ fail: (message, moodleErrorCode) => this.errors.api(message, moodleErrorCode),
29142
+ usage: (message, hint) => this.errors.usage ? this.errors.usage(message, hint) : new MoodleClientCoreError("usage", message, hint)
29143
+ }, request);
28774
29144
  }
28775
29145
  async getNewsForums(courseId) {
28776
29146
  const units = courseId === void 0 ? await this.getCourses() : (await this.getCourses()).filter((c) => c.id === courseId);
@@ -28780,7 +29150,7 @@ var MoodleClientCore = class {
28780
29150
  await this.ensureSession();
28781
29151
  const data = await this.call("mod_forum_get_forums_by_courses", { courseids: units.map((c) => c.id) });
28782
29152
  for (const f of Array.isArray(data) ? data : []) {
28783
- if (!isRecord6(f) || f.type !== "news" || typeof f.cmid !== "number") continue;
29153
+ if (!isRecord7(f) || f.type !== "news" || typeof f.cmid !== "number") continue;
28784
29154
  const c = units.find((c2) => c2.id === f.course);
28785
29155
  forums.push({ id: f.cmid, name: String(f.name || ""), course_id: Number(f.course), course_name: c?.fullname || "", url: `${this.baseUrl}/mod/forum/view.php?id=${f.cmid}` });
28786
29156
  }
@@ -28908,16 +29278,16 @@ var MoodleClientCore = class {
28908
29278
  async getAbsolute(url2) {
28909
29279
  return (await this.requestAbsolute(url2)).text();
28910
29280
  }
28911
- async requestAbsoluteInternal(url2, init, allowRetry) {
29281
+ async requestAbsoluteInternal(url2, init, allowRetry, allowErrorStatus = false) {
28912
29282
  const response = await fetchWithSession(url2, init, this.baseUrl, this.cookie, this.fetchImpl);
28913
29283
  if (response.url.includes("/login/")) {
28914
29284
  if (this.onLoginRequired && allowRetry && !this.retryingLogin) {
28915
29285
  await this.reauthenticate();
28916
- return this.requestAbsoluteInternal(url2, init, false);
29286
+ return this.requestAbsoluteInternal(url2, init, false, allowErrorStatus);
28917
29287
  }
28918
29288
  throw this.errors.api("Session expired", "servicerequireslogin");
28919
29289
  }
28920
- if (!response.ok) {
29290
+ if (!response.ok && !allowErrorStatus) {
28921
29291
  const context = `HTTP ${response.status} loading ${safeUrl(url2)}`;
28922
29292
  const contentType2 = response.headers.get("content-type")?.toLowerCase() ?? "";
28923
29293
  if (contentType2.includes("text/html") || contentType2.includes("application/xhtml+xml")) {
@@ -28935,7 +29305,7 @@ var MoodleClientCore = class {
28935
29305
  let offset = 0;
28936
29306
  while (true) {
28937
29307
  const data = await this.call(FUNC_GET_COURSES_BY_TIMELINE, { classification: "all", limit: 100, offset });
28938
- if (!isRecord6(data) || !Array.isArray(data.courses) || !data.courses.length) {
29308
+ if (!isRecord7(data) || !Array.isArray(data.courses) || !data.courses.length) {
28939
29309
  break;
28940
29310
  }
28941
29311
  courses.push(...data.courses);
@@ -29098,19 +29468,19 @@ function placeholderUserInfo(baseUrl, userid) {
29098
29468
  };
29099
29469
  }
29100
29470
  function parseTodoPayload(value) {
29101
- return parseTodoItems(isRecord6(value) && Array.isArray(value.events) ? value.events : []);
29471
+ return parseTodoItems(isRecord7(value) && Array.isArray(value.events) ? value.events : []);
29102
29472
  }
29103
29473
  function errorMessage(value) {
29104
29474
  return value instanceof Error ? value.message : "Unknown Moodle error";
29105
29475
  }
29106
- function chunks(values, size) {
29476
+ function chunks(values, size2) {
29107
29477
  const result = [];
29108
- for (let index = 0; index < values.length; index += size) {
29109
- result.push(values.slice(index, index + size));
29478
+ for (let index = 0; index < values.length; index += size2) {
29479
+ result.push(values.slice(index, index + size2));
29110
29480
  }
29111
29481
  return result;
29112
29482
  }
29113
- function isRecord6(value) {
29483
+ function isRecord7(value) {
29114
29484
  return !!value && typeof value === "object" && !Array.isArray(value);
29115
29485
  }
29116
29486
  function isLoginErrorCode(code) {
@@ -29133,7 +29503,7 @@ function safeUrl(value) {
29133
29503
  }
29134
29504
 
29135
29505
  // src/mcp/gateway.ts
29136
- var import_node_html_parser3 = __toESM(require_dist(), 1);
29506
+ var import_node_html_parser4 = __toESM(require_dist(), 1);
29137
29507
  var MAX_MCP_FILE_BYTES = 16 * 1024 * 1024;
29138
29508
  var MoodleGatewayError = class extends Error {
29139
29509
  code;
@@ -29143,13 +29513,14 @@ var MoodleGatewayError = class extends Error {
29143
29513
  this.code = code;
29144
29514
  }
29145
29515
  };
29146
- function createMoodleGateway(client) {
29516
+ function createMoodleGateway(client, hooks = {}) {
29147
29517
  return {
29148
29518
  getUser: () => client.getSiteInfo(),
29149
29519
  listThreads: (id2) => client.getForumDiscussionRefs ? client.getForumDiscussionRefs(id2) : Promise.resolve([]),
29150
29520
  listNewsForums: (id2) => client.getNewsForums ? client.getNewsForums(id2) : Promise.resolve([]),
29151
29521
  getOverview: (input3) => client.getOverview(input3.todoLimit, input3.todoDays, input3.alertsLimit),
29152
29522
  ...client.getTodo ? { getDue: (days, courseId) => client.getTodo(Number.MAX_SAFE_INTEGER, days, courseId) } : {},
29523
+ ...client.submitAssignmentFiles ? { submitAssignment: (input3) => client.submitAssignmentFiles({ ...input3, ...hooks.onSubmitProgress ? { onProgress: hooks.onSubmitProgress } : {} }) } : {},
29153
29524
  listCourses: () => client.getCourses(),
29154
29525
  async getCourse({ courseId }) {
29155
29526
  const [courses, sections] = await Promise.all([
@@ -29273,7 +29644,7 @@ async function fileFromActivity(client, activityId) {
29273
29644
  return { url: entry.url, name: entry.name };
29274
29645
  }
29275
29646
  function resourceLinks(html, baseUrl) {
29276
- const root = (0, import_node_html_parser3.parse)(html);
29647
+ const root = (0, import_node_html_parser4.parse)(html);
29277
29648
  const entries = root.querySelectorAll(".resourceworkaround a[href], .resourcecontent a[href], a.resourceworkaround[href]").map((link) => ({
29278
29649
  name: link.textContent.trim(),
29279
29650
  url: new URL(link.getAttribute("href") ?? "", baseUrl).toString()
@@ -29327,7 +29698,7 @@ function isHtml(response) {
29327
29698
  return type.includes("text/html") || type.includes("application/xhtml+xml");
29328
29699
  }
29329
29700
  function looksLikeLoginPage(html) {
29330
- const root = (0, import_node_html_parser3.parse)(html);
29701
+ const root = (0, import_node_html_parser4.parse)(html);
29331
29702
  return root.querySelector('form[action*="/login/"], input[name="password"], #page-login-index') !== null || /<title>\s*(?:log in|login)/iu.test(html);
29332
29703
  }
29333
29704
  function contentType(response) {
@@ -29400,6 +29771,8 @@ var counts = external_exports.object(Object.fromEntries(["notification_count", "
29400
29771
  var postSchema = external_exports.object({ id, parent_id: n, name: s, author: external_exports.object({ id, name: s }), time_created: id, created: s, message_text: s, links: external_exports.array(external_exports.object({ text: s, url: s })).optional() });
29401
29772
  var gradeSchema = external_exports.object({ unit_id: id, code: s, graded: id, total: id, total_grade: s, total_range: s, total_percentage: s, items: external_exports.array(external_exports.object({ name: s, type: s, grade: s, range: s, percentage: s, weight: s, contribution: s, feedback: s, status: s, due: s, due_at: n })).optional() });
29402
29773
  var searchSchema = external_exports.object({ unit_id: id, forum_id: id, discussion_id: id, name: s, post_id: id, snippet: s, time_created: n, created: s, url: s });
29774
+ var submissionFile = external_exports.object({ name: external_exports.string(), bytes: n, url: s });
29775
+ var receiptSchema = external_exports.object({ id, name: s, unit_id: n, url: s, action: external_exports.enum(["planned", "saved", "submitted"]), submission_status: s, grading_status: s, due: s, time_remaining: s, last_modified: s, statement: s, statement_accepted: external_exports.boolean().optional(), files: external_exports.array(submissionFile).optional(), uploads: external_exports.array(external_exports.object({ name: external_exports.string(), bytes: id, path: s })).optional(), removed: external_exports.array(external_exports.string()).optional(), limits: external_exports.object({ max_bytes: n, max_files: n, area_max_bytes: n, accepted_types: external_exports.array(external_exports.string()).optional() }).optional(), checked_at: external_exports.string() });
29403
29776
  var input2 = (shape) => external_exports.object(shape).strict();
29404
29777
  var list = (key, value) => external_exports.object({ [key]: external_exports.array(value).optional(), total: id });
29405
29778
  var intentContracts = {
@@ -29413,6 +29786,7 @@ var intentContracts = {
29413
29786
  news: { when: "announcements", command: "moodle news [UNIT]", what: "Latest announcement threads with first-post text.", instead: "search_forums for other discussions", refs: "optional unit code, name, id or URL", then: "thread with discussion id", cost: "up to 5 announcements by default", input: input2({ unit: ref.optional(), limit: limit.default(5) }), output: list("news", external_exports.object({ id, name: s, unit_id: id, unit_code: s, forum_id: id, post: postSchema.optional() })) },
29414
29787
  thread: { when: "discussion posts", command: "moodle threads show ID", what: "A discussion and a page of compact posts, with attachment links.", instead: "news for announcements", refs: "discussion_id; offset and limit", then: "increase offset while posts_total exceeds returned", cost: "up to 20 posts by default", input: input2({ discussion_id: external_exports.number().int().positive(), limit, offset: external_exports.number().int().nonnegative().default(0) }), output: external_exports.object({ thread: external_exports.object({ id, name: s, unit_id: id, forum_id: id, url: s, posts: external_exports.array(postSchema).optional(), posts_total: id, offset: id }) }) },
29415
29788
  search_forums: { when: "forum post text", command: 'moodle forums search "QUERY" --unit UNIT', what: "Matching posts with unit and forum name maps.", instead: "find for activity names", refs: "query, optional unit or courseId and forumId", then: "thread with discussion_id", cost: "bounded forum scan; total covers scanned scope", input: input2({ query: external_exports.string().trim().min(1), unit: ref.optional(), courseId: external_exports.number().int().positive().optional(), forumId: external_exports.number().int().positive().optional(), limit, includePostText: external_exports.boolean().default(true), titlesOnly: external_exports.boolean().default(false), unreadOnly: external_exports.boolean().default(false), sortBy: external_exports.enum(["relevance", "recent"]).default("relevance"), maxForums: limit.default(20), maxDiscussionsPerForum: limit.default(50) }), output: external_exports.object({ results: external_exports.array(searchSchema).optional(), total: id, forums: external_exports.record(external_exports.string(), external_exports.string()).optional(), units: external_exports.record(external_exports.string(), external_exports.string()).optional(), scope: external_exports.object({ max_forums: id, max_discussions_per_forum: id }) }) },
29789
+ submit: { when: "upload assignment files", command: 'moodle submit "UNIT TASK" FILE... [--final]', what: "Upload local files into an assignment; returns the receipt Moodle shows afterwards.", instead: "item for status only", refs: "assignment id, same-site URL or UNIT TASK phrase; local file paths", then: "dry_run (default) only plans; show the plan to the person, then rerun with dry_run false; final submits for grading and cannot be undone", cost: "writes to Moodle", input: input2({ ref, files: external_exports.array(external_exports.string().trim().min(1)).max(20).default([]), final: external_exports.boolean().default(false), replace: external_exports.boolean().default(false), accept_statement: external_exports.boolean().default(false), dry_run: external_exports.boolean().default(true) }), output: external_exports.object({ submission: receiptSchema }) },
29416
29790
  file: { when: "download a file", command: 'moodle get "UNIT TASK" --to DIR', what: "One authenticated file as embedded content, at most 16 MiB.", instead: "item for file choices", refs: "resource id, same-site URL, or UNIT TASK phrase", then: "read the returned resource", cost: "binary content up to 16 MiB", input: input2({ ref }), output: external_exports.object({ file: external_exports.object({ name: external_exports.string(), mime_type: external_exports.string(), bytes: id, uri: external_exports.string() }) }) }
29417
29791
  };
29418
29792
  function intentDescription(name) {
@@ -29472,9 +29846,9 @@ function postRow(p, subject, tz = "UTC") {
29472
29846
  }
29473
29847
 
29474
29848
  // src/intents.ts
29475
- async function inParallel2(items, size, run) {
29849
+ async function inParallel2(items, size2, run) {
29476
29850
  const results = [];
29477
- for (let index = 0; index < items.length; index += size) results.push(...await Promise.all(items.slice(index, index + size).map(run)));
29851
+ for (let index = 0; index < items.length; index += size2) results.push(...await Promise.all(items.slice(index, index + size2).map(run)));
29478
29852
  return results;
29479
29853
  }
29480
29854
  function createIntentService(gateway, now = () => Date.now()) {
@@ -29665,6 +30039,12 @@ function createIntentService(gateway, now = () => Date.now()) {
29665
30039
  result = { file: { name: file3.name, mime_type: file3.mimeType, bytes: file3.bytes, uri: file3.uri } };
29666
30040
  break;
29667
30041
  }
30042
+ case "submit": {
30043
+ if (!gateway.submitAssignment) throw new MoodleGatewayError("MOODLE_TOOL_UNAVAILABLE", "Submitting needs local files; run moodle submit or the local MCP server on the machine that holds them.");
30044
+ const activityId = await resolveItem(input3.ref);
30045
+ result = { submission: await gateway.submitAssignment({ activityId, files: input3.files, final: Boolean(input3.final), replace: Boolean(input3.replace), acceptStatement: Boolean(input3.accept_statement), dryRun: Boolean(input3.dry_run) }) };
30046
+ break;
30047
+ }
29668
30048
  }
29669
30049
  return intentContracts[name].output.parse(stripEmpty(result));
29670
30050
  }
@@ -29683,13 +30063,23 @@ var READ_ONLY_ANNOTATIONS = {
29683
30063
  idempotentHint: true,
29684
30064
  openWorldHint: true
29685
30065
  };
30066
+ var WRITE_ANNOTATIONS = {
30067
+ readOnlyHint: false,
30068
+ destructiveHint: true,
30069
+ idempotentHint: false,
30070
+ openWorldHint: true
30071
+ };
30072
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["submit"]);
29686
30073
  var aliases = { get_overview: "home", list_courses: "units", get_course: "unit", get_activity: "item", get_grades: "grades", get_thread: "thread", get_file: "file" };
29687
30074
  var TOOL_CATALOG = Object.entries(intentContracts).map(([name, contract]) => ({
29688
30075
  name,
29689
30076
  description: intentDescription(name),
29690
30077
  inputSchema: compactSchema(external_exports.toJSONSchema(contract.input, { io: "input" })),
29691
- annotations: READ_ONLY_ANNOTATIONS
30078
+ annotations: WRITE_TOOLS.has(name) ? WRITE_ANNOTATIONS : READ_ONLY_ANNOTATIONS
29692
30079
  }));
30080
+ function toolsFor(gateway) {
30081
+ return gateway.submitAssignment ? TOOL_CATALOG : TOOL_CATALOG.filter((tool) => !WRITE_TOOLS.has(tool.name));
30082
+ }
29693
30083
  var TOOL_OUTPUT_SCHEMAS = Object.fromEntries(Object.entries(intentContracts).map(([name, contract]) => [name, compactSchema(external_exports.toJSONSchema(contract.output))]));
29694
30084
  function compactSchema(value) {
29695
30085
  if (Array.isArray(value)) return value.map(compactSchema);
@@ -29727,7 +30117,7 @@ function createMoodleMcpServer(gateway, options = {}) {
29727
30117
  protocolVersion,
29728
30118
  capabilities: { tools: { listChanged: false } },
29729
30119
  serverInfo,
29730
- instructions: "Read-only access to the authenticated user's Moodle data."
30120
+ instructions: gateway.submitAssignment ? "Access to the authenticated user's Moodle data. Only submit writes; it defaults to a dry run." : "Read-only access to the authenticated user's Moodle data."
29731
30121
  });
29732
30122
  }
29733
30123
  if (request.method === "ping") {
@@ -29735,7 +30125,7 @@ function createMoodleMcpServer(gateway, options = {}) {
29735
30125
  }
29736
30126
  if (request.method === "tools/list") {
29737
30127
  return jsonRpcSuccess(id2, {
29738
- tools: TOOL_CATALOG,
30128
+ tools: toolsFor(gateway),
29739
30129
  resultType: "complete",
29740
30130
  _meta: RESULT_META
29741
30131
  });
@@ -29791,8 +30181,9 @@ async function callTool(gateway, params) {
29791
30181
  const requested = typeof params?.name === "string" ? params.name : "";
29792
30182
  const name = Object.hasOwn(aliases, requested) ? aliases[requested] : requested;
29793
30183
  if (!Object.hasOwn(intentContracts, name) && !["get_user", "list_activities", "list_forums"].includes(name)) throw new McpCallError("TOOL_NOT_FOUND", `Unknown Moodle tool: ${requested || "<missing>"}`);
30184
+ if (WRITE_TOOLS.has(name) && !gateway.submitAssignment) throw new McpCallError("TOOL_NOT_FOUND", `${requested} is only available on a local MCP server with access to the files.`);
29794
30185
  const raw = params?.arguments ?? {};
29795
- if (!isRecord7(raw)) throw new McpCallError("INVALID_TOOL_ARGUMENTS", "Tool arguments must be an object.");
30186
+ if (!isRecord8(raw)) throw new McpCallError("INVALID_TOOL_ARGUMENTS", "Tool arguments must be an object.");
29796
30187
  const args = { ...raw };
29797
30188
  if (Object.hasOwn(aliases, requested)) {
29798
30189
  const renames = { courseId: "unit", activityId: "ref", discussionId: "discussion_id", source: "ref", todoDays: "days", gradedOnly: "graded_only" };
@@ -29840,7 +30231,7 @@ async function callTool(gateway, params) {
29840
30231
  const mapped2 = { type: "MOODLE_RESULT_INVALID", message: `Moodle returned ${name} data in an unexpected shape.`, hint: "Retry once; if it persists, run the same command locally with --verbose and report the tool name.", issues: error62.issues.slice(0, 5).map((issue2) => ({ path: issue2.path.join("."), message: issue2.message })) };
29841
30232
  return { content: [{ type: "text", text: JSON.stringify({ error: mapped2 }) }], structuredContent: { error: mapped2 }, isError: true, resultType: "complete", _meta: RESULT_META };
29842
30233
  }
29843
- const mapped = error62 instanceof ReferenceError2 ? { type: error62.code, code: error62.code, message: error62.message, hint: error62.hint, candidates: error62.candidates } : mapMoodleError(error62);
30234
+ const mapped = error62 instanceof ReferenceError2 ? { type: error62.code, code: error62.code, message: error62.message, hint: error62.hint, candidates: error62.candidates } : mapMoodleError(error62, WRITE_TOOLS.has(name));
29844
30235
  return { content: [{ type: "text", text: JSON.stringify({ error: mapped }) }], structuredContent: { error: mapped }, isError: true, resultType: "complete", _meta: RESULT_META };
29845
30236
  }
29846
30237
  }
@@ -29859,9 +30250,11 @@ function toolContent(name, payload, structuredContent) {
29859
30250
  }
29860
30251
  ];
29861
30252
  }
29862
- function mapMoodleError(error62) {
29863
- const record2 = isRecord7(error62) ? error62 : {};
29864
- const code = typeof record2.code === "string" ? record2.code : "";
30253
+ function mapMoodleError(error62, verbatim = false) {
30254
+ const record3 = isRecord8(error62) ? error62 : {};
30255
+ const code = typeof record3.code === "string" ? record3.code : "";
30256
+ const own2 = verbatim && typeof record3.message === "string" && record3.message.trim() && code !== "auth" ? record3.message : void 0;
30257
+ const ownHint = verbatim && typeof record3.hint === "string" && record3.hint.trim() ? record3.hint : void 0;
29865
30258
  const typeByCode = {
29866
30259
  auth: "MOODLE_AUTH_REQUIRED",
29867
30260
  not_found: "MOODLE_NOT_FOUND",
@@ -29870,8 +30263,8 @@ function mapMoodleError(error62) {
29870
30263
  };
29871
30264
  const type = code.startsWith("MOODLE_") ? code : typeByCode[code] ?? "MOODLE_UPSTREAM_ERROR";
29872
30265
  const message = type === "MOODLE_AUTH_REQUIRED" ? "The Moodle session has expired. Sign in again." : type === "MOODLE_NOT_FOUND" || type === "MOODLE_COURSE_NOT_FOUND" ? "The requested Moodle item was not found." : type === "MOODLE_INVALID_REQUEST" ? "The Moodle request is invalid." : "Moodle could not complete the request.";
29873
- const moodleCode = typeof record2.moodleErrorCode === "string" && /^[a-z][a-z0-9_]{0,63}$/u.test(record2.moodleErrorCode) ? record2.moodleErrorCode : void 0;
29874
- return { type, message, hint: type === "MOODLE_AUTH_REQUIRED" ? "Run moodle mcp login for a remote server, or moodle auth login locally; then retry." : "Run moodle doctor, or refine the request using units and find.", ...type === "MOODLE_AUTH_REQUIRED" ? { recovery: { action: "moodle mcp login", where: "machine running moodle-cli", then: "retry this tool" } } : {}, ...moodleCode ? { moodleCode } : {} };
30266
+ const moodleCode = typeof record3.moodleErrorCode === "string" && /^[a-z][a-z0-9_]{0,63}$/u.test(record3.moodleErrorCode) ? record3.moodleErrorCode : void 0;
30267
+ return { type, message: own2 ?? message, hint: ownHint ?? (type === "MOODLE_AUTH_REQUIRED" ? "Run moodle mcp login for a remote server, or moodle auth login locally; then retry." : "Run moodle doctor, or refine the request using units and find."), ...type === "MOODLE_AUTH_REQUIRED" ? { recovery: { action: "moodle mcp login", where: "machine running moodle-cli", then: "retry this tool" } } : {}, ...moodleCode ? { moodleCode } : {} };
29875
30268
  }
29876
30269
  var McpCallError = class extends Error {
29877
30270
  type;
@@ -29883,11 +30276,11 @@ var McpCallError = class extends Error {
29883
30276
  this.details = details;
29884
30277
  }
29885
30278
  };
29886
- function isRecord7(value) {
30279
+ function isRecord8(value) {
29887
30280
  return typeof value === "object" && value !== null && !Array.isArray(value);
29888
30281
  }
29889
30282
  function isMoodleFile(value) {
29890
- return isRecord7(value) && typeof value.name === "string" && typeof value.mimeType === "string" && typeof value.bytes === "number" && typeof value.uri === "string" && typeof value.blob === "string";
30283
+ return isRecord8(value) && typeof value.name === "string" && typeof value.mimeType === "string" && typeof value.bytes === "number" && typeof value.uri === "string" && typeof value.blob === "string";
29891
30284
  }
29892
30285
 
29893
30286
  // src/worker/moodle-upstream.ts
@@ -29958,17 +30351,17 @@ var FetchMoodleSessionUpstream = class {
29958
30351
  return { alive: false, remainingSeconds: null };
29959
30352
  }
29960
30353
  if (!Array.isArray(body) || body.length === 0) return { alive: null, remainingSeconds: null };
29961
- const first2 = isRecord8(body[0]) ? body[0] : void 0;
30354
+ const first2 = isRecord9(body[0]) ? body[0] : void 0;
29962
30355
  if (first2?.error) {
29963
- const exception = isRecord8(first2.exception) ? first2.exception : void 0;
30356
+ const exception = isRecord9(first2.exception) ? first2.exception : void 0;
29964
30357
  const errorCode = typeof exception?.errorcode === "string" ? exception.errorcode : "";
29965
30358
  if (errorCode === "servicerequireslogin" || errorCode === "sitepolicynotagreed") {
29966
30359
  return { alive: false, remainingSeconds: null };
29967
30360
  }
29968
30361
  return { alive: null, remainingSeconds: null };
29969
30362
  }
29970
- const last = isRecord8(body.at(-1)) ? body.at(-1) : void 0;
29971
- const data = isRecord8(last?.data) ? last.data : void 0;
30363
+ const last = isRecord9(body.at(-1)) ? body.at(-1) : void 0;
30364
+ const data = isRecord9(last?.data) ? last.data : void 0;
29972
30365
  const remainingSeconds = typeof data?.timeremaining === "number" ? data.timeremaining : null;
29973
30366
  const rotatedCookie = cookieFromSetCookie(response.headers.get("set-cookie"), session.cookieName);
29974
30367
  return {
@@ -29990,7 +30383,7 @@ function cookieFromSetCookie(header, cookieName) {
29990
30383
  const escaped = cookieName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29991
30384
  return header.match(new RegExp(`(?:^|,\\s*)${escaped}=([^;,\\s]+)`))?.[1];
29992
30385
  }
29993
- function isRecord8(value) {
30386
+ function isRecord9(value) {
29994
30387
  return typeof value === "object" && value !== null && !Array.isArray(value);
29995
30388
  }
29996
30389
 
@@ -30212,16 +30605,16 @@ var SessionBroker = class {
30212
30605
  });
30213
30606
  }
30214
30607
  async readSession(storage) {
30215
- const record2 = await storage.get(SESSION_STORAGE_KEY);
30216
- if (!record2) return void 0;
30217
- if (record2.version === 2 && typeof record2.encrypted_session === "string") {
30218
- const decrypted = await decryptValue(record2.encrypted_session, await this.keyring());
30608
+ const record3 = await storage.get(SESSION_STORAGE_KEY);
30609
+ if (!record3) return void 0;
30610
+ if (record3.version === 2 && typeof record3.encrypted_session === "string") {
30611
+ const decrypted = await decryptValue(record3.encrypted_session, await this.keyring());
30219
30612
  const session = JSON.parse(decrypted.value);
30220
30613
  if (typeof session.cookie_value !== "string" || typeof session.sesskey !== "string" || !Number.isSafeInteger(session.moodle_user_id) || session.moodle_user_id <= 0) throw new Error("Invalid encrypted session.");
30221
30614
  return session;
30222
30615
  }
30223
- if (typeof record2.encrypted_cookie !== "string") throw new Error("Invalid legacy session.");
30224
- const { encrypted_cookie, ...metadata } = record2;
30616
+ if (typeof record3.encrypted_cookie !== "string") throw new Error("Invalid legacy session.");
30617
+ const { encrypted_cookie, ...metadata } = record3;
30225
30618
  const cookie = await decryptValue(encrypted_cookie, await this.keyring());
30226
30619
  return { ...metadata, cookie_value: cookie.value };
30227
30620
  }
@@ -30233,10 +30626,10 @@ var SessionBroker = class {
30233
30626
  }
30234
30627
  async loadSession() {
30235
30628
  return this.transaction(async (storage) => {
30236
- const record2 = await storage.get(SESSION_STORAGE_KEY);
30237
- if (!record2) return void 0;
30629
+ const record3 = await storage.get(SESSION_STORAGE_KEY);
30630
+ if (!record3) return void 0;
30238
30631
  const session = await this.readSession(storage);
30239
- if (session && (record2.version !== 2 || (await decryptValue(record2.encrypted_session, await this.keyring())).needsRotation)) {
30632
+ if (session && (record3.version !== 2 || (await decryptValue(record3.encrypted_session, await this.keyring())).needsRotation)) {
30240
30633
  await this.writeSession(storage, session);
30241
30634
  }
30242
30635
  return session;
@@ -30312,14 +30705,14 @@ function normalizeOrigin(value) {
30312
30705
  }
30313
30706
  }
30314
30707
  function isSessionCandidate(value) {
30315
- if (!isRecord9(value)) return false;
30708
+ if (!isRecord10(value)) return false;
30316
30709
  return typeof value.moodleOrigin === "string" && typeof value.cookieName === "string" && /^MoodleSession[A-Za-z0-9_-]*$/.test(value.cookieName) && typeof value.cookieValue === "string" && value.cookieValue.length > 0 && value.cookieValue.length <= 4096 && !/[\u0000-\u001f\u007f;]/.test(value.cookieValue) && (value.expectedRevision === null || Number.isInteger(value.expectedRevision) && value.expectedRevision >= 0);
30317
30710
  }
30318
- function isRecord9(value) {
30711
+ function isRecord10(value) {
30319
30712
  return typeof value === "object" && value !== null && !Array.isArray(value);
30320
30713
  }
30321
30714
  function isMcpEnvelope(value) {
30322
- if (!isRecord9(value) || !("request" in value) || !isRecord9(value.context)) return false;
30715
+ if (!isRecord10(value) || !("request" in value) || !isRecord10(value.context)) return false;
30323
30716
  return (value.context.protocolVersion === void 0 || typeof value.context.protocolVersion === "string") && (value.context.method === void 0 || typeof value.context.method === "string") && (value.context.toolName === void 0 || typeof value.context.toolName === "string");
30324
30717
  }
30325
30718