moodle-cli 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.1";
26456
26456
 
26457
26457
  // src/worker/http.ts
26458
26458
  var HEALTH_PATH = "/healthz";
@@ -27004,7 +27004,12 @@ async function fetchWithSession(input3, init, moodleOrigin, cookie, fetchImpl =
27004
27004
  headers.delete("authorization");
27005
27005
  headers.delete("proxy-authorization");
27006
27006
  }
27007
- const response = await fetchImpl(url2.toString(), { ...init, method, body, headers: Object.fromEntries(headers), signal, redirect: "manual" });
27007
+ let response;
27008
+ try {
27009
+ response = await fetchImpl(url2.toString(), { ...init, method, body, headers: Object.fromEntries(headers), signal, redirect: "manual" });
27010
+ } catch (error62) {
27011
+ throw requestFailure(error62, url2, deadline);
27012
+ }
27008
27013
  if (!REDIRECT_STATUSES.has(response.status)) return response;
27009
27014
  const location = response.headers.get("location");
27010
27015
  if (!location) return response;
@@ -27025,6 +27030,25 @@ async function fetchWithSession(input3, init, moodleOrigin, cookie, fetchImpl =
27025
27030
  url2 = next;
27026
27031
  }
27027
27032
  }
27033
+ var RequestFailed = class extends Error {
27034
+ host;
27035
+ timedOut;
27036
+ constructor(host, timedOut, cause) {
27037
+ super(timedOut ? `${host} did not respond within ${REQUEST_TIMEOUT_MS / 1e3}s.` : `Could not reach ${host}: ${cause}`);
27038
+ this.name = "RequestFailed";
27039
+ this.host = host;
27040
+ this.timedOut = timedOut;
27041
+ }
27042
+ };
27043
+ function requestFailure(error62, url2, deadline) {
27044
+ if (deadline.aborted) {
27045
+ return new RequestFailed(url2.host, true);
27046
+ }
27047
+ if (!(error62 instanceof Error)) {
27048
+ return error62;
27049
+ }
27050
+ return new RequestFailed(url2.host, false, error62.cause instanceof Error ? error62.cause.message : error62.message);
27051
+ }
27028
27052
 
27029
27053
  // src/constants.ts
27030
27054
  var PACKAGE_NAME = "moodle-cli";
@@ -27057,6 +27081,9 @@ var FUNC_GET_UNREAD_CONVERSATION_COUNTS = "core_message_get_unread_conversation_
27057
27081
  var FUNC_GET_DISCUSSION_POSTS = "mod_forum_get_discussion_posts";
27058
27082
  var DEFAULT_SESSION_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
27059
27083
 
27084
+ // src/moodle-assign-core.ts
27085
+ var import_node_html_parser3 = __toESM(require_dist(), 1);
27086
+
27060
27087
  // src/html-utils.ts
27061
27088
  var import_node_html_parser = __toESM(require_dist(), 1);
27062
27089
  function htmlToStructuredContent(html, baseUrl) {
@@ -27126,262 +27153,6 @@ function decodeHtml(value) {
27126
27153
  return value.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
27127
27154
  }
27128
27155
 
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
27156
  // src/scraper.ts
27386
27157
  var import_node_html_parser2 = __toESM(require_dist(), 1);
27387
27158
  function parseMoodleErrorHtml(html) {
@@ -27413,8 +27184,8 @@ function parseMoodleErrorHtml(html) {
27413
27184
  function parsePageContext(html, baseUrl) {
27414
27185
  const root = (0, import_node_html_parser2.parse)(html);
27415
27186
  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"));
27187
+ const sesskey = stringValue2(config2.sesskey).trim();
27188
+ const userid = numberValue(config2.userId) || numberValue(root.querySelector("[data-user-id]")?.getAttribute("data-user-id"));
27418
27189
  if (!sesskey || !userid) {
27419
27190
  throw new Error("Session appears invalid: could not load authenticated Moodle context");
27420
27191
  }
@@ -27426,8 +27197,8 @@ function parsePageContext(html, baseUrl) {
27426
27197
  fullname: cleanNodeText(root.querySelector(".userfullname")),
27427
27198
  sitename: extractSitename(root),
27428
27199
  siteurl: baseUrl,
27429
- ...config2.timezone ? { timezone: stringValue3(config2.timezone) } : {},
27430
- lang: stringValue3(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
27200
+ ...config2.timezone ? { timezone: stringValue2(config2.timezone) } : {},
27201
+ lang: stringValue2(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
27431
27202
  }
27432
27203
  };
27433
27204
  }
@@ -27967,7 +27738,7 @@ function numberQueryValue(href, key) {
27967
27738
  return null;
27968
27739
  }
27969
27740
  }
27970
- function numberValue2(value) {
27741
+ function numberValue(value) {
27971
27742
  if (typeof value === "number" && Number.isFinite(value)) {
27972
27743
  return value;
27973
27744
  }
@@ -27976,7 +27747,7 @@ function numberValue2(value) {
27976
27747
  }
27977
27748
  return 0;
27978
27749
  }
27979
- function stringValue3(value) {
27750
+ function stringValue2(value) {
27980
27751
  return typeof value === "string" ? value : value == null ? "" : String(value);
27981
27752
  }
27982
27753
  function fileEntry(name, url2, baseUrl) {
@@ -27990,6 +27761,619 @@ function unique(items) {
27990
27761
  return [...new Set(items)];
27991
27762
  }
27992
27763
 
27764
+ // src/moodle-assign-core.ts
27765
+ async function submitAssignmentFiles(deps, request) {
27766
+ const id2 = request.activityId;
27767
+ if (!Number.isSafeInteger(id2) || id2 <= 0) throw deps.usage("The assignment id must be a positive integer.");
27768
+ if (!request.files.length && !request.final) throw deps.usage("Give at least one file to upload, or use --final to submit the existing draft.");
27769
+ const seen = /* @__PURE__ */ new Set();
27770
+ for (const file3 of request.files) {
27771
+ if (!file3.name || /[\\/]/u.test(file3.name)) throw deps.usage(`'${file3.name}' is not a plain file name.`);
27772
+ if (seen.has(file3.name.toLowerCase())) throw deps.usage(`'${file3.name}' is given twice; Moodle keeps one file per name.`);
27773
+ seen.add(file3.name.toLowerCase());
27774
+ }
27775
+ const viewUrl = `${deps.baseUrl}${ASSIGN_VIEW_PATH}?id=${id2}`;
27776
+ request.onProgress?.("Reading the assignment");
27777
+ const before = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
27778
+ const form = parseSubmissionForm(await pageText(deps, `${viewUrl}&action=editsubmission`), deps);
27779
+ const draft = await listDraftFiles(deps, form);
27780
+ const removed = request.replace ? draft.map((file3) => file3.name) : [];
27781
+ const kept = request.replace ? [] : draft.filter((file3) => !seen.has(file3.name.toLowerCase()));
27782
+ checkLimits(deps, form, kept, request.files);
27783
+ let statement = form.statement;
27784
+ let confirm;
27785
+ if (request.final && !statement) {
27786
+ confirm = parseConfirmForm(await pageText(deps, `${viewUrl}&action=submit`), deps);
27787
+ statement = confirm.statement;
27788
+ }
27789
+ if (statement && !request.acceptStatement) {
27790
+ throw deps.usage(`Moodle requires you to accept this statement: "${statement}"`, "Re-run with --accept-statement once you agree.");
27791
+ }
27792
+ const limits = describeLimits(form);
27793
+ const uploads = request.files.map((file3) => ({ name: file3.name, bytes: file3.bytes.byteLength, ...file3.path ? { path: file3.path } : {} }));
27794
+ if (request.dryRun) {
27795
+ return {
27796
+ ...before,
27797
+ action: "planned",
27798
+ files: draft.map((file3) => ({ name: file3.name, bytes: file3.bytes })),
27799
+ uploads,
27800
+ removed,
27801
+ limits,
27802
+ ...statement ? { statement, statement_accepted: true } : {},
27803
+ checked_at: timestamp(deps)
27804
+ };
27805
+ }
27806
+ if (removed.length) {
27807
+ request.onProgress?.(`Removing ${removed.join(", ")}`);
27808
+ await deleteDraftFiles(deps, form, draft);
27809
+ }
27810
+ const storedNames = [];
27811
+ for (const [index, file3] of request.files.entries()) {
27812
+ request.onProgress?.(`Uploading ${file3.name} (${index + 1}/${request.files.length})`);
27813
+ storedNames.push(await uploadDraftFile(deps, form, file3));
27814
+ }
27815
+ if (storedNames.length || removed.length) {
27816
+ request.onProgress?.("Saving the submission");
27817
+ const savedHtml = await postForm(deps, form.action, [...form.fields, ...form.statement ? [["submissionstatement", "1"]] : [], ["submitbutton", "Save changes"]]);
27818
+ if (savedHtml !== null) throw deps.fail(`Moodle did not save the submission: ${noticesOf(savedHtml) || "it returned the edit form again without a reason"}`);
27819
+ }
27820
+ request.onProgress?.("Reading the receipt");
27821
+ let receipt = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
27822
+ const listed = new Set(receipt.files.map((file3) => file3.name.toLowerCase()));
27823
+ const missing = storedNames.filter((name) => !listed.has(name.toLowerCase()));
27824
+ 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.`);
27825
+ let action = "saved";
27826
+ if (isSubmitted(receipt.submission_status)) action = "submitted";
27827
+ else if (request.final) {
27828
+ request.onProgress?.("Submitting for grading");
27829
+ confirm ??= parseConfirmForm(await pageText(deps, `${viewUrl}&action=submit`), deps);
27830
+ const errorHtml = await postForm(deps, confirm.action, [...confirm.fields, ...confirm.statement ? [["submissionstatement", "1"]] : [], ["submitbutton", "Continue"]]);
27831
+ 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"}`);
27832
+ receipt = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
27833
+ 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.`);
27834
+ action = "submitted";
27835
+ }
27836
+ return {
27837
+ ...receipt,
27838
+ action,
27839
+ uploads,
27840
+ removed,
27841
+ limits,
27842
+ ...statement ? { statement, statement_accepted: true } : {},
27843
+ checked_at: timestamp(deps)
27844
+ };
27845
+ }
27846
+ function parseSubmissionForm(html, deps) {
27847
+ const root = (0, import_node_html_parser3.parse)(html);
27848
+ const form = formWithAction(root, "savesubmission");
27849
+ if (!form) {
27850
+ const notice = noticesOf(html);
27851
+ if (root.querySelectorAll("input[type=submit], button").some((button) => /begin assignment/iu.test(cleanText(button.getAttribute("value") ?? button.textContent)))) {
27852
+ throw deps.fail("This is a timed assignment; start it in a browser before uploading files.");
27853
+ }
27854
+ throw deps.fail(notice ? `Moodle is not accepting a submission: ${notice}` : "Moodle did not show a submission form for this assignment.");
27855
+ }
27856
+ const itemid = form.querySelector("input[name=files_filemanager]")?.getAttribute("value")?.trim() ?? "";
27857
+ if (!itemid) throw deps.fail("This assignment does not accept file uploads.");
27858
+ const options = filemanagerOptions(html, itemid);
27859
+ if (!options) throw deps.fail("Moodle did not describe the file upload area for this assignment.");
27860
+ const fields2 = formFields(form);
27861
+ const sesskey = fields2.find(([name]) => name === "sesskey")?.[1] ?? "";
27862
+ if (!sesskey) throw deps.fail("The submission form has no session key.");
27863
+ const picker = record2(options.filepicker);
27864
+ const repositories = (Array.isArray(picker.repositories) ? picker.repositories : Object.values(record2(picker.repositories))).map(record2);
27865
+ const upload = repositories.find((repo) => repo.type === "upload");
27866
+ if (!upload || upload.id === void 0) throw deps.fail("The site does not allow direct file uploads for this assignment.");
27867
+ const accepted = options.accepted_types;
27868
+ const acceptedTypes = Array.isArray(accepted) ? accepted.map(String).filter(Boolean) : accepted === void 0 || accepted === "*" ? "*" : [String(accepted)];
27869
+ return {
27870
+ action: resolveUrl(deps.baseUrl, form.getAttribute("action") || `${deps.baseUrl}${ASSIGN_VIEW_PATH}`),
27871
+ fields: fields2,
27872
+ sesskey,
27873
+ itemid,
27874
+ clientId: String(options.client_id ?? ""),
27875
+ contextId: String(record2(options.context).id ?? ""),
27876
+ repoId: String(upload.id),
27877
+ author: String(picker.author ?? ""),
27878
+ license: String(picker.defaultlicense ?? ""),
27879
+ maxBytes: integer2(options.maxbytes),
27880
+ areaMaxBytes: integer2(options.areamaxbytes),
27881
+ maxFiles: integer2(options.maxfiles),
27882
+ acceptedTypes: acceptedTypes.length === 1 && acceptedTypes[0] === "*" ? "*" : acceptedTypes,
27883
+ ...statementOf(form)
27884
+ };
27885
+ }
27886
+ function parseConfirmForm(html, deps) {
27887
+ const root = (0, import_node_html_parser3.parse)(html);
27888
+ const form = formWithAction(root, "confirmsubmit");
27889
+ if (!form) {
27890
+ const notice = noticesOf(html);
27891
+ throw deps.fail(notice ? `Moodle is not accepting a submission for grading: ${notice}` : "Moodle did not show the submit-for-grading confirmation.");
27892
+ }
27893
+ return { action: resolveUrl(deps.baseUrl, form.getAttribute("action") || `${deps.baseUrl}${ASSIGN_VIEW_PATH}`), fields: formFields(form), ...statementOf(form) };
27894
+ }
27895
+ function parseReceiptPage(html, activityId, baseUrl) {
27896
+ const page2 = parseAssignmentHtml(html, activityId, baseUrl);
27897
+ const root = (0, import_node_html_parser3.parse)(html);
27898
+ const files = /* @__PURE__ */ new Map();
27899
+ const add = (link) => {
27900
+ const name = cleanText(link.textContent);
27901
+ const href = link.getAttribute("href") ?? "";
27902
+ if (name && !files.has(name.toLowerCase())) files.set(name.toLowerCase(), { name, ...href ? { url: resolveUrl(baseUrl, href) } : {} });
27903
+ };
27904
+ for (const link of root.querySelectorAll(".fileuploadsubmission a[href]")) add(link);
27905
+ for (const link of tableCell(root, "File submissions")?.querySelectorAll("a[href]") ?? []) add(link);
27906
+ return {
27907
+ id: activityId,
27908
+ name: page2.name,
27909
+ ...page2.course_id ? { unit_id: page2.course_id } : {},
27910
+ url: page2.url,
27911
+ submission_status: page2.submission_status,
27912
+ grading_status: page2.grading_status,
27913
+ due: page2.due_pretty || cleanText(tableCell(root, "Due date")?.textContent),
27914
+ time_remaining: page2.time_remaining,
27915
+ last_modified: cleanText(tableCell(root, "Last modified")?.textContent),
27916
+ files: [...files.values()]
27917
+ };
27918
+ }
27919
+ function noticesOf(html) {
27920
+ const root = (0, import_node_html_parser3.parse)(html);
27921
+ const texts = [];
27922
+ for (const node2 of root.querySelectorAll(".alert, .invalid-feedback, .form-control-feedback, .error, [data-fieldtype] .text-danger")) {
27923
+ for (const junk of node2.querySelectorAll("button, .close")) junk.remove();
27924
+ const text = cleanText(node2.textContent);
27925
+ if (text && !texts.includes(text)) texts.push(text);
27926
+ }
27927
+ return texts.join(" ");
27928
+ }
27929
+ function formWithAction(root, action) {
27930
+ for (const form of root.querySelectorAll("form")) {
27931
+ if (form.querySelectorAll("input[name=action]").some((input3) => input3.getAttribute("value") === action)) return form;
27932
+ }
27933
+ return null;
27934
+ }
27935
+ function formFields(form) {
27936
+ const fields2 = [];
27937
+ for (const element of form.querySelectorAll("input, textarea, select")) {
27938
+ const name = element.getAttribute("name");
27939
+ if (!name) continue;
27940
+ const tag = element.tagName.toLowerCase();
27941
+ if (tag === "textarea") {
27942
+ fields2.push([name, element.textContent]);
27943
+ continue;
27944
+ }
27945
+ if (tag === "select") {
27946
+ const options = element.querySelectorAll("option");
27947
+ const chosen = options.find((option) => option.hasAttribute("selected")) ?? options[0];
27948
+ if (chosen) fields2.push([name, chosen.getAttribute("value") ?? cleanText(chosen.textContent)]);
27949
+ continue;
27950
+ }
27951
+ const type = (element.getAttribute("type") ?? "text").toLowerCase();
27952
+ if (["submit", "button", "image", "file", "reset"].includes(type)) continue;
27953
+ if ((type === "checkbox" || type === "radio") && !element.hasAttribute("checked")) continue;
27954
+ fields2.push([name, element.getAttribute("value") ?? (type === "checkbox" ? "on" : "")]);
27955
+ }
27956
+ return fields2;
27957
+ }
27958
+ function statementOf(form) {
27959
+ const box = form.querySelector("input[name=submissionstatement]");
27960
+ if (!box) return {};
27961
+ const id2 = box.getAttribute("id");
27962
+ const label = (id2 ? form.querySelector(`label[for="${id2}"]`) : null) ?? box.closest("label") ?? box.parentNode?.querySelector("label") ?? null;
27963
+ const text = cleanText(label?.textContent).replace(/\s*Required\s*$/u, "").trim();
27964
+ return { statement: text || "Submission statement" };
27965
+ }
27966
+ function filemanagerOptions(html, itemid) {
27967
+ const pattern = /M\.form_filemanager\.init\(\s*Y\s*,\s*/gu;
27968
+ let match;
27969
+ while (match = pattern.exec(html)) {
27970
+ const json2 = balancedObject(html, match.index + match[0].length);
27971
+ if (!json2) continue;
27972
+ try {
27973
+ const options = JSON.parse(json2);
27974
+ if (isRecord5(options) && String(options.itemid) === itemid) return options;
27975
+ } catch {
27976
+ }
27977
+ }
27978
+ return null;
27979
+ }
27980
+ function balancedObject(text, start) {
27981
+ if (text[start] !== "{") return null;
27982
+ let depth = 0;
27983
+ let quoted = false;
27984
+ for (let index = start; index < text.length; index += 1) {
27985
+ const char = text[index];
27986
+ if (quoted) {
27987
+ if (char === "\\") index += 1;
27988
+ else if (char === '"') quoted = false;
27989
+ } else if (char === '"') quoted = true;
27990
+ else if (char === "{") depth += 1;
27991
+ else if (char === "}") {
27992
+ depth -= 1;
27993
+ if (depth === 0) return text.slice(start, index + 1);
27994
+ }
27995
+ }
27996
+ return null;
27997
+ }
27998
+ function tableCell(root, label) {
27999
+ for (const row of root.querySelectorAll("tr")) {
28000
+ const cells = row.querySelectorAll("th, td");
28001
+ if (cells.length > 1 && cleanText(cells[0].textContent) === label) return cells[1];
28002
+ }
28003
+ return null;
28004
+ }
28005
+ async function pageText(deps, url2) {
28006
+ return (await deps.request(url2)).text();
28007
+ }
28008
+ async function postForm(deps, action, fields2) {
28009
+ const response = await deps.request(action, {
28010
+ method: "POST",
28011
+ headers: { "content-type": "application/x-www-form-urlencoded" },
28012
+ body: new URLSearchParams(fields2).toString()
28013
+ });
28014
+ const html = await response.text();
28015
+ return landedOnView(response.url) ? null : html;
28016
+ }
28017
+ function landedOnView(url2) {
28018
+ try {
28019
+ const parsed = new URL(url2);
28020
+ return parsed.pathname.endsWith(ASSIGN_VIEW_PATH) && (parsed.searchParams.get("action") ?? "view") === "view";
28021
+ } catch {
28022
+ return false;
28023
+ }
28024
+ }
28025
+ async function draftAjax(deps, form, action, params) {
28026
+ const body = new URLSearchParams({ sesskey: form.sesskey, client_id: form.clientId, itemid: form.itemid, ...params });
28027
+ const response = await deps.request(`${deps.baseUrl}/repository/draftfiles_ajax.php?action=${action}`, {
28028
+ method: "POST",
28029
+ headers: { "content-type": "application/x-www-form-urlencoded" },
28030
+ body: body.toString()
28031
+ }, { allowErrorStatus: true });
28032
+ return jsonOf(deps, response, `draft file ${action}`);
28033
+ }
28034
+ async function listDraftFiles(deps, form) {
28035
+ const data = record2(await draftAjax(deps, form, "list", { filepath: "/" }));
28036
+ const list2 = Array.isArray(data.list) ? data.list.map(record2) : [];
28037
+ 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);
28038
+ }
28039
+ async function deleteDraftFiles(deps, form, files) {
28040
+ const selected = JSON.stringify(files.map((file3) => ({ filename: file3.name, filepath: file3.path })));
28041
+ const result = await draftAjax(deps, form, "deleteselected", { selected });
28042
+ if (result === false) throw deps.fail("Moodle did not remove the existing submission files.");
28043
+ }
28044
+ async function uploadDraftFile(deps, form, file3) {
28045
+ const body = new FormData();
28046
+ body.set("sesskey", form.sesskey);
28047
+ body.set("client_id", form.clientId);
28048
+ body.set("repo_id", form.repoId);
28049
+ body.set("itemid", form.itemid);
28050
+ body.set("env", "filemanager");
28051
+ body.set("ctx_id", form.contextId);
28052
+ body.set("title", file3.name);
28053
+ body.set("author", form.author);
28054
+ body.set("license", form.license);
28055
+ body.set("savepath", "/");
28056
+ body.set("maxbytes", String(form.maxBytes));
28057
+ body.set("areamaxbytes", String(form.areaMaxBytes));
28058
+ for (const type of form.acceptedTypes === "*" ? ["*"] : form.acceptedTypes) body.append("accepted_types[]", type);
28059
+ body.set("overwrite", "1");
28060
+ body.set("repo_upload_file", new Blob([Uint8Array.from(file3.bytes)]), file3.name);
28061
+ const response = await deps.request(`${deps.baseUrl}/repository/repository_ajax.php?action=upload`, { method: "POST", body }, { allowErrorStatus: true });
28062
+ const data = record2(await jsonOf(deps, response, `upload of ${file3.name}`));
28063
+ if (typeof data.error === "string" && data.error) throw deps.fail(`Moodle refused ${file3.name}: ${data.error}`, typeof data.errorcode === "string" ? data.errorcode : void 0);
28064
+ if (data.event === "fileexists") throw deps.fail(`Moodle reports ${file3.name} already exists and did not overwrite it.`);
28065
+ const stored = typeof data.file === "string" && data.file ? data.file : file3.name;
28066
+ if (!data.url && !data.id && !data.file) throw deps.fail(`Moodle did not confirm the upload of ${file3.name}.`);
28067
+ return stored;
28068
+ }
28069
+ async function jsonOf(deps, response, step) {
28070
+ const text = await response.text();
28071
+ try {
28072
+ return JSON.parse(text);
28073
+ } catch {
28074
+ const notice = noticesOf(text);
28075
+ throw deps.fail(`Moodle did not answer the ${step} with JSON (HTTP ${response.status})${notice ? `: ${notice}` : ""}`);
28076
+ }
28077
+ }
28078
+ function checkLimits(deps, form, kept, files) {
28079
+ if (form.maxFiles > 0 && kept.length + files.length > form.maxFiles) {
28080
+ 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);
28081
+ }
28082
+ for (const file3 of files) {
28083
+ 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)}.`);
28084
+ if (form.acceptedTypes !== "*" && form.acceptedTypes.every((type) => type.startsWith(".")) && !form.acceptedTypes.some((type) => file3.name.toLowerCase().endsWith(type.toLowerCase()))) {
28085
+ throw deps.usage(`${file3.name} is not an accepted type; allowed: ${form.acceptedTypes.join(", ")}.`);
28086
+ }
28087
+ }
28088
+ const total = kept.reduce((sum, file3) => sum + file3.bytes, 0) + files.reduce((sum, file3) => sum + file3.bytes.byteLength, 0);
28089
+ 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);
28090
+ }
28091
+ function describeLimits(form) {
28092
+ return {
28093
+ ...form.maxBytes > 0 ? { max_bytes: form.maxBytes } : {},
28094
+ ...form.maxFiles > 0 ? { max_files: form.maxFiles } : {},
28095
+ ...form.areaMaxBytes > 0 ? { area_max_bytes: form.areaMaxBytes } : {},
28096
+ ...form.acceptedTypes !== "*" ? { accepted_types: form.acceptedTypes } : {}
28097
+ };
28098
+ }
28099
+ function isSubmitted(status) {
28100
+ return /\bsubmitted\b/iu.test(status) && !/\bnot submitted\b/iu.test(status);
28101
+ }
28102
+ function size(bytes) {
28103
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
28104
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
28105
+ return `${bytes} B`;
28106
+ }
28107
+ function timestamp(deps) {
28108
+ return (deps.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
28109
+ }
28110
+ function integer2(value) {
28111
+ const number4 = Number(value);
28112
+ return Number.isSafeInteger(number4) ? number4 : 0;
28113
+ }
28114
+ function isRecord5(value) {
28115
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28116
+ }
28117
+ function record2(value) {
28118
+ return isRecord5(value) ? value : {};
28119
+ }
28120
+
28121
+ // src/parsers.ts
28122
+ function schema(parser) {
28123
+ return { parse: parser };
28124
+ }
28125
+ var UserInfoSchema = schema(parseUserInfo);
28126
+ var CourseSchema = schema(parseCourse);
28127
+ var CoursesSchema = schema(parseCourses);
28128
+ var ActivitySchema = schema(parseActivity);
28129
+ var SectionSchema = schema(parseSection);
28130
+ var CourseContentsSchema = schema(parseCourseContents);
28131
+ var TodoItemSchema = schema(parseTodoItem);
28132
+ function parseUserInfo(value) {
28133
+ const data = asRecord(value);
28134
+ return {
28135
+ userid: numberValue2(data.userid),
28136
+ username: stringValue3(data.username),
28137
+ fullname: stringValue3(data.fullname),
28138
+ sitename: stringValue3(data.sitename),
28139
+ siteurl: stringValue3(data.siteurl),
28140
+ lang: stringValue3(data.lang),
28141
+ ...data.timezone ? { timezone: String(data.timezone) } : {}
28142
+ };
28143
+ }
28144
+ function parseCourse(value, nowSeconds = Math.floor(Date.now() / 1e3)) {
28145
+ const data = asRecord(value);
28146
+ const course = {
28147
+ id: numberValue2(data.id),
28148
+ shortname: stringValue3(data.shortname),
28149
+ fullname: stringValue3(data.fullname),
28150
+ category: numberValue2(data.category),
28151
+ visible: booleanValue(data.visible, true),
28152
+ startdate: numberValue2(data.startdate)
28153
+ };
28154
+ const enddate = numberValue2(data.enddate);
28155
+ if (enddate > 0) {
28156
+ course.enddate = enddate;
28157
+ }
28158
+ return course;
28159
+ }
28160
+ function parseCourses(value) {
28161
+ return asArray(value).map((item) => parseCourse(item));
28162
+ }
28163
+ function parseActivity(value) {
28164
+ const data = asRecord(value);
28165
+ return {
28166
+ id: numberValue2(data.id),
28167
+ name: stringValue3(data.name),
28168
+ modname: stringValue3(data.modname),
28169
+ url: stringValue3(data.url),
28170
+ visible: booleanValue(data.visible, true),
28171
+ description: stringValue3(data.description),
28172
+ ...data.completiondata && typeof data.completiondata === "object" ? { completion: numberValue2(asRecord(data.completiondata).state) } : {},
28173
+ ...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 })) } : {}
28174
+ };
28175
+ }
28176
+ function parseSection(value) {
28177
+ const data = asRecord(value);
28178
+ return {
28179
+ id: numberValue2(data.id),
28180
+ name: stringValue3(data.name),
28181
+ section: numberValue2(data.section),
28182
+ visible: booleanValue(data.visible, true),
28183
+ summary: stringValue3(data.summary),
28184
+ ...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
28185
+ activities: asArray(data.modules).map((item) => parseActivity(item))
28186
+ };
28187
+ }
28188
+ function parseCourseContents(value) {
28189
+ return asArray(value).map((item) => parseSection(item));
28190
+ }
28191
+ function parseCourseFormatState(value, baseUrl) {
28192
+ const state = asRecord(parseJsonValue(value));
28193
+ const activities = /* @__PURE__ */ new Map();
28194
+ const activitiesBySection = /* @__PURE__ */ new Map();
28195
+ for (const item of asArray(state.cm)) {
28196
+ const data = asRecord(item);
28197
+ const id2 = numberValue2(data.id);
28198
+ const sectionId = stringValue3(data.sectionid);
28199
+ const module = stringValue3(data.module) || stringValue3(data.plugin).replace(/^mod_/u, "") || stringValue3(data.modname).toLowerCase();
28200
+ const activity = {
28201
+ id: id2,
28202
+ name: htmlText(data.name, baseUrl),
28203
+ modname: module.toLowerCase(),
28204
+ url: stringValue3(data.url) ? resolveUrl(baseUrl, stringValue3(data.url)) : "",
28205
+ visible: booleanValue(data.visible, true) && booleanValue(data.uservisible, true) && !booleanValue(data.stealth),
28206
+ description: htmlText(data.content ?? data.description, baseUrl),
28207
+ ...data.completionstate !== void 0 && data.completionstate !== null ? { completion: numberValue2(data.completionstate) } : {}
28208
+ };
28209
+ activities.set(String(id2), activity);
28210
+ const sectionActivities = activitiesBySection.get(sectionId) ?? [];
28211
+ sectionActivities.push(activity);
28212
+ activitiesBySection.set(sectionId, sectionActivities);
28213
+ }
28214
+ return asArray(state.section).map((item) => {
28215
+ const data = asRecord(item);
28216
+ const id2 = numberValue2(data.id);
28217
+ const hasActivityList = Array.isArray(data.cmlist);
28218
+ const listedActivities = asArray(data.cmlist).map((activityId) => activities.get(stringValue3(activityId))).filter((activity) => activity !== void 0);
28219
+ return {
28220
+ id: id2,
28221
+ name: htmlText(data.title || data.rawtitle, baseUrl),
28222
+ section: numberValue2(data.section ?? data.number),
28223
+ visible: booleanValue(data.visible, true),
28224
+ summary: htmlText(data.summary, baseUrl),
28225
+ ...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
28226
+ activities: hasActivityList ? listedActivities : activitiesBySection.get(String(id2)) ?? []
28227
+ };
28228
+ });
28229
+ }
28230
+ function parseTodoItem(value) {
28231
+ const data = asRecord(value);
28232
+ const course = asRecord(data.course);
28233
+ const action = asRecord(data.action);
28234
+ const progress = course.progress;
28235
+ return {
28236
+ id: numberValue2(data.id),
28237
+ name: stringValue3(data.name),
28238
+ activity_name: stringValue3(data.activityname),
28239
+ modname: stringValue3(data.modulename),
28240
+ course_id: numberValue2(course.id),
28241
+ course_name: stringValue3(course.fullname),
28242
+ due_at: numberValue2(data.timesort) || numberValue2(data.timestart),
28243
+ overdue: booleanValue(data.overdue),
28244
+ actionable: booleanValue(action.actionable),
28245
+ action_name: stringValue3(action.name),
28246
+ action_url: stringValue3(action.url),
28247
+ url: stringValue3(data.url),
28248
+ event_type: stringValue3(data.eventtype),
28249
+ course_progress: typeof progress === "number" ? progress : void 0
28250
+ };
28251
+ }
28252
+ function parseTodoItems(value) {
28253
+ return asArray(value).map((item) => parseTodoItem(item));
28254
+ }
28255
+ function parseAlertNotification(value) {
28256
+ const data = asRecord(value);
28257
+ return {
28258
+ id: numberValue2(data.id),
28259
+ subject: stringValue3(data.subject),
28260
+ short_subject: stringValue3(data.shortenedsubject),
28261
+ event_type: stringValue3(data.eventtype),
28262
+ component: stringValue3(data.component),
28263
+ created_at: numberValue2(data.timecreated),
28264
+ created_pretty: stringValue3(data.timecreatedpretty),
28265
+ read: booleanValue(data.read),
28266
+ context_url: stringValue3(data.contexturl),
28267
+ context_name: stringValue3(data.contexturlname)
28268
+ };
28269
+ }
28270
+ function parseAlertSummary(notificationsData, countsData, unreadCountsData) {
28271
+ const notificationsRecord = asRecord(notificationsData);
28272
+ const counts2 = asRecord(countsData);
28273
+ const unreadCounts = asRecord(unreadCountsData);
28274
+ const types = asRecord(counts2.types);
28275
+ const unreadTypes = asRecord(unreadCounts.types);
28276
+ const notifications = asArray(notificationsRecord.notifications).map((item) => parseAlertNotification(item));
28277
+ return {
28278
+ notifications,
28279
+ notification_count: notifications.length,
28280
+ unread_notification_count: notifications.filter((notification) => !notification.read).length,
28281
+ starred_message_count: numberValue2(counts2.favourites),
28282
+ direct_message_count: numberValue2(types["1"]),
28283
+ group_message_count: numberValue2(types["2"]),
28284
+ self_message_count: numberValue2(types["3"]),
28285
+ unread_starred_message_count: numberValue2(unreadCounts.favourites),
28286
+ unread_direct_message_count: numberValue2(unreadTypes["1"]),
28287
+ unread_group_message_count: numberValue2(unreadTypes["2"]),
28288
+ unread_self_message_count: numberValue2(unreadTypes["3"])
28289
+ };
28290
+ }
28291
+ function parseForumPostAuthor(value) {
28292
+ const data = asRecord(value);
28293
+ const urls = asRecord(data.urls);
28294
+ return {
28295
+ id: numberValue2(data.id),
28296
+ fullname: stringValue3(data.fullname),
28297
+ profile_url: stringValue3(urls.profile),
28298
+ profile_image_url: stringValue3(urls.profileimage)
28299
+ };
28300
+ }
28301
+ function parseForumPost(value, baseUrl = "") {
28302
+ const data = asRecord(value);
28303
+ const urls = asRecord(data.urls);
28304
+ const messageHtml = stringValue3(data.message);
28305
+ const structured = htmlToStructuredContent(messageHtml, stringValue3(urls.view || urls.discuss) || baseUrl);
28306
+ return {
28307
+ id: numberValue2(data.id),
28308
+ discussion_id: numberValue2(data.discussionid),
28309
+ subject: stringValue3(data.subject),
28310
+ message_html: messageHtml,
28311
+ message_text: structured.text,
28312
+ image_urls: structured.image_urls,
28313
+ links: structured.links,
28314
+ tables: structured.tables,
28315
+ author: parseForumPostAuthor(data.author),
28316
+ parent_id: numberValue2(data.parentid),
28317
+ time_created: numberValue2(data.timecreated),
28318
+ time_modified: numberValue2(data.timemodified),
28319
+ created_pretty: "",
28320
+ unread: booleanValue(data.unread),
28321
+ is_deleted: booleanValue(data.isdeleted),
28322
+ is_private_reply: booleanValue(data.isprivatereply),
28323
+ url: stringValue3(urls.view || urls.viewisolated),
28324
+ reply_url: stringValue3(urls.reply)
28325
+ };
28326
+ }
28327
+ function parseForumDiscussion(value, discussionId, baseUrl = "") {
28328
+ const data = asRecord(value);
28329
+ const posts = asArray(data.posts).map((item) => parseForumPost(item, baseUrl));
28330
+ return {
28331
+ id: discussionId,
28332
+ subject: posts[0]?.subject ?? "",
28333
+ course_id: numberValue2(data.courseid),
28334
+ forum_id: numberValue2(data.forumid),
28335
+ group_id: numberValue2(data.groupid),
28336
+ group_name: stringValue3(data.groupname),
28337
+ url: posts[0]?.url ? posts[0].url.split("#", 1)[0] : "",
28338
+ posts
28339
+ };
28340
+ }
28341
+ function asRecord(value) {
28342
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
28343
+ }
28344
+ function asArray(value) {
28345
+ return Array.isArray(value) ? value : [];
28346
+ }
28347
+ function stringValue3(value) {
28348
+ return typeof value === "string" ? value : value == null ? "" : String(value);
28349
+ }
28350
+ function numberValue2(value) {
28351
+ if (typeof value === "number" && Number.isFinite(value)) {
28352
+ return value;
28353
+ }
28354
+ if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
28355
+ return Number(value);
28356
+ }
28357
+ return 0;
28358
+ }
28359
+ function booleanValue(value, defaultValue = false) {
28360
+ if (value === void 0 || value === null) {
28361
+ return defaultValue;
28362
+ }
28363
+ return Boolean(value);
28364
+ }
28365
+ function parseJsonValue(value) {
28366
+ if (typeof value !== "string") return value;
28367
+ try {
28368
+ return JSON.parse(value);
28369
+ } catch {
28370
+ return {};
28371
+ }
28372
+ }
28373
+ function htmlText(value, baseUrl) {
28374
+ return htmlToStructuredContent(stringValue3(value), baseUrl).text;
28375
+ }
28376
+
27993
28377
  // src/moodle-forum-core.ts
27994
28378
  var ForumModule = class {
27995
28379
  baseUrl;
@@ -28124,13 +28508,13 @@ var ForumModule = class {
28124
28508
  }
28125
28509
  };
28126
28510
  function shouldFallbackForumAjax(error62) {
28127
- if (!isRecord5(error62)) {
28511
+ if (!isRecord6(error62)) {
28128
28512
  return false;
28129
28513
  }
28130
28514
  const code = typeof error62.moodleErrorCode === "string" ? error62.moodleErrorCode : "";
28131
28515
  return code === "servicenotavailable" || code === "accessexception" || error62 instanceof Error && error62.message.includes("Web service is not available");
28132
28516
  }
28133
- function isRecord5(value) {
28517
+ function isRecord6(value) {
28134
28518
  return typeof value === "object" && value !== null && !Array.isArray(value);
28135
28519
  }
28136
28520
 
@@ -28262,9 +28646,9 @@ async function searchForumContent(source, query, options = {}) {
28262
28646
  hits.sort(sortBy === "recent" ? sortRecent : sortRelevant);
28263
28647
  return hits.slice(0, options.limit ?? 20).map(([, hit]) => includePostText ? hit : { ...hit, snippet: "" });
28264
28648
  }
28265
- async function inParallel(items, size, run) {
28649
+ async function inParallel(items, size2, run) {
28266
28650
  const results = [];
28267
- for (let index = 0; index < items.length; index += size) results.push(...await Promise.all(items.slice(index, index + size).map(run)));
28651
+ for (let index = 0; index < items.length; index += size2) results.push(...await Promise.all(items.slice(index, index + size2).map(run)));
28268
28652
  return results;
28269
28653
  }
28270
28654
  function normalizeQuery(value) {
@@ -28450,7 +28834,7 @@ var MoodleClientCore = class {
28450
28834
  try {
28451
28835
  if (this.unavailable.has(FUNC_GET_SITE_INFO) && this.userInfo?.fullname) return this.userInfo;
28452
28836
  const data = await this.call(FUNC_GET_SITE_INFO);
28453
- if (isRecord6(data) && "userid" in data) {
28837
+ if (isRecord7(data) && "userid" in data) {
28454
28838
  const info = parseUserInfo(data);
28455
28839
  this.sesskey = typeof data.sesskey === "string" ? data.sesskey : this.sesskey;
28456
28840
  this.userid = info.userid;
@@ -28549,9 +28933,9 @@ var MoodleClientCore = class {
28549
28933
  let type = "";
28550
28934
  try {
28551
28935
  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;
28936
+ const module = isRecord7(data) && isRecord7(data.cm) ? data.cm : data;
28937
+ type = isRecord7(module) && typeof module.modname === "string" ? module.modname : "";
28938
+ courseId = isRecord7(module) && typeof module.course === "number" ? module.course : void 0;
28555
28939
  } catch (error62) {
28556
28940
  if (!this.errors.isApi(error62) || error62.moodleErrorCode !== "servicenotavailable") {
28557
28941
  throw error62;
@@ -28595,14 +28979,14 @@ var MoodleClientCore = class {
28595
28979
  } else {
28596
28980
  data = await this.call(FUNC_GET_ACTION_EVENTS, { ...window, limittononsuspendedevents: true });
28597
28981
  }
28598
- const events = isRecord6(data) && Array.isArray(data.events) ? data.events : [];
28982
+ const events = isRecord7(data) && Array.isArray(data.events) ? data.events : [];
28599
28983
  for (const item of parseTodoItems(events)) if (!seen.has(item.id)) {
28600
28984
  seen.add(item.id);
28601
28985
  items.push(item);
28602
28986
  }
28603
28987
  if (events.length < batchSize) break;
28604
28988
  const last = events.at(-1);
28605
- const next = isRecord6(last) && typeof last.id === "number" ? last.id : void 0;
28989
+ const next = isRecord7(last) && typeof last.id === "number" ? last.id : void 0;
28606
28990
  if (!next || next === aftereventid) throw this.errors.api("Moodle repeated a calendar page; refine the date window.");
28607
28991
  aftereventid = next;
28608
28992
  }
@@ -28769,8 +29153,18 @@ var MoodleClientCore = class {
28769
29153
  async getFolder(id2) {
28770
29154
  return parseFolderHtml(await this.get(FOLDER_VIEW_PATH, { id: id2 }), id2, this.baseUrl);
28771
29155
  }
28772
- async requestAbsolute(url2, init = {}) {
28773
- return this.requestAbsoluteInternal(url2, init, true);
29156
+ async requestAbsolute(url2, init = {}, options = {}) {
29157
+ return this.requestAbsoluteInternal(url2, init, true, Boolean(options.allowErrorStatus));
29158
+ }
29159
+ /** Uploads files into an assignment and reads the receipt back from the site. */
29160
+ async submitAssignment(request) {
29161
+ await this.ensureSession();
29162
+ return submitAssignmentFiles({
29163
+ baseUrl: this.baseUrl,
29164
+ request: (url2, init, options) => this.requestAbsolute(url2, init, options),
29165
+ fail: (message, moodleErrorCode) => this.errors.api(message, moodleErrorCode),
29166
+ usage: (message, hint) => this.errors.usage ? this.errors.usage(message, hint) : new MoodleClientCoreError("usage", message, hint)
29167
+ }, request);
28774
29168
  }
28775
29169
  async getNewsForums(courseId) {
28776
29170
  const units = courseId === void 0 ? await this.getCourses() : (await this.getCourses()).filter((c) => c.id === courseId);
@@ -28780,7 +29174,7 @@ var MoodleClientCore = class {
28780
29174
  await this.ensureSession();
28781
29175
  const data = await this.call("mod_forum_get_forums_by_courses", { courseids: units.map((c) => c.id) });
28782
29176
  for (const f of Array.isArray(data) ? data : []) {
28783
- if (!isRecord6(f) || f.type !== "news" || typeof f.cmid !== "number") continue;
29177
+ if (!isRecord7(f) || f.type !== "news" || typeof f.cmid !== "number") continue;
28784
29178
  const c = units.find((c2) => c2.id === f.course);
28785
29179
  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
29180
  }
@@ -28908,16 +29302,16 @@ var MoodleClientCore = class {
28908
29302
  async getAbsolute(url2) {
28909
29303
  return (await this.requestAbsolute(url2)).text();
28910
29304
  }
28911
- async requestAbsoluteInternal(url2, init, allowRetry) {
29305
+ async requestAbsoluteInternal(url2, init, allowRetry, allowErrorStatus = false) {
28912
29306
  const response = await fetchWithSession(url2, init, this.baseUrl, this.cookie, this.fetchImpl);
28913
29307
  if (response.url.includes("/login/")) {
28914
29308
  if (this.onLoginRequired && allowRetry && !this.retryingLogin) {
28915
29309
  await this.reauthenticate();
28916
- return this.requestAbsoluteInternal(url2, init, false);
29310
+ return this.requestAbsoluteInternal(url2, init, false, allowErrorStatus);
28917
29311
  }
28918
29312
  throw this.errors.api("Session expired", "servicerequireslogin");
28919
29313
  }
28920
- if (!response.ok) {
29314
+ if (!response.ok && !allowErrorStatus) {
28921
29315
  const context = `HTTP ${response.status} loading ${safeUrl(url2)}`;
28922
29316
  const contentType2 = response.headers.get("content-type")?.toLowerCase() ?? "";
28923
29317
  if (contentType2.includes("text/html") || contentType2.includes("application/xhtml+xml")) {
@@ -28935,7 +29329,7 @@ var MoodleClientCore = class {
28935
29329
  let offset = 0;
28936
29330
  while (true) {
28937
29331
  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) {
29332
+ if (!isRecord7(data) || !Array.isArray(data.courses) || !data.courses.length) {
28939
29333
  break;
28940
29334
  }
28941
29335
  courses.push(...data.courses);
@@ -29098,19 +29492,19 @@ function placeholderUserInfo(baseUrl, userid) {
29098
29492
  };
29099
29493
  }
29100
29494
  function parseTodoPayload(value) {
29101
- return parseTodoItems(isRecord6(value) && Array.isArray(value.events) ? value.events : []);
29495
+ return parseTodoItems(isRecord7(value) && Array.isArray(value.events) ? value.events : []);
29102
29496
  }
29103
29497
  function errorMessage(value) {
29104
29498
  return value instanceof Error ? value.message : "Unknown Moodle error";
29105
29499
  }
29106
- function chunks(values, size) {
29500
+ function chunks(values, size2) {
29107
29501
  const result = [];
29108
- for (let index = 0; index < values.length; index += size) {
29109
- result.push(values.slice(index, index + size));
29502
+ for (let index = 0; index < values.length; index += size2) {
29503
+ result.push(values.slice(index, index + size2));
29110
29504
  }
29111
29505
  return result;
29112
29506
  }
29113
- function isRecord6(value) {
29507
+ function isRecord7(value) {
29114
29508
  return !!value && typeof value === "object" && !Array.isArray(value);
29115
29509
  }
29116
29510
  function isLoginErrorCode(code) {
@@ -29133,7 +29527,7 @@ function safeUrl(value) {
29133
29527
  }
29134
29528
 
29135
29529
  // src/mcp/gateway.ts
29136
- var import_node_html_parser3 = __toESM(require_dist(), 1);
29530
+ var import_node_html_parser4 = __toESM(require_dist(), 1);
29137
29531
  var MAX_MCP_FILE_BYTES = 16 * 1024 * 1024;
29138
29532
  var MoodleGatewayError = class extends Error {
29139
29533
  code;
@@ -29143,13 +29537,14 @@ var MoodleGatewayError = class extends Error {
29143
29537
  this.code = code;
29144
29538
  }
29145
29539
  };
29146
- function createMoodleGateway(client) {
29540
+ function createMoodleGateway(client, hooks = {}) {
29147
29541
  return {
29148
29542
  getUser: () => client.getSiteInfo(),
29149
29543
  listThreads: (id2) => client.getForumDiscussionRefs ? client.getForumDiscussionRefs(id2) : Promise.resolve([]),
29150
29544
  listNewsForums: (id2) => client.getNewsForums ? client.getNewsForums(id2) : Promise.resolve([]),
29151
29545
  getOverview: (input3) => client.getOverview(input3.todoLimit, input3.todoDays, input3.alertsLimit),
29152
29546
  ...client.getTodo ? { getDue: (days, courseId) => client.getTodo(Number.MAX_SAFE_INTEGER, days, courseId) } : {},
29547
+ ...client.submitAssignmentFiles ? { submitAssignment: (input3) => client.submitAssignmentFiles({ ...input3, ...hooks.onSubmitProgress ? { onProgress: hooks.onSubmitProgress } : {} }) } : {},
29153
29548
  listCourses: () => client.getCourses(),
29154
29549
  async getCourse({ courseId }) {
29155
29550
  const [courses, sections] = await Promise.all([
@@ -29273,7 +29668,7 @@ async function fileFromActivity(client, activityId) {
29273
29668
  return { url: entry.url, name: entry.name };
29274
29669
  }
29275
29670
  function resourceLinks(html, baseUrl) {
29276
- const root = (0, import_node_html_parser3.parse)(html);
29671
+ const root = (0, import_node_html_parser4.parse)(html);
29277
29672
  const entries = root.querySelectorAll(".resourceworkaround a[href], .resourcecontent a[href], a.resourceworkaround[href]").map((link) => ({
29278
29673
  name: link.textContent.trim(),
29279
29674
  url: new URL(link.getAttribute("href") ?? "", baseUrl).toString()
@@ -29327,7 +29722,7 @@ function isHtml(response) {
29327
29722
  return type.includes("text/html") || type.includes("application/xhtml+xml");
29328
29723
  }
29329
29724
  function looksLikeLoginPage(html) {
29330
- const root = (0, import_node_html_parser3.parse)(html);
29725
+ const root = (0, import_node_html_parser4.parse)(html);
29331
29726
  return root.querySelector('form[action*="/login/"], input[name="password"], #page-login-index') !== null || /<title>\s*(?:log in|login)/iu.test(html);
29332
29727
  }
29333
29728
  function contentType(response) {
@@ -29400,6 +29795,8 @@ var counts = external_exports.object(Object.fromEntries(["notification_count", "
29400
29795
  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
29796
  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
29797
  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 });
29798
+ var submissionFile = external_exports.object({ name: external_exports.string(), bytes: n, url: s });
29799
+ 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
29800
  var input2 = (shape) => external_exports.object(shape).strict();
29404
29801
  var list = (key, value) => external_exports.object({ [key]: external_exports.array(value).optional(), total: id });
29405
29802
  var intentContracts = {
@@ -29413,6 +29810,7 @@ var intentContracts = {
29413
29810
  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
29811
  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
29812
  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 }) }) },
29813
+ 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
29814
  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
29815
  };
29418
29816
  function intentDescription(name) {
@@ -29472,9 +29870,9 @@ function postRow(p, subject, tz = "UTC") {
29472
29870
  }
29473
29871
 
29474
29872
  // src/intents.ts
29475
- async function inParallel2(items, size, run) {
29873
+ async function inParallel2(items, size2, run) {
29476
29874
  const results = [];
29477
- for (let index = 0; index < items.length; index += size) results.push(...await Promise.all(items.slice(index, index + size).map(run)));
29875
+ for (let index = 0; index < items.length; index += size2) results.push(...await Promise.all(items.slice(index, index + size2).map(run)));
29478
29876
  return results;
29479
29877
  }
29480
29878
  function createIntentService(gateway, now = () => Date.now()) {
@@ -29665,6 +30063,12 @@ function createIntentService(gateway, now = () => Date.now()) {
29665
30063
  result = { file: { name: file3.name, mime_type: file3.mimeType, bytes: file3.bytes, uri: file3.uri } };
29666
30064
  break;
29667
30065
  }
30066
+ case "submit": {
30067
+ 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.");
30068
+ const activityId = await resolveItem(input3.ref);
30069
+ 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) }) };
30070
+ break;
30071
+ }
29668
30072
  }
29669
30073
  return intentContracts[name].output.parse(stripEmpty(result));
29670
30074
  }
@@ -29683,13 +30087,23 @@ var READ_ONLY_ANNOTATIONS = {
29683
30087
  idempotentHint: true,
29684
30088
  openWorldHint: true
29685
30089
  };
30090
+ var WRITE_ANNOTATIONS = {
30091
+ readOnlyHint: false,
30092
+ destructiveHint: true,
30093
+ idempotentHint: false,
30094
+ openWorldHint: true
30095
+ };
30096
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["submit"]);
29686
30097
  var aliases = { get_overview: "home", list_courses: "units", get_course: "unit", get_activity: "item", get_grades: "grades", get_thread: "thread", get_file: "file" };
29687
30098
  var TOOL_CATALOG = Object.entries(intentContracts).map(([name, contract]) => ({
29688
30099
  name,
29689
30100
  description: intentDescription(name),
29690
30101
  inputSchema: compactSchema(external_exports.toJSONSchema(contract.input, { io: "input" })),
29691
- annotations: READ_ONLY_ANNOTATIONS
30102
+ annotations: WRITE_TOOLS.has(name) ? WRITE_ANNOTATIONS : READ_ONLY_ANNOTATIONS
29692
30103
  }));
30104
+ function toolsFor(gateway) {
30105
+ return gateway.submitAssignment ? TOOL_CATALOG : TOOL_CATALOG.filter((tool) => !WRITE_TOOLS.has(tool.name));
30106
+ }
29693
30107
  var TOOL_OUTPUT_SCHEMAS = Object.fromEntries(Object.entries(intentContracts).map(([name, contract]) => [name, compactSchema(external_exports.toJSONSchema(contract.output))]));
29694
30108
  function compactSchema(value) {
29695
30109
  if (Array.isArray(value)) return value.map(compactSchema);
@@ -29727,7 +30141,7 @@ function createMoodleMcpServer(gateway, options = {}) {
29727
30141
  protocolVersion,
29728
30142
  capabilities: { tools: { listChanged: false } },
29729
30143
  serverInfo,
29730
- instructions: "Read-only access to the authenticated user's Moodle data."
30144
+ 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
30145
  });
29732
30146
  }
29733
30147
  if (request.method === "ping") {
@@ -29735,7 +30149,7 @@ function createMoodleMcpServer(gateway, options = {}) {
29735
30149
  }
29736
30150
  if (request.method === "tools/list") {
29737
30151
  return jsonRpcSuccess(id2, {
29738
- tools: TOOL_CATALOG,
30152
+ tools: toolsFor(gateway),
29739
30153
  resultType: "complete",
29740
30154
  _meta: RESULT_META
29741
30155
  });
@@ -29791,8 +30205,9 @@ async function callTool(gateway, params) {
29791
30205
  const requested = typeof params?.name === "string" ? params.name : "";
29792
30206
  const name = Object.hasOwn(aliases, requested) ? aliases[requested] : requested;
29793
30207
  if (!Object.hasOwn(intentContracts, name) && !["get_user", "list_activities", "list_forums"].includes(name)) throw new McpCallError("TOOL_NOT_FOUND", `Unknown Moodle tool: ${requested || "<missing>"}`);
30208
+ 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
30209
  const raw = params?.arguments ?? {};
29795
- if (!isRecord7(raw)) throw new McpCallError("INVALID_TOOL_ARGUMENTS", "Tool arguments must be an object.");
30210
+ if (!isRecord8(raw)) throw new McpCallError("INVALID_TOOL_ARGUMENTS", "Tool arguments must be an object.");
29796
30211
  const args = { ...raw };
29797
30212
  if (Object.hasOwn(aliases, requested)) {
29798
30213
  const renames = { courseId: "unit", activityId: "ref", discussionId: "discussion_id", source: "ref", todoDays: "days", gradedOnly: "graded_only" };
@@ -29840,7 +30255,7 @@ async function callTool(gateway, params) {
29840
30255
  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
30256
  return { content: [{ type: "text", text: JSON.stringify({ error: mapped2 }) }], structuredContent: { error: mapped2 }, isError: true, resultType: "complete", _meta: RESULT_META };
29842
30257
  }
29843
- const mapped = error62 instanceof ReferenceError2 ? { type: error62.code, code: error62.code, message: error62.message, hint: error62.hint, candidates: error62.candidates } : mapMoodleError(error62);
30258
+ 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
30259
  return { content: [{ type: "text", text: JSON.stringify({ error: mapped }) }], structuredContent: { error: mapped }, isError: true, resultType: "complete", _meta: RESULT_META };
29845
30260
  }
29846
30261
  }
@@ -29859,9 +30274,11 @@ function toolContent(name, payload, structuredContent) {
29859
30274
  }
29860
30275
  ];
29861
30276
  }
29862
- function mapMoodleError(error62) {
29863
- const record2 = isRecord7(error62) ? error62 : {};
29864
- const code = typeof record2.code === "string" ? record2.code : "";
30277
+ function mapMoodleError(error62, verbatim = false) {
30278
+ const record3 = isRecord8(error62) ? error62 : {};
30279
+ const code = typeof record3.code === "string" ? record3.code : "";
30280
+ const own2 = verbatim && typeof record3.message === "string" && record3.message.trim() && code !== "auth" ? record3.message : void 0;
30281
+ const ownHint = verbatim && typeof record3.hint === "string" && record3.hint.trim() ? record3.hint : void 0;
29865
30282
  const typeByCode = {
29866
30283
  auth: "MOODLE_AUTH_REQUIRED",
29867
30284
  not_found: "MOODLE_NOT_FOUND",
@@ -29870,8 +30287,8 @@ function mapMoodleError(error62) {
29870
30287
  };
29871
30288
  const type = code.startsWith("MOODLE_") ? code : typeByCode[code] ?? "MOODLE_UPSTREAM_ERROR";
29872
30289
  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 } : {} };
30290
+ const moodleCode = typeof record3.moodleErrorCode === "string" && /^[a-z][a-z0-9_]{0,63}$/u.test(record3.moodleErrorCode) ? record3.moodleErrorCode : void 0;
30291
+ 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
30292
  }
29876
30293
  var McpCallError = class extends Error {
29877
30294
  type;
@@ -29883,11 +30300,11 @@ var McpCallError = class extends Error {
29883
30300
  this.details = details;
29884
30301
  }
29885
30302
  };
29886
- function isRecord7(value) {
30303
+ function isRecord8(value) {
29887
30304
  return typeof value === "object" && value !== null && !Array.isArray(value);
29888
30305
  }
29889
30306
  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";
30307
+ return isRecord8(value) && typeof value.name === "string" && typeof value.mimeType === "string" && typeof value.bytes === "number" && typeof value.uri === "string" && typeof value.blob === "string";
29891
30308
  }
29892
30309
 
29893
30310
  // src/worker/moodle-upstream.ts
@@ -29958,17 +30375,17 @@ var FetchMoodleSessionUpstream = class {
29958
30375
  return { alive: false, remainingSeconds: null };
29959
30376
  }
29960
30377
  if (!Array.isArray(body) || body.length === 0) return { alive: null, remainingSeconds: null };
29961
- const first2 = isRecord8(body[0]) ? body[0] : void 0;
30378
+ const first2 = isRecord9(body[0]) ? body[0] : void 0;
29962
30379
  if (first2?.error) {
29963
- const exception = isRecord8(first2.exception) ? first2.exception : void 0;
30380
+ const exception = isRecord9(first2.exception) ? first2.exception : void 0;
29964
30381
  const errorCode = typeof exception?.errorcode === "string" ? exception.errorcode : "";
29965
30382
  if (errorCode === "servicerequireslogin" || errorCode === "sitepolicynotagreed") {
29966
30383
  return { alive: false, remainingSeconds: null };
29967
30384
  }
29968
30385
  return { alive: null, remainingSeconds: null };
29969
30386
  }
29970
- const last = isRecord8(body.at(-1)) ? body.at(-1) : void 0;
29971
- const data = isRecord8(last?.data) ? last.data : void 0;
30387
+ const last = isRecord9(body.at(-1)) ? body.at(-1) : void 0;
30388
+ const data = isRecord9(last?.data) ? last.data : void 0;
29972
30389
  const remainingSeconds = typeof data?.timeremaining === "number" ? data.timeremaining : null;
29973
30390
  const rotatedCookie = cookieFromSetCookie(response.headers.get("set-cookie"), session.cookieName);
29974
30391
  return {
@@ -29990,7 +30407,7 @@ function cookieFromSetCookie(header, cookieName) {
29990
30407
  const escaped = cookieName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29991
30408
  return header.match(new RegExp(`(?:^|,\\s*)${escaped}=([^;,\\s]+)`))?.[1];
29992
30409
  }
29993
- function isRecord8(value) {
30410
+ function isRecord9(value) {
29994
30411
  return typeof value === "object" && value !== null && !Array.isArray(value);
29995
30412
  }
29996
30413
 
@@ -30212,16 +30629,16 @@ var SessionBroker = class {
30212
30629
  });
30213
30630
  }
30214
30631
  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());
30632
+ const record3 = await storage.get(SESSION_STORAGE_KEY);
30633
+ if (!record3) return void 0;
30634
+ if (record3.version === 2 && typeof record3.encrypted_session === "string") {
30635
+ const decrypted = await decryptValue(record3.encrypted_session, await this.keyring());
30219
30636
  const session = JSON.parse(decrypted.value);
30220
30637
  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
30638
  return session;
30222
30639
  }
30223
- if (typeof record2.encrypted_cookie !== "string") throw new Error("Invalid legacy session.");
30224
- const { encrypted_cookie, ...metadata } = record2;
30640
+ if (typeof record3.encrypted_cookie !== "string") throw new Error("Invalid legacy session.");
30641
+ const { encrypted_cookie, ...metadata } = record3;
30225
30642
  const cookie = await decryptValue(encrypted_cookie, await this.keyring());
30226
30643
  return { ...metadata, cookie_value: cookie.value };
30227
30644
  }
@@ -30233,10 +30650,10 @@ var SessionBroker = class {
30233
30650
  }
30234
30651
  async loadSession() {
30235
30652
  return this.transaction(async (storage) => {
30236
- const record2 = await storage.get(SESSION_STORAGE_KEY);
30237
- if (!record2) return void 0;
30653
+ const record3 = await storage.get(SESSION_STORAGE_KEY);
30654
+ if (!record3) return void 0;
30238
30655
  const session = await this.readSession(storage);
30239
- if (session && (record2.version !== 2 || (await decryptValue(record2.encrypted_session, await this.keyring())).needsRotation)) {
30656
+ if (session && (record3.version !== 2 || (await decryptValue(record3.encrypted_session, await this.keyring())).needsRotation)) {
30240
30657
  await this.writeSession(storage, session);
30241
30658
  }
30242
30659
  return session;
@@ -30312,14 +30729,14 @@ function normalizeOrigin(value) {
30312
30729
  }
30313
30730
  }
30314
30731
  function isSessionCandidate(value) {
30315
- if (!isRecord9(value)) return false;
30732
+ if (!isRecord10(value)) return false;
30316
30733
  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
30734
  }
30318
- function isRecord9(value) {
30735
+ function isRecord10(value) {
30319
30736
  return typeof value === "object" && value !== null && !Array.isArray(value);
30320
30737
  }
30321
30738
  function isMcpEnvelope(value) {
30322
- if (!isRecord9(value) || !("request" in value) || !isRecord9(value.context)) return false;
30739
+ if (!isRecord10(value) || !("request" in value) || !isRecord10(value.context)) return false;
30323
30740
  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
30741
  }
30325
30742