moodle-cli 0.7.0-alpha.3 → 0.7.0-alpha.5

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.
@@ -3573,7 +3573,7 @@ var require_parse = __commonJS({
3573
3573
  var whitespace = /* @__PURE__ */ new Set([9, 10, 12, 13, 32]);
3574
3574
  var ZERO = "0".charCodeAt(0);
3575
3575
  var NINE = "9".charCodeAt(0);
3576
- function parse6(formula) {
3576
+ function parse7(formula) {
3577
3577
  formula = formula.trim().toLowerCase();
3578
3578
  if (formula === "even") {
3579
3579
  return [2, 0];
@@ -3625,7 +3625,7 @@ var require_parse = __commonJS({
3625
3625
  }
3626
3626
  }
3627
3627
  }
3628
- exports.parse = parse6;
3628
+ exports.parse = parse7;
3629
3629
  }
3630
3630
  });
3631
3631
 
@@ -5075,7 +5075,7 @@ var require_html = __commonJS({
5075
5075
  }).join("");
5076
5076
  }
5077
5077
  set innerHTML(content) {
5078
- const r = parse6(content, this._parseOptions);
5078
+ const r = parse7(content, this._parseOptions);
5079
5079
  const nodes = r.childNodes.length ? r.childNodes : [new text_1.default(content, this)];
5080
5080
  resetParent(nodes, this);
5081
5081
  resetParent(this.childNodes, null);
@@ -5086,7 +5086,7 @@ var require_html = __commonJS({
5086
5086
  content = [content];
5087
5087
  } else if (typeof content == "string") {
5088
5088
  options = Object.assign(Object.assign({}, this._parseOptions), options);
5089
- const r = parse6(content, options);
5089
+ const r = parse7(content, options);
5090
5090
  content = r.childNodes.length ? r.childNodes : [new text_1.default(r.innerHTML, this)];
5091
5091
  }
5092
5092
  resetParent(this.childNodes, null);
@@ -5100,7 +5100,7 @@ var require_html = __commonJS({
5100
5100
  if (node instanceof node_1.default) {
5101
5101
  return [node];
5102
5102
  } else if (typeof node == "string") {
5103
- const r = parse6(node, this._parseOptions);
5103
+ const r = parse7(node, this._parseOptions);
5104
5104
  return r.childNodes.length ? r.childNodes : [new text_1.default(node, this)];
5105
5105
  }
5106
5106
  return [];
@@ -5484,7 +5484,7 @@ var require_html = __commonJS({
5484
5484
  if (arguments.length < 2) {
5485
5485
  throw new Error("2 arguments required");
5486
5486
  }
5487
- const p = parse6(html, this._parseOptions);
5487
+ const p = parse7(html, this._parseOptions);
5488
5488
  if (where === "afterend") {
5489
5489
  this.after(...p.childNodes);
5490
5490
  } else if (where === "afterbegin") {
@@ -5630,7 +5630,7 @@ var require_html = __commonJS({
5630
5630
  }
5631
5631
  /** Clone this Node */
5632
5632
  clone() {
5633
- return parse6(this.toString(), this._parseOptions).firstChild;
5633
+ return parse7(this.toString(), this._parseOptions).firstChild;
5634
5634
  }
5635
5635
  };
5636
5636
  exports.default = HTMLElement2;
@@ -5832,7 +5832,7 @@ var require_html = __commonJS({
5832
5832
  return stack;
5833
5833
  }
5834
5834
  exports.base_parse = base_parse;
5835
- function parse6(data, options = {}) {
5835
+ function parse7(data, options = {}) {
5836
5836
  const stack = base_parse(data, options);
5837
5837
  const [root] = stack;
5838
5838
  while (stack.length > 1) {
@@ -5860,7 +5860,7 @@ var require_html = __commonJS({
5860
5860
  }
5861
5861
  return root;
5862
5862
  }
5863
- exports.parse = parse6;
5863
+ exports.parse = parse7;
5864
5864
  function resolveInsertable(insertable) {
5865
5865
  return insertable.map((val) => {
5866
5866
  if (typeof val === "string") {
@@ -5928,18 +5928,18 @@ var require_dist = __commonJS({
5928
5928
  var parse_1 = __importDefault(require_parse2());
5929
5929
  var valid_1 = __importDefault(require_valid());
5930
5930
  exports.valid = valid_1.default;
5931
- function parse6(data, options = {}) {
5931
+ function parse7(data, options = {}) {
5932
5932
  return (0, parse_1.default)(data, options);
5933
5933
  }
5934
- exports.default = parse6;
5935
- exports.parse = parse6;
5936
- parse6.parse = parse_1.default;
5937
- parse6.HTMLElement = html_1.default;
5938
- parse6.CommentNode = comment_1.default;
5939
- parse6.valid = valid_1.default;
5940
- parse6.Node = node_1.default;
5941
- parse6.TextNode = text_1.default;
5942
- parse6.NodeType = type_1.default;
5934
+ exports.default = parse7;
5935
+ exports.parse = parse7;
5936
+ parse7.parse = parse_1.default;
5937
+ parse7.HTMLElement = html_1.default;
5938
+ parse7.CommentNode = comment_1.default;
5939
+ parse7.valid = valid_1.default;
5940
+ parse7.Node = node_1.default;
5941
+ parse7.TextNode = text_1.default;
5942
+ parse7.NodeType = type_1.default;
5943
5943
  }
5944
5944
  });
5945
5945
 
@@ -5995,300 +5995,6 @@ function problemResponse(status, code, title, detail, headers) {
5995
5995
  );
5996
5996
  }
5997
5997
 
5998
- // src/version.ts
5999
- var VERSION = "0.7.0-alpha.3";
6000
-
6001
- // src/worker/http.ts
6002
- var HEALTH_PATH = "/healthz";
6003
- var READY_PATH = "/readyz";
6004
- var MCP_PATH = "/mcp";
6005
- var SESSION_PATH = "/session";
6006
- var SESSION_TOUCH_PATH = "/session/touch";
6007
- var WORKER_SERVICE_ID = "moodle-mcp";
6008
- var WORKER_SERVICE_VERSION = VERSION;
6009
- function createWorkerHandler(dependencies) {
6010
- return {
6011
- async fetch(request, env) {
6012
- const url2 = new URL(request.url);
6013
- const authorityProblem = validateRequestAuthority(request, url2, env.EXPECTED_HOST);
6014
- if (authorityProblem) return authorityProblem;
6015
- if (hasQueryCredential(url2)) return unauthorized("Bearer credentials are not accepted in the query string.");
6016
- if (url2.pathname === HEALTH_PATH && request.method === "GET") {
6017
- return Response.json(
6018
- { status: "pass", serviceId: WORKER_SERVICE_ID, version: WORKER_SERVICE_VERSION },
6019
- { headers: { "content-type": "application/health+json; charset=utf-8" } }
6020
- );
6021
- }
6022
- if (url2.pathname === MCP_PATH && request.method !== "POST") {
6023
- return problemResponse(405, "METHOD_NOT_ALLOWED", "Method Not Allowed", "The MCP transport accepts POST requests only.", {
6024
- allow: "POST"
6025
- });
6026
- }
6027
- if (url2.pathname === MCP_PATH) {
6028
- if (!await verifyBearerToken(request.headers.get("authorization"), [
6029
- env.MCP_ACCESS_TOKEN_DIGEST,
6030
- activePreviousDigest(env.MCP_ACCESS_TOKEN_PREVIOUS_DIGEST, env.TOKEN_OVERLAP_EXPIRES_AT)
6031
- ])) {
6032
- return unauthorized();
6033
- }
6034
- const server = typeof dependencies.mcpServer === "function" ? dependencies.mcpServer(env) : dependencies.mcpServer;
6035
- return handleMcpRequest(request, server);
6036
- }
6037
- if (url2.pathname === READY_PATH && request.method === "GET") {
6038
- if (!await authorizeSessionSync(request, env)) return unauthorized();
6039
- const broker = resolveBroker(dependencies, env);
6040
- if (broker instanceof Response) return broker;
6041
- return broker.ready();
6042
- }
6043
- if (url2.pathname === SESSION_PATH && request.method === "PUT") {
6044
- if (!await authorizeSessionSync(request, env)) return unauthorized();
6045
- const broker = resolveBroker(dependencies, env);
6046
- if (broker instanceof Response) return broker;
6047
- return broker.replaceSession(request);
6048
- }
6049
- if (url2.pathname === SESSION_TOUCH_PATH && request.method === "POST") {
6050
- if (!await authorizeSessionSync(request, env)) return unauthorized();
6051
- const broker = resolveBroker(dependencies, env);
6052
- if (broker instanceof Response) return broker;
6053
- return broker.touch();
6054
- }
6055
- if (url2.pathname === READY_PATH || url2.pathname === SESSION_PATH || url2.pathname === SESSION_TOUCH_PATH) {
6056
- const method = url2.pathname === READY_PATH ? "GET" : url2.pathname === SESSION_PATH ? "PUT" : "POST";
6057
- return problemResponse(405, "METHOD_NOT_ALLOWED", "Method Not Allowed", "The session route does not accept this method.", {
6058
- allow: method
6059
- });
6060
- }
6061
- return problemResponse(404, "NOT_FOUND", "Not Found", "The requested route does not exist.");
6062
- }
6063
- };
6064
- }
6065
- function createDurableObjectBrokerApi(namespace) {
6066
- const stub = namespace.get(namespace.idFromName("primary"));
6067
- return {
6068
- async handleMcp(body, context) {
6069
- const response = await stub.fetch(new Request("https://session-broker/mcp", {
6070
- method: "POST",
6071
- headers: { "content-type": "application/json" },
6072
- body: JSON.stringify({ request: body, context })
6073
- }));
6074
- if (!response.ok) {
6075
- const problem = await safeProblem(response);
6076
- throw new SessionBrokerMcpError(response.status, problem?.code);
6077
- }
6078
- const envelope = await response.json();
6079
- return envelope.response ?? null;
6080
- },
6081
- ready: () => stub.fetch(new Request("https://session-broker/readyz")),
6082
- async replaceSession(request) {
6083
- return stub.fetch(new Request("https://session-broker/session", {
6084
- method: "PUT",
6085
- headers: { "content-type": request.headers.get("content-type") ?? "application/json" },
6086
- body: await request.arrayBuffer()
6087
- }));
6088
- },
6089
- touch: () => stub.fetch(new Request("https://session-broker/session/touch", { method: "POST" }))
6090
- };
6091
- }
6092
- var SessionBrokerMcpError = class extends Error {
6093
- constructor(status, code) {
6094
- super("The session broker could not complete the MCP request.");
6095
- this.status = status;
6096
- this.code = code;
6097
- this.name = "SessionBrokerMcpError";
6098
- }
6099
- status;
6100
- code;
6101
- };
6102
- function resolveBroker(dependencies, env) {
6103
- if (dependencies.broker) return dependencies.broker(env);
6104
- if (env.SESSION_BROKER) return createDurableObjectBrokerApi(env.SESSION_BROKER);
6105
- return problemResponse(503, "SESSION_BROKER_UNAVAILABLE", "Service Unavailable", "The session broker is unavailable.");
6106
- }
6107
- function validateRequestAuthority(request, url2, configuredHost) {
6108
- const expectedHost = (configuredHost ?? url2.host).toLowerCase();
6109
- const requestHost = (request.headers.get("host") ?? url2.host).toLowerCase();
6110
- if (requestHost !== expectedHost) {
6111
- return problemResponse(403, "INVALID_HOST", "Forbidden", "The request Host is not allowed.");
6112
- }
6113
- const origin = request.headers.get("origin");
6114
- if (!origin) return null;
6115
- try {
6116
- const parsed = new URL(origin);
6117
- if (parsed.origin !== url2.origin || parsed.host.toLowerCase() !== expectedHost) {
6118
- return problemResponse(403, "INVALID_ORIGIN", "Forbidden", "The request Origin is not allowed.");
6119
- }
6120
- } catch {
6121
- return problemResponse(403, "INVALID_ORIGIN", "Forbidden", "The request Origin is not allowed.");
6122
- }
6123
- return null;
6124
- }
6125
- function unauthorized(detail = "A valid Bearer token is required.") {
6126
- return problemResponse(401, "INVALID_BEARER_TOKEN", "Unauthorized", detail, {
6127
- "www-authenticate": 'Bearer realm="moodle-mcp"'
6128
- });
6129
- }
6130
- async function handleMcpRequest(request, server) {
6131
- if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
6132
- return problemResponse(415, "UNSUPPORTED_MEDIA_TYPE", "Unsupported Media Type", "MCP requests must use application/json.");
6133
- }
6134
- let body;
6135
- try {
6136
- body = await request.json();
6137
- } catch {
6138
- return problemResponse(400, "INVALID_JSON", "Bad Request", "The request body is not valid JSON.");
6139
- }
6140
- const protocolVersion = request.headers.get("mcp-protocol-version");
6141
- const metadataProblem = validateProtocolMetadata(request.headers, body, protocolVersion);
6142
- if (metadataProblem) return metadataProblem;
6143
- try {
6144
- const response = await server.handle(body, {
6145
- protocolVersion,
6146
- method: request.headers.get("mcp-method") ?? void 0,
6147
- toolName: request.headers.get("mcp-name") ?? void 0
6148
- });
6149
- if (response === null) return new Response(null, { status: 202 });
6150
- if (request.headers.get("accept")?.toLowerCase().includes("text/event-stream")) {
6151
- return new Response(`event: message
6152
- data: ${JSON.stringify(response)}
6153
-
6154
- `, {
6155
- headers: {
6156
- "cache-control": "private, no-store",
6157
- "content-type": "text/event-stream; charset=utf-8"
6158
- }
6159
- });
6160
- }
6161
- return Response.json(response, { headers: { "cache-control": "private, no-store" } });
6162
- } catch (error51) {
6163
- if (error51 instanceof SessionBrokerMcpError) {
6164
- const status = error51.status === 503 ? 503 : 500;
6165
- const code = error51.code === "SESSION_MISSING" || error51.code === "SESSION_EXPIRED" ? error51.code : "SESSION_UNAVAILABLE";
6166
- return problemResponse(status, code, "Service Unavailable", "The Moodle session is not ready.");
6167
- }
6168
- if (error51 instanceof Error && error51.name === "UnsupportedProtocolVersionError") {
6169
- return problemResponse(400, "UNSUPPORTED_PROTOCOL_VERSION", "Unsupported Protocol Version", "The requested MCP protocol version is not supported.");
6170
- }
6171
- return problemResponse(500, "MCP_REQUEST_FAILED", "Internal Server Error", "The MCP request could not be completed.");
6172
- }
6173
- }
6174
- async function safeProblem(response) {
6175
- try {
6176
- const value = await response.json();
6177
- return isRecord(value) && (value.code === void 0 || typeof value.code === "string") ? value : null;
6178
- } catch {
6179
- return null;
6180
- }
6181
- }
6182
- function validateProtocolMetadata(headers, body, protocolVersion) {
6183
- if (!protocolVersion) {
6184
- return problemResponse(400, "MCP_PROTOCOL_METADATA_INVALID", "Bad Request", "MCP-Protocol-Version is required.");
6185
- }
6186
- const params = isRecord(body.params) ? body.params : void 0;
6187
- const metadata = params && isRecord(params._meta) ? params._meta : void 0;
6188
- const bodyVersion = metadata?.["io.modelcontextprotocol/protocolVersion"];
6189
- if (bodyVersion !== void 0 && bodyVersion !== protocolVersion) {
6190
- return problemResponse(400, "MCP_PROTOCOL_METADATA_MISMATCH", "Bad Request", "MCP protocol metadata does not match the HTTP headers.");
6191
- }
6192
- if (typeof body.method !== "string") {
6193
- return problemResponse(400, "MCP_PROTOCOL_METADATA_INVALID", "Bad Request", "The JSON-RPC method is required.");
6194
- }
6195
- const headerMethod = headers.get("mcp-method");
6196
- const headerName = headers.get("mcp-name") ?? void 0;
6197
- const paramsName = params && typeof params.name === "string" ? params.name : void 0;
6198
- if (headerMethod !== body.method || headerName !== paramsName) {
6199
- return problemResponse(400, "MCP_PROTOCOL_METADATA_MISMATCH", "Bad Request", "MCP method metadata does not match the JSON-RPC request.");
6200
- }
6201
- return null;
6202
- }
6203
- function authorizeSessionSync(request, env) {
6204
- return verifyBearerToken(request.headers.get("authorization"), [
6205
- env.SESSION_SYNC_TOKEN_DIGEST,
6206
- activePreviousDigest(env.SESSION_SYNC_TOKEN_PREVIOUS_DIGEST, env.TOKEN_OVERLAP_EXPIRES_AT)
6207
- ]);
6208
- }
6209
- function activePreviousDigest(digest, expiresAt2) {
6210
- if (!digest || !expiresAt2) return void 0;
6211
- const expiration = Number(expiresAt2);
6212
- return Number.isFinite(expiration) && Date.now() < expiration ? digest : void 0;
6213
- }
6214
- function isRecord(value) {
6215
- return typeof value === "object" && value !== null && !Array.isArray(value);
6216
- }
6217
-
6218
- // src/worker/crypto.ts
6219
- async function createEncryptionKeyring(currentSecret, previousSecret) {
6220
- if (!currentSecret) throw new Error("SESSION_ENCRYPTION_KEY is required.");
6221
- const current = await deriveEncryptionKey(currentSecret);
6222
- const keys = /* @__PURE__ */ new Map([[current.id, current.key]]);
6223
- if (previousSecret) {
6224
- const previous = await deriveEncryptionKey(previousSecret);
6225
- keys.set(previous.id, previous.key);
6226
- }
6227
- return { current, keys };
6228
- }
6229
- async function encryptValue(value, keyring) {
6230
- const iv = crypto.getRandomValues(new Uint8Array(12));
6231
- const ciphertext = await crypto.subtle.encrypt(
6232
- { name: "AES-GCM", iv },
6233
- keyring.current.key,
6234
- new TextEncoder().encode(value)
6235
- );
6236
- return JSON.stringify({
6237
- version: 1,
6238
- keyId: keyring.current.id,
6239
- iv: toBase64Url(iv),
6240
- ciphertext: toBase64Url(new Uint8Array(ciphertext))
6241
- });
6242
- }
6243
- async function decryptValue(encrypted, keyring) {
6244
- const envelope = parseEnvelope(encrypted);
6245
- const key = keyring.keys.get(envelope.keyId);
6246
- if (!key) throw new Error("The session encryption key is unavailable.");
6247
- const plaintext = await crypto.subtle.decrypt(
6248
- { name: "AES-GCM", iv: fromBase64Url(envelope.iv) },
6249
- key,
6250
- fromBase64Url(envelope.ciphertext)
6251
- );
6252
- return {
6253
- value: new TextDecoder().decode(plaintext),
6254
- keyId: envelope.keyId,
6255
- needsRotation: envelope.keyId !== keyring.current.id
6256
- };
6257
- }
6258
- async function deriveEncryptionKey(secret) {
6259
- const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret)));
6260
- const key = await crypto.subtle.importKey("raw", digest, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
6261
- const id = Array.from(digest.slice(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("");
6262
- return { id, key };
6263
- }
6264
- function parseEnvelope(value) {
6265
- let parsed;
6266
- try {
6267
- parsed = JSON.parse(value);
6268
- } catch {
6269
- throw new Error("The encrypted session record is invalid.");
6270
- }
6271
- if (!isRecord2(parsed) || parsed.version !== 1 || typeof parsed.keyId !== "string" || typeof parsed.iv !== "string" || typeof parsed.ciphertext !== "string") {
6272
- throw new Error("The encrypted session record is invalid.");
6273
- }
6274
- return parsed;
6275
- }
6276
- function toBase64Url(bytes) {
6277
- let binary = "";
6278
- for (const byte of bytes) binary += String.fromCharCode(byte);
6279
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
6280
- }
6281
- function fromBase64Url(value) {
6282
- const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
6283
- const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
6284
- const bytes = new Uint8Array(binary.length);
6285
- for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
6286
- return bytes;
6287
- }
6288
- function isRecord2(value) {
6289
- return typeof value === "object" && value !== null && !Array.isArray(value);
6290
- }
6291
-
6292
5998
  // node_modules/zod/v4/classic/external.js
6293
5999
  var external_exports = {};
6294
6000
  __export(external_exports, {
@@ -20678,131 +20384,553 @@ function convertBaseSchema(schema2, ctx) {
20678
20384
  if (typeof schema2.maxItems === "number") {
20679
20385
  arraySchema = arraySchema.max(schema2.maxItems);
20680
20386
  }
20681
- zodSchema = arraySchema;
20682
- } else {
20683
- zodSchema = z.array(z.any());
20387
+ zodSchema = arraySchema;
20388
+ } else {
20389
+ zodSchema = z.array(z.any());
20390
+ }
20391
+ break;
20392
+ }
20393
+ default:
20394
+ throw new Error(`Unsupported type: ${type}`);
20395
+ }
20396
+ return zodSchema;
20397
+ }
20398
+ function convertSchema(schema2, ctx) {
20399
+ if (typeof schema2 === "boolean") {
20400
+ return schema2 ? z.any() : z.never();
20401
+ }
20402
+ let baseSchema = convertBaseSchema(schema2, ctx);
20403
+ const hasExplicitType = schema2.type || schema2.enum !== void 0 || schema2.const !== void 0;
20404
+ if (schema2.anyOf && Array.isArray(schema2.anyOf)) {
20405
+ const options = schema2.anyOf.map((s) => convertSchema(s, ctx));
20406
+ const anyOfUnion = z.union(options);
20407
+ baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion;
20408
+ }
20409
+ if (schema2.oneOf && Array.isArray(schema2.oneOf)) {
20410
+ const options = schema2.oneOf.map((s) => convertSchema(s, ctx));
20411
+ const oneOfUnion = z.xor(options);
20412
+ baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion;
20413
+ }
20414
+ if (schema2.allOf && Array.isArray(schema2.allOf)) {
20415
+ if (schema2.allOf.length === 0) {
20416
+ baseSchema = hasExplicitType ? baseSchema : z.any();
20417
+ } else {
20418
+ let result = hasExplicitType ? baseSchema : convertSchema(schema2.allOf[0], ctx);
20419
+ const startIdx = hasExplicitType ? 0 : 1;
20420
+ for (let i = startIdx; i < schema2.allOf.length; i++) {
20421
+ result = z.intersection(result, convertSchema(schema2.allOf[i], ctx));
20422
+ }
20423
+ baseSchema = result;
20424
+ }
20425
+ }
20426
+ if (schema2.nullable === true && ctx.version === "openapi-3.0") {
20427
+ baseSchema = z.nullable(baseSchema);
20428
+ }
20429
+ if (schema2.readOnly === true) {
20430
+ baseSchema = z.readonly(baseSchema);
20431
+ }
20432
+ if (schema2.default !== void 0) {
20433
+ baseSchema = baseSchema.default(schema2.default);
20434
+ }
20435
+ const extraMeta = {};
20436
+ const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
20437
+ for (const key of coreMetadataKeys) {
20438
+ if (key in schema2) {
20439
+ extraMeta[key] = schema2[key];
20440
+ }
20441
+ }
20442
+ const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
20443
+ for (const key of contentMetadataKeys) {
20444
+ if (key in schema2) {
20445
+ extraMeta[key] = schema2[key];
20446
+ }
20447
+ }
20448
+ for (const key of Object.keys(schema2)) {
20449
+ if (!RECOGNIZED_KEYS.has(key)) {
20450
+ extraMeta[key] = schema2[key];
20451
+ }
20452
+ }
20453
+ if (Object.keys(extraMeta).length > 0) {
20454
+ ctx.registry.add(baseSchema, extraMeta);
20455
+ }
20456
+ if (schema2.description) {
20457
+ baseSchema = baseSchema.describe(schema2.description);
20458
+ }
20459
+ return baseSchema;
20460
+ }
20461
+ function fromJSONSchema(schema2, params) {
20462
+ if (typeof schema2 === "boolean") {
20463
+ return schema2 ? z.any() : z.never();
20464
+ }
20465
+ let normalized;
20466
+ try {
20467
+ normalized = JSON.parse(JSON.stringify(schema2));
20468
+ } catch {
20469
+ throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
20470
+ }
20471
+ const version2 = detectVersion(normalized, params?.defaultTarget);
20472
+ const defs = normalized.$defs || normalized.definitions || {};
20473
+ const ctx = {
20474
+ version: version2,
20475
+ defs,
20476
+ refs: /* @__PURE__ */ new Map(),
20477
+ processing: /* @__PURE__ */ new Set(),
20478
+ rootSchema: normalized,
20479
+ registry: params?.registry ?? globalRegistry
20480
+ };
20481
+ return convertSchema(normalized, ctx);
20482
+ }
20483
+
20484
+ // node_modules/zod/v4/classic/coerce.js
20485
+ var coerce_exports = {};
20486
+ __export(coerce_exports, {
20487
+ bigint: () => bigint3,
20488
+ boolean: () => boolean3,
20489
+ date: () => date4,
20490
+ number: () => number3,
20491
+ string: () => string3
20492
+ });
20493
+ function string3(params) {
20494
+ return _coercedString(ZodString, params);
20495
+ }
20496
+ function number3(params) {
20497
+ return _coercedNumber(ZodNumber, params);
20498
+ }
20499
+ function boolean3(params) {
20500
+ return _coercedBoolean(ZodBoolean, params);
20501
+ }
20502
+ function bigint3(params) {
20503
+ return _coercedBigint(ZodBigInt, params);
20504
+ }
20505
+ function date4(params) {
20506
+ return _coercedDate(ZodDate, params);
20507
+ }
20508
+
20509
+ // node_modules/zod/v4/classic/external.js
20510
+ config(en_default());
20511
+
20512
+ // src/mcp/protocol.ts
20513
+ var MODERN_PROTOCOL_VERSION = "2026-07-28";
20514
+ var LEGACY_PROTOCOL_VERSION = "2025-11-25";
20515
+ var SUPPORTED_PROTOCOL_VERSIONS = [MODERN_PROTOCOL_VERSION, LEGACY_PROTOCOL_VERSION];
20516
+ var JsonRpcRequestSchema = external_exports.object({
20517
+ jsonrpc: external_exports.literal("2.0"),
20518
+ id: external_exports.union([external_exports.string(), external_exports.number(), external_exports.null()]).optional(),
20519
+ method: external_exports.string().trim().min(1),
20520
+ params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
20521
+ }).strict();
20522
+ var ModernClientMetadataSchema = external_exports.object({
20523
+ "io.modelcontextprotocol/clientCapabilities": external_exports.record(external_exports.string(), external_exports.unknown()),
20524
+ "io.modelcontextprotocol/clientInfo": external_exports.object({
20525
+ name: external_exports.string().trim().min(1),
20526
+ version: external_exports.string().trim().min(1)
20527
+ }).passthrough(),
20528
+ "io.modelcontextprotocol/protocolVersion": external_exports.literal(MODERN_PROTOCOL_VERSION)
20529
+ }).passthrough();
20530
+ var UnsupportedProtocolVersionError = class extends Error {
20531
+ protocolVersion;
20532
+ supportedVersions = SUPPORTED_PROTOCOL_VERSIONS;
20533
+ constructor(protocolVersion) {
20534
+ super(`Unsupported MCP protocol version: ${protocolVersion}`);
20535
+ this.name = "UnsupportedProtocolVersionError";
20536
+ this.protocolVersion = protocolVersion;
20537
+ }
20538
+ };
20539
+ var RequestMetadataMismatchError = class extends Error {
20540
+ field;
20541
+ expected;
20542
+ actual;
20543
+ constructor(field, expected, actual) {
20544
+ super(`MCP request metadata mismatch for ${field}.`);
20545
+ this.name = "RequestMetadataMismatchError";
20546
+ this.field = field;
20547
+ this.expected = expected;
20548
+ this.actual = actual;
20549
+ }
20550
+ };
20551
+ function parseJsonRpcRequest(input) {
20552
+ return JsonRpcRequestSchema.parse(input);
20553
+ }
20554
+ function resolveProtocolVersion(request, context = {}) {
20555
+ const meta3 = isRecord(request.params?._meta) ? request.params._meta : void 0;
20556
+ const initializeVersion = request.method === "initialize" && typeof request.params?.protocolVersion === "string" ? request.params.protocolVersion : void 0;
20557
+ const requested = context.protocolVersion ?? stringValue(meta3?.["io.modelcontextprotocol/protocolVersion"]) ?? initializeVersion ?? MODERN_PROTOCOL_VERSION;
20558
+ if (!isSupportedProtocolVersion(requested)) {
20559
+ throw new UnsupportedProtocolVersionError(requested);
20560
+ }
20561
+ return requested;
20562
+ }
20563
+ function assertRequestMetadata(request, protocolVersion, context = {}) {
20564
+ const meta3 = isRecord(request.params?._meta) ? request.params._meta : void 0;
20565
+ const metaVersion = stringValue(meta3?.["io.modelcontextprotocol/protocolVersion"]);
20566
+ if (protocolVersion === MODERN_PROTOCOL_VERSION) {
20567
+ const parsed = ModernClientMetadataSchema.safeParse(meta3);
20568
+ if (!parsed.success) {
20569
+ throw new RequestMetadataMismatchError("_meta", "valid modern client metadata");
20570
+ }
20571
+ }
20572
+ if (context.protocolVersion && metaVersion && context.protocolVersion !== metaVersion) {
20573
+ throw new RequestMetadataMismatchError("protocolVersion", context.protocolVersion, metaVersion);
20574
+ }
20575
+ if (context.method && context.method !== request.method) {
20576
+ throw new RequestMetadataMismatchError("method", context.method, request.method);
20577
+ }
20578
+ const toolName = typeof request.params?.name === "string" ? request.params.name : void 0;
20579
+ if (context.toolName && context.toolName !== toolName) {
20580
+ throw new RequestMetadataMismatchError("name", context.toolName, toolName);
20581
+ }
20582
+ }
20583
+ function isSupportedProtocolVersion(value) {
20584
+ return SUPPORTED_PROTOCOL_VERSIONS.some((version2) => version2 === value);
20585
+ }
20586
+ function jsonRpcSuccess(id, result) {
20587
+ return { jsonrpc: "2.0", id, result };
20588
+ }
20589
+ function jsonRpcFailure(id, error51) {
20590
+ return { jsonrpc: "2.0", id, error: error51 };
20591
+ }
20592
+ function isRecord(value) {
20593
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20594
+ }
20595
+ function stringValue(value) {
20596
+ return typeof value === "string" ? value : void 0;
20597
+ }
20598
+
20599
+ // src/version.ts
20600
+ var VERSION = "0.7.0-alpha.5";
20601
+
20602
+ // src/worker/http.ts
20603
+ var HEALTH_PATH = "/healthz";
20604
+ var READY_PATH = "/readyz";
20605
+ var MCP_PATH = "/mcp";
20606
+ var SESSION_PATH = "/session";
20607
+ var SESSION_TOUCH_PATH = "/session/touch";
20608
+ var WORKER_SERVICE_ID = "moodle-mcp";
20609
+ var WORKER_SERVICE_VERSION = VERSION;
20610
+ function createWorkerHandler(dependencies) {
20611
+ return {
20612
+ async fetch(request, env) {
20613
+ const url2 = new URL(request.url);
20614
+ const authorityProblem = validateRequestAuthority(request, url2, env.EXPECTED_HOSTS ?? env.EXPECTED_HOST);
20615
+ if (authorityProblem) return authorityProblem;
20616
+ if (hasQueryCredential(url2)) return unauthorized("Bearer credentials are not accepted in the query string.");
20617
+ if (url2.pathname === HEALTH_PATH && request.method === "GET") {
20618
+ return Response.json(
20619
+ { status: "pass", serviceId: WORKER_SERVICE_ID, version: WORKER_SERVICE_VERSION },
20620
+ { headers: { "content-type": "application/health+json; charset=utf-8" } }
20621
+ );
20622
+ }
20623
+ if (url2.pathname === MCP_PATH && request.method !== "POST") {
20624
+ return problemResponse(405, "METHOD_NOT_ALLOWED", "Method Not Allowed", "The MCP transport accepts POST requests only.", {
20625
+ allow: "POST"
20626
+ });
20627
+ }
20628
+ if (url2.pathname === MCP_PATH) {
20629
+ if (!await verifyBearerToken(request.headers.get("authorization"), [
20630
+ env.MCP_ACCESS_TOKEN_DIGEST,
20631
+ activePreviousDigest(env.MCP_ACCESS_TOKEN_PREVIOUS_DIGEST, env.TOKEN_OVERLAP_EXPIRES_AT)
20632
+ ])) {
20633
+ return unauthorized();
20634
+ }
20635
+ const server = typeof dependencies.mcpServer === "function" ? dependencies.mcpServer(env) : dependencies.mcpServer;
20636
+ return handleMcpRequest(request, server);
20684
20637
  }
20685
- break;
20638
+ if (url2.pathname === READY_PATH && request.method === "GET") {
20639
+ if (!await authorizeSessionSync(request, env)) return unauthorized();
20640
+ const broker = resolveBroker(dependencies, env);
20641
+ if (broker instanceof Response) return broker;
20642
+ return broker.ready();
20643
+ }
20644
+ if (url2.pathname === SESSION_PATH && request.method === "PUT") {
20645
+ if (!await authorizeSessionSync(request, env)) return unauthorized();
20646
+ const broker = resolveBroker(dependencies, env);
20647
+ if (broker instanceof Response) return broker;
20648
+ return broker.replaceSession(request);
20649
+ }
20650
+ if (url2.pathname === SESSION_TOUCH_PATH && request.method === "POST") {
20651
+ if (!await authorizeSessionSync(request, env)) return unauthorized();
20652
+ const broker = resolveBroker(dependencies, env);
20653
+ if (broker instanceof Response) return broker;
20654
+ return broker.touch();
20655
+ }
20656
+ if (url2.pathname === READY_PATH || url2.pathname === SESSION_PATH || url2.pathname === SESSION_TOUCH_PATH) {
20657
+ const method = url2.pathname === READY_PATH ? "GET" : url2.pathname === SESSION_PATH ? "PUT" : "POST";
20658
+ return problemResponse(405, "METHOD_NOT_ALLOWED", "Method Not Allowed", "The session route does not accept this method.", {
20659
+ allow: method
20660
+ });
20661
+ }
20662
+ return problemResponse(404, "NOT_FOUND", "Not Found", "The requested route does not exist.");
20686
20663
  }
20687
- default:
20688
- throw new Error(`Unsupported type: ${type}`);
20689
- }
20690
- return zodSchema;
20664
+ };
20691
20665
  }
20692
- function convertSchema(schema2, ctx) {
20693
- if (typeof schema2 === "boolean") {
20694
- return schema2 ? z.any() : z.never();
20695
- }
20696
- let baseSchema = convertBaseSchema(schema2, ctx);
20697
- const hasExplicitType = schema2.type || schema2.enum !== void 0 || schema2.const !== void 0;
20698
- if (schema2.anyOf && Array.isArray(schema2.anyOf)) {
20699
- const options = schema2.anyOf.map((s) => convertSchema(s, ctx));
20700
- const anyOfUnion = z.union(options);
20701
- baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion;
20666
+ function createDurableObjectBrokerApi(namespace) {
20667
+ const stub = namespace.get(namespace.idFromName("primary"));
20668
+ return {
20669
+ async handleMcp(body, context) {
20670
+ const response = await stub.fetch(new Request("https://session-broker/mcp", {
20671
+ method: "POST",
20672
+ headers: { "content-type": "application/json" },
20673
+ body: JSON.stringify({ request: body, context })
20674
+ }));
20675
+ if (!response.ok) {
20676
+ const problem = await safeProblem(response);
20677
+ throw new SessionBrokerMcpError(response.status, problem?.code);
20678
+ }
20679
+ const envelope = await response.json();
20680
+ return envelope.response ?? null;
20681
+ },
20682
+ ready: () => stub.fetch(new Request("https://session-broker/readyz")),
20683
+ async replaceSession(request) {
20684
+ return stub.fetch(new Request("https://session-broker/session", {
20685
+ method: "PUT",
20686
+ headers: { "content-type": request.headers.get("content-type") ?? "application/json" },
20687
+ body: await request.arrayBuffer()
20688
+ }));
20689
+ },
20690
+ touch: () => stub.fetch(new Request("https://session-broker/session/touch", { method: "POST" }))
20691
+ };
20692
+ }
20693
+ var SessionBrokerMcpError = class extends Error {
20694
+ constructor(status, code) {
20695
+ super("The session broker could not complete the MCP request.");
20696
+ this.status = status;
20697
+ this.code = code;
20698
+ this.name = "SessionBrokerMcpError";
20702
20699
  }
20703
- if (schema2.oneOf && Array.isArray(schema2.oneOf)) {
20704
- const options = schema2.oneOf.map((s) => convertSchema(s, ctx));
20705
- const oneOfUnion = z.xor(options);
20706
- baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion;
20700
+ status;
20701
+ code;
20702
+ };
20703
+ function resolveBroker(dependencies, env) {
20704
+ if (dependencies.broker) return dependencies.broker(env);
20705
+ if (env.SESSION_BROKER) return createDurableObjectBrokerApi(env.SESSION_BROKER);
20706
+ return problemResponse(503, "SESSION_BROKER_UNAVAILABLE", "Service Unavailable", "The session broker is unavailable.");
20707
+ }
20708
+ function validateRequestAuthority(request, url2, configuredHosts) {
20709
+ const expectedHosts = (configuredHosts ?? url2.host).split(",").map((host) => host.trim().toLowerCase()).filter(Boolean);
20710
+ const requestHost = (request.headers.get("host") ?? url2.host).toLowerCase();
20711
+ if (requestHost !== url2.host.toLowerCase() || !expectedHosts.includes(requestHost)) {
20712
+ return problemResponse(403, "INVALID_HOST", "Forbidden", "The request Host is not allowed.");
20707
20713
  }
20708
- if (schema2.allOf && Array.isArray(schema2.allOf)) {
20709
- if (schema2.allOf.length === 0) {
20710
- baseSchema = hasExplicitType ? baseSchema : z.any();
20711
- } else {
20712
- let result = hasExplicitType ? baseSchema : convertSchema(schema2.allOf[0], ctx);
20713
- const startIdx = hasExplicitType ? 0 : 1;
20714
- for (let i = startIdx; i < schema2.allOf.length; i++) {
20715
- result = z.intersection(result, convertSchema(schema2.allOf[i], ctx));
20716
- }
20717
- baseSchema = result;
20714
+ const origin = request.headers.get("origin");
20715
+ if (!origin) return null;
20716
+ try {
20717
+ const parsed = new URL(origin);
20718
+ if (parsed.origin !== url2.origin || !expectedHosts.includes(parsed.host.toLowerCase())) {
20719
+ return problemResponse(403, "INVALID_ORIGIN", "Forbidden", "The request Origin is not allowed.");
20718
20720
  }
20721
+ } catch {
20722
+ return problemResponse(403, "INVALID_ORIGIN", "Forbidden", "The request Origin is not allowed.");
20719
20723
  }
20720
- if (schema2.nullable === true && ctx.version === "openapi-3.0") {
20721
- baseSchema = z.nullable(baseSchema);
20722
- }
20723
- if (schema2.readOnly === true) {
20724
- baseSchema = z.readonly(baseSchema);
20724
+ return null;
20725
+ }
20726
+ function unauthorized(detail = "A valid Bearer token is required.") {
20727
+ return problemResponse(401, "INVALID_BEARER_TOKEN", "Unauthorized", detail, {
20728
+ "www-authenticate": 'Bearer realm="moodle-mcp"'
20729
+ });
20730
+ }
20731
+ async function handleMcpRequest(request, server) {
20732
+ if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
20733
+ return problemResponse(415, "UNSUPPORTED_MEDIA_TYPE", "Unsupported Media Type", "MCP requests must use application/json.");
20725
20734
  }
20726
- if (schema2.default !== void 0) {
20727
- baseSchema = baseSchema.default(schema2.default);
20735
+ let body;
20736
+ try {
20737
+ body = await request.json();
20738
+ } catch {
20739
+ return problemResponse(400, "INVALID_JSON", "Bad Request", "The request body is not valid JSON.");
20728
20740
  }
20729
- const extraMeta = {};
20730
- const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
20731
- for (const key of coreMetadataKeys) {
20732
- if (key in schema2) {
20733
- extraMeta[key] = schema2[key];
20741
+ const protocolVersion = request.headers.get("mcp-protocol-version");
20742
+ const metadataProblem = validateProtocolMetadata(request.headers, body, protocolVersion);
20743
+ if (metadataProblem) return metadataProblem;
20744
+ try {
20745
+ const response = await server.handle(body, {
20746
+ protocolVersion,
20747
+ method: request.headers.get("mcp-method") ?? void 0,
20748
+ toolName: request.headers.get("mcp-name") ?? void 0
20749
+ });
20750
+ if (response === null) return new Response(null, { status: 202 });
20751
+ if (isUnsupportedProtocolVersionResponse(response)) {
20752
+ return Response.json(response, {
20753
+ status: 400,
20754
+ headers: { "cache-control": "private, no-store" }
20755
+ });
20734
20756
  }
20735
- }
20736
- const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
20737
- for (const key of contentMetadataKeys) {
20738
- if (key in schema2) {
20739
- extraMeta[key] = schema2[key];
20757
+ if (request.headers.get("accept")?.toLowerCase().includes("text/event-stream")) {
20758
+ return new Response(`event: message
20759
+ data: ${JSON.stringify(response)}
20760
+
20761
+ `, {
20762
+ headers: {
20763
+ "cache-control": "private, no-store",
20764
+ "content-type": "text/event-stream; charset=utf-8"
20765
+ }
20766
+ });
20740
20767
  }
20741
- }
20742
- for (const key of Object.keys(schema2)) {
20743
- if (!RECOGNIZED_KEYS.has(key)) {
20744
- extraMeta[key] = schema2[key];
20768
+ return Response.json(response, { headers: { "cache-control": "private, no-store" } });
20769
+ } catch (error51) {
20770
+ if (error51 instanceof SessionBrokerMcpError) {
20771
+ const status = error51.status === 503 ? 503 : 500;
20772
+ const code = error51.code === "SESSION_MISSING" || error51.code === "SESSION_EXPIRED" ? error51.code : "SESSION_UNAVAILABLE";
20773
+ return problemResponse(status, code, "Service Unavailable", "The Moodle session is not ready.");
20745
20774
  }
20775
+ if (error51 instanceof Error && error51.name === "UnsupportedProtocolVersionError") {
20776
+ const requested = "protocolVersion" in error51 && typeof error51.protocolVersion === "string" ? error51.protocolVersion : protocolVersion ?? "unknown";
20777
+ return Response.json({
20778
+ jsonrpc: "2.0",
20779
+ id: jsonRpcRequestId(body),
20780
+ error: {
20781
+ code: -32022,
20782
+ message: "Unsupported protocol version",
20783
+ data: { supported: [...SUPPORTED_PROTOCOL_VERSIONS], requested }
20784
+ }
20785
+ }, {
20786
+ status: 400,
20787
+ headers: { "cache-control": "private, no-store" }
20788
+ });
20789
+ }
20790
+ return problemResponse(500, "MCP_REQUEST_FAILED", "Internal Server Error", "The MCP request could not be completed.");
20746
20791
  }
20747
- if (Object.keys(extraMeta).length > 0) {
20748
- ctx.registry.add(baseSchema, extraMeta);
20749
- }
20750
- if (schema2.description) {
20751
- baseSchema = baseSchema.describe(schema2.description);
20752
- }
20753
- return baseSchema;
20754
20792
  }
20755
- function fromJSONSchema(schema2, params) {
20756
- if (typeof schema2 === "boolean") {
20757
- return schema2 ? z.any() : z.never();
20758
- }
20759
- let normalized;
20793
+ async function safeProblem(response) {
20760
20794
  try {
20761
- normalized = JSON.parse(JSON.stringify(schema2));
20795
+ const value = await response.json();
20796
+ return isRecord2(value) && (value.code === void 0 || typeof value.code === "string") ? value : null;
20762
20797
  } catch {
20763
- throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
20798
+ return null;
20799
+ }
20800
+ }
20801
+ function validateProtocolMetadata(headers, body, protocolVersion) {
20802
+ if (!protocolVersion) {
20803
+ return headerMismatchResponse(body, "MCP-Protocol-Version is required.");
20804
+ }
20805
+ const params = isRecord2(body.params) ? body.params : void 0;
20806
+ const metadata = params && isRecord2(params._meta) ? params._meta : void 0;
20807
+ const bodyVersion = metadata?.["io.modelcontextprotocol/protocolVersion"];
20808
+ if (bodyVersion !== void 0 && bodyVersion !== protocolVersion) {
20809
+ return headerMismatchResponse(body, "MCP protocol metadata does not match the HTTP headers.");
20810
+ }
20811
+ if (typeof body.method !== "string") {
20812
+ return problemResponse(400, "MCP_PROTOCOL_METADATA_INVALID", "Bad Request", "The JSON-RPC method is required.");
20813
+ }
20814
+ const headerMethod = headers.get("mcp-method");
20815
+ const headerName = headers.get("mcp-name") ?? void 0;
20816
+ const paramsName = params && typeof params.name === "string" ? params.name : void 0;
20817
+ const requiresModernHeaders = protocolVersion !== LEGACY_PROTOCOL_VERSION;
20818
+ const hasModernHeaders = headerMethod !== null || headerName !== void 0;
20819
+ if ((requiresModernHeaders || hasModernHeaders) && (headerMethod !== body.method || headerName !== paramsName)) {
20820
+ return headerMismatchResponse(body, "MCP method metadata does not match the JSON-RPC request.");
20821
+ }
20822
+ return null;
20823
+ }
20824
+ function headerMismatchResponse(body, detail) {
20825
+ return Response.json({
20826
+ jsonrpc: "2.0",
20827
+ id: jsonRpcRequestId(body),
20828
+ error: {
20829
+ code: -32020,
20830
+ message: "HeaderMismatch",
20831
+ data: { detail }
20832
+ }
20833
+ }, {
20834
+ status: 400,
20835
+ headers: { "cache-control": "private, no-store" }
20836
+ });
20837
+ }
20838
+ function jsonRpcRequestId(body) {
20839
+ return typeof body.id === "string" || typeof body.id === "number" || body.id === null ? body.id : null;
20840
+ }
20841
+ function isUnsupportedProtocolVersionResponse(value) {
20842
+ if (!isRecord2(value) || !isRecord2(value.error)) return false;
20843
+ return value.jsonrpc === "2.0" && value.error.code === -32022;
20844
+ }
20845
+ function authorizeSessionSync(request, env) {
20846
+ return verifyBearerToken(request.headers.get("authorization"), [
20847
+ env.SESSION_SYNC_TOKEN_DIGEST,
20848
+ activePreviousDigest(env.SESSION_SYNC_TOKEN_PREVIOUS_DIGEST, env.TOKEN_OVERLAP_EXPIRES_AT)
20849
+ ]);
20850
+ }
20851
+ function activePreviousDigest(digest, expiresAt2) {
20852
+ if (!digest || !expiresAt2) return void 0;
20853
+ const expiration = Number(expiresAt2);
20854
+ return Number.isFinite(expiration) && Date.now() < expiration ? digest : void 0;
20855
+ }
20856
+ function isRecord2(value) {
20857
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20858
+ }
20859
+
20860
+ // src/worker/crypto.ts
20861
+ async function createEncryptionKeyring(currentSecret, previousSecret) {
20862
+ if (!currentSecret) throw new Error("SESSION_ENCRYPTION_KEY is required.");
20863
+ const current = await deriveEncryptionKey(currentSecret);
20864
+ const keys = /* @__PURE__ */ new Map([[current.id, current.key]]);
20865
+ if (previousSecret) {
20866
+ const previous = await deriveEncryptionKey(previousSecret);
20867
+ keys.set(previous.id, previous.key);
20764
20868
  }
20765
- const version2 = detectVersion(normalized, params?.defaultTarget);
20766
- const defs = normalized.$defs || normalized.definitions || {};
20767
- const ctx = {
20768
- version: version2,
20769
- defs,
20770
- refs: /* @__PURE__ */ new Map(),
20771
- processing: /* @__PURE__ */ new Set(),
20772
- rootSchema: normalized,
20773
- registry: params?.registry ?? globalRegistry
20869
+ return { current, keys };
20870
+ }
20871
+ async function encryptValue(value, keyring) {
20872
+ const iv = crypto.getRandomValues(new Uint8Array(12));
20873
+ const ciphertext = await crypto.subtle.encrypt(
20874
+ { name: "AES-GCM", iv },
20875
+ keyring.current.key,
20876
+ new TextEncoder().encode(value)
20877
+ );
20878
+ return JSON.stringify({
20879
+ version: 1,
20880
+ keyId: keyring.current.id,
20881
+ iv: toBase64Url(iv),
20882
+ ciphertext: toBase64Url(new Uint8Array(ciphertext))
20883
+ });
20884
+ }
20885
+ async function decryptValue(encrypted, keyring) {
20886
+ const envelope = parseEnvelope(encrypted);
20887
+ const key = keyring.keys.get(envelope.keyId);
20888
+ if (!key) throw new Error("The session encryption key is unavailable.");
20889
+ const plaintext = await crypto.subtle.decrypt(
20890
+ { name: "AES-GCM", iv: fromBase64Url(envelope.iv) },
20891
+ key,
20892
+ fromBase64Url(envelope.ciphertext)
20893
+ );
20894
+ return {
20895
+ value: new TextDecoder().decode(plaintext),
20896
+ keyId: envelope.keyId,
20897
+ needsRotation: envelope.keyId !== keyring.current.id
20774
20898
  };
20775
- return convertSchema(normalized, ctx);
20776
20899
  }
20777
-
20778
- // node_modules/zod/v4/classic/coerce.js
20779
- var coerce_exports = {};
20780
- __export(coerce_exports, {
20781
- bigint: () => bigint3,
20782
- boolean: () => boolean3,
20783
- date: () => date4,
20784
- number: () => number3,
20785
- string: () => string3
20786
- });
20787
- function string3(params) {
20788
- return _coercedString(ZodString, params);
20900
+ async function deriveEncryptionKey(secret) {
20901
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret)));
20902
+ const key = await crypto.subtle.importKey("raw", digest, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
20903
+ const id = Array.from(digest.slice(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("");
20904
+ return { id, key };
20789
20905
  }
20790
- function number3(params) {
20791
- return _coercedNumber(ZodNumber, params);
20906
+ function parseEnvelope(value) {
20907
+ let parsed;
20908
+ try {
20909
+ parsed = JSON.parse(value);
20910
+ } catch {
20911
+ throw new Error("The encrypted session record is invalid.");
20912
+ }
20913
+ if (!isRecord3(parsed) || parsed.version !== 1 || typeof parsed.keyId !== "string" || typeof parsed.iv !== "string" || typeof parsed.ciphertext !== "string") {
20914
+ throw new Error("The encrypted session record is invalid.");
20915
+ }
20916
+ return parsed;
20792
20917
  }
20793
- function boolean3(params) {
20794
- return _coercedBoolean(ZodBoolean, params);
20918
+ function toBase64Url(bytes) {
20919
+ let binary = "";
20920
+ for (const byte of bytes) binary += String.fromCharCode(byte);
20921
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
20795
20922
  }
20796
- function bigint3(params) {
20797
- return _coercedBigint(ZodBigInt, params);
20923
+ function fromBase64Url(value) {
20924
+ const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
20925
+ const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
20926
+ const bytes = new Uint8Array(binary.length);
20927
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
20928
+ return bytes;
20798
20929
  }
20799
- function date4(params) {
20800
- return _coercedDate(ZodDate, params);
20930
+ function isRecord3(value) {
20931
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20801
20932
  }
20802
20933
 
20803
- // node_modules/zod/v4/classic/external.js
20804
- config(en_default());
20805
-
20806
20934
  // src/constants.ts
20807
20935
  var PACKAGE_NAME = "moodle-cli";
20808
20936
  var NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
@@ -20917,19 +21045,19 @@ function parseUserInfo(value) {
20917
21045
  const data = asRecord(value);
20918
21046
  return {
20919
21047
  userid: numberValue(data.userid),
20920
- username: stringValue(data.username),
20921
- fullname: stringValue(data.fullname),
20922
- sitename: stringValue(data.sitename),
20923
- siteurl: stringValue(data.siteurl),
20924
- lang: stringValue(data.lang)
21048
+ username: stringValue2(data.username),
21049
+ fullname: stringValue2(data.fullname),
21050
+ sitename: stringValue2(data.sitename),
21051
+ siteurl: stringValue2(data.siteurl),
21052
+ lang: stringValue2(data.lang)
20925
21053
  };
20926
21054
  }
20927
21055
  function parseCourse(value, nowSeconds = Math.floor(Date.now() / 1e3)) {
20928
21056
  const data = asRecord(value);
20929
21057
  const course = {
20930
21058
  id: numberValue(data.id),
20931
- shortname: stringValue(data.shortname),
20932
- fullname: stringValue(data.fullname),
21059
+ shortname: stringValue2(data.shortname),
21060
+ fullname: stringValue2(data.fullname),
20933
21061
  category: numberValue(data.category),
20934
21062
  visible: booleanValue(data.visible, true),
20935
21063
  startdate: numberValue(data.startdate)
@@ -20947,21 +21075,21 @@ function parseActivity(value) {
20947
21075
  const data = asRecord(value);
20948
21076
  return {
20949
21077
  id: numberValue(data.id),
20950
- name: stringValue(data.name),
20951
- modname: stringValue(data.modname),
20952
- url: stringValue(data.url),
21078
+ name: stringValue2(data.name),
21079
+ modname: stringValue2(data.modname),
21080
+ url: stringValue2(data.url),
20953
21081
  visible: booleanValue(data.visible, true),
20954
- description: stringValue(data.description)
21082
+ description: stringValue2(data.description)
20955
21083
  };
20956
21084
  }
20957
21085
  function parseSection(value) {
20958
21086
  const data = asRecord(value);
20959
21087
  return {
20960
21088
  id: numberValue(data.id),
20961
- name: stringValue(data.name),
21089
+ name: stringValue2(data.name),
20962
21090
  section: numberValue(data.section),
20963
21091
  visible: booleanValue(data.visible, true),
20964
- summary: stringValue(data.summary),
21092
+ summary: stringValue2(data.summary),
20965
21093
  activities: asArray(data.modules).map((item) => parseActivity(item))
20966
21094
  };
20967
21095
  }
@@ -20975,13 +21103,13 @@ function parseCourseFormatState(value, baseUrl) {
20975
21103
  for (const item of asArray(state.cm)) {
20976
21104
  const data = asRecord(item);
20977
21105
  const id = numberValue(data.id);
20978
- const sectionId = stringValue(data.sectionid);
20979
- const module = stringValue(data.module) || stringValue(data.plugin).replace(/^mod_/u, "") || stringValue(data.modname).toLowerCase();
21106
+ const sectionId = stringValue2(data.sectionid);
21107
+ const module = stringValue2(data.module) || stringValue2(data.plugin).replace(/^mod_/u, "") || stringValue2(data.modname).toLowerCase();
20980
21108
  const activity = {
20981
21109
  id,
20982
21110
  name: htmlText(data.name, baseUrl),
20983
21111
  modname: module.toLowerCase(),
20984
- url: stringValue(data.url) ? resolveUrl(baseUrl, stringValue(data.url)) : "",
21112
+ url: stringValue2(data.url) ? resolveUrl(baseUrl, stringValue2(data.url)) : "",
20985
21113
  visible: booleanValue(data.visible, true) && booleanValue(data.uservisible, true) && !booleanValue(data.stealth),
20986
21114
  description: htmlText(data.content ?? data.description, baseUrl)
20987
21115
  };
@@ -20994,7 +21122,7 @@ function parseCourseFormatState(value, baseUrl) {
20994
21122
  const data = asRecord(item);
20995
21123
  const id = numberValue(data.id);
20996
21124
  const hasActivityList = Array.isArray(data.cmlist);
20997
- const listedActivities = asArray(data.cmlist).map((activityId) => activities.get(stringValue(activityId))).filter((activity) => activity !== void 0);
21125
+ const listedActivities = asArray(data.cmlist).map((activityId) => activities.get(stringValue2(activityId))).filter((activity) => activity !== void 0);
20998
21126
  return {
20999
21127
  id,
21000
21128
  name: htmlText(data.title || data.rawtitle, baseUrl),
@@ -21012,18 +21140,18 @@ function parseTodoItem(value) {
21012
21140
  const progress = course.progress;
21013
21141
  return {
21014
21142
  id: numberValue(data.id),
21015
- name: stringValue(data.name),
21016
- activity_name: stringValue(data.activityname),
21017
- modname: stringValue(data.modulename),
21143
+ name: stringValue2(data.name),
21144
+ activity_name: stringValue2(data.activityname),
21145
+ modname: stringValue2(data.modulename),
21018
21146
  course_id: numberValue(course.id),
21019
- course_name: stringValue(course.fullname),
21147
+ course_name: stringValue2(course.fullname),
21020
21148
  due_at: numberValue(data.timesort) || numberValue(data.timestart),
21021
21149
  overdue: booleanValue(data.overdue),
21022
21150
  actionable: booleanValue(action.actionable),
21023
- action_name: stringValue(action.name),
21024
- action_url: stringValue(action.url),
21025
- url: stringValue(data.url),
21026
- event_type: stringValue(data.eventtype),
21151
+ action_name: stringValue2(action.name),
21152
+ action_url: stringValue2(action.url),
21153
+ url: stringValue2(data.url),
21154
+ event_type: stringValue2(data.eventtype),
21027
21155
  course_progress: typeof progress === "number" ? progress : void 0
21028
21156
  };
21029
21157
  }
@@ -21034,15 +21162,15 @@ function parseAlertNotification(value) {
21034
21162
  const data = asRecord(value);
21035
21163
  return {
21036
21164
  id: numberValue(data.id),
21037
- subject: stringValue(data.subject),
21038
- short_subject: stringValue(data.shortenedsubject),
21039
- event_type: stringValue(data.eventtype),
21040
- component: stringValue(data.component),
21165
+ subject: stringValue2(data.subject),
21166
+ short_subject: stringValue2(data.shortenedsubject),
21167
+ event_type: stringValue2(data.eventtype),
21168
+ component: stringValue2(data.component),
21041
21169
  created_at: numberValue(data.timecreated),
21042
- created_pretty: stringValue(data.timecreatedpretty),
21170
+ created_pretty: stringValue2(data.timecreatedpretty),
21043
21171
  read: booleanValue(data.read),
21044
- context_url: stringValue(data.contexturl),
21045
- context_name: stringValue(data.contexturlname)
21172
+ context_url: stringValue2(data.contexturl),
21173
+ context_name: stringValue2(data.contexturlname)
21046
21174
  };
21047
21175
  }
21048
21176
  function parseAlertSummary(notificationsData, countsData, unreadCountsData) {
@@ -21071,20 +21199,20 @@ function parseForumPostAuthor(value) {
21071
21199
  const urls = asRecord(data.urls);
21072
21200
  return {
21073
21201
  id: numberValue(data.id),
21074
- fullname: stringValue(data.fullname),
21075
- profile_url: stringValue(urls.profile),
21076
- profile_image_url: stringValue(urls.profileimage)
21202
+ fullname: stringValue2(data.fullname),
21203
+ profile_url: stringValue2(urls.profile),
21204
+ profile_image_url: stringValue2(urls.profileimage)
21077
21205
  };
21078
21206
  }
21079
21207
  function parseForumPost(value, baseUrl = "") {
21080
21208
  const data = asRecord(value);
21081
21209
  const urls = asRecord(data.urls);
21082
- const messageHtml = stringValue(data.message);
21083
- const structured = htmlToStructuredContent(messageHtml, stringValue(urls.view || urls.discuss) || baseUrl);
21210
+ const messageHtml = stringValue2(data.message);
21211
+ const structured = htmlToStructuredContent(messageHtml, stringValue2(urls.view || urls.discuss) || baseUrl);
21084
21212
  return {
21085
21213
  id: numberValue(data.id),
21086
21214
  discussion_id: numberValue(data.discussionid),
21087
- subject: stringValue(data.subject),
21215
+ subject: stringValue2(data.subject),
21088
21216
  message_html: messageHtml,
21089
21217
  message_text: structured.text,
21090
21218
  image_urls: structured.image_urls,
@@ -21098,8 +21226,8 @@ function parseForumPost(value, baseUrl = "") {
21098
21226
  unread: booleanValue(data.unread),
21099
21227
  is_deleted: booleanValue(data.isdeleted),
21100
21228
  is_private_reply: booleanValue(data.isprivatereply),
21101
- url: stringValue(urls.view || urls.viewisolated),
21102
- reply_url: stringValue(urls.reply)
21229
+ url: stringValue2(urls.view || urls.viewisolated),
21230
+ reply_url: stringValue2(urls.reply)
21103
21231
  };
21104
21232
  }
21105
21233
  function parseForumDiscussion(value, discussionId, baseUrl = "") {
@@ -21111,7 +21239,7 @@ function parseForumDiscussion(value, discussionId, baseUrl = "") {
21111
21239
  course_id: numberValue(data.courseid),
21112
21240
  forum_id: numberValue(data.forumid),
21113
21241
  group_id: numberValue(data.groupid),
21114
- group_name: stringValue(data.groupname),
21242
+ group_name: stringValue2(data.groupname),
21115
21243
  url: posts[0]?.url ? posts[0].url.split("#", 1)[0] : "",
21116
21244
  posts
21117
21245
  };
@@ -21122,7 +21250,7 @@ function asRecord(value) {
21122
21250
  function asArray(value) {
21123
21251
  return Array.isArray(value) ? value : [];
21124
21252
  }
21125
- function stringValue(value) {
21253
+ function stringValue2(value) {
21126
21254
  return typeof value === "string" ? value : value == null ? "" : String(value);
21127
21255
  }
21128
21256
  function numberValue(value) {
@@ -21149,15 +21277,41 @@ function parseJsonValue(value) {
21149
21277
  }
21150
21278
  }
21151
21279
  function htmlText(value, baseUrl) {
21152
- return htmlToStructuredContent(stringValue(value), baseUrl).text;
21280
+ return htmlToStructuredContent(stringValue2(value), baseUrl).text;
21153
21281
  }
21154
21282
 
21155
21283
  // src/scraper.ts
21156
21284
  var import_node_html_parser2 = __toESM(require_dist(), 1);
21285
+ function parseMoodleErrorHtml(html) {
21286
+ const root = (0, import_node_html_parser2.parse)(html);
21287
+ const messageNode = first(root, [
21288
+ ".errormessage",
21289
+ ".alert-danger .alert-message",
21290
+ "[data-region='error-message']",
21291
+ ".alert-danger[role='alert']",
21292
+ ".alert-danger"
21293
+ ]);
21294
+ if (!messageNode) {
21295
+ return null;
21296
+ }
21297
+ const messageRoot = (0, import_node_html_parser2.parse)(messageNode.toString());
21298
+ for (const unwanted of messageRoot.querySelectorAll(
21299
+ "button, .close, .errorcode, .stacktrace, .debuginfo, .backtrace, a.alert-link, a[href*='/error/']"
21300
+ )) {
21301
+ unwanted.remove();
21302
+ }
21303
+ const message = cleanText(messageRoot.textContent);
21304
+ if (!message) {
21305
+ return null;
21306
+ }
21307
+ const errorCodeText = cleanNodeText(root.querySelector(".errorcode"));
21308
+ const errorCode = errorCodeText.match(/^error\s+code\s*:\s*([a-z][a-z0-9_]*)\s*$/iu)?.[1] ?? moodleDocsErrorCode(root);
21309
+ return { message, ...errorCode ? { code: errorCode } : {} };
21310
+ }
21157
21311
  function parsePageContext(html, baseUrl) {
21158
21312
  const root = (0, import_node_html_parser2.parse)(html);
21159
21313
  const config2 = parseMoodleConfig(html);
21160
- const sesskey = stringValue2(config2.sesskey).trim();
21314
+ const sesskey = stringValue3(config2.sesskey).trim();
21161
21315
  const userid = numberValue2(config2.userId) || numberValue2(root.querySelector("[data-user-id]")?.getAttribute("data-user-id"));
21162
21316
  if (!sesskey || !userid) {
21163
21317
  throw new Error("Session appears invalid: could not load authenticated Moodle context");
@@ -21170,7 +21324,7 @@ function parsePageContext(html, baseUrl) {
21170
21324
  fullname: cleanNodeText(root.querySelector(".userfullname")),
21171
21325
  sitename: extractSitename(root),
21172
21326
  siteurl: baseUrl,
21173
- lang: stringValue2(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
21327
+ lang: stringValue3(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
21174
21328
  }
21175
21329
  };
21176
21330
  }
@@ -21240,9 +21394,9 @@ function parseCourseSectionNumbers(html, courseId) {
21240
21394
  for (const href of hrefs) {
21241
21395
  const url2 = parseMaybeUrl(href.replace(/&amp;/gu, "&"), "https://moodle.invalid");
21242
21396
  const id = url2?.searchParams.get("id");
21243
- const sectionValue = url2?.searchParams.get("section");
21244
- if (id === String(courseId) && sectionValue && /^\d+$/.test(sectionValue)) {
21245
- const section = Number(sectionValue);
21397
+ const sectionValue2 = url2?.searchParams.get("section");
21398
+ if (id === String(courseId) && sectionValue2 && /^\d+$/.test(sectionValue2)) {
21399
+ const section = Number(sectionValue2);
21246
21400
  if (!sections.includes(section)) {
21247
21401
  sections.push(section);
21248
21402
  }
@@ -21585,6 +21739,16 @@ function selectedGroupName(root, groupId) {
21585
21739
  }
21586
21740
  return "";
21587
21741
  }
21742
+ function moodleDocsErrorCode(root) {
21743
+ for (const link of root.querySelectorAll("a[href*='/error/']")) {
21744
+ const href = link.getAttribute("href") ?? "";
21745
+ const code = href.match(/\/error\/[^/]+\/([a-z][a-z0-9_]*)/iu)?.[1];
21746
+ if (code) {
21747
+ return code;
21748
+ }
21749
+ }
21750
+ return void 0;
21751
+ }
21588
21752
  function cleanNodeText(node) {
21589
21753
  return cleanText(node?.textContent ?? "");
21590
21754
  }
@@ -21707,7 +21871,7 @@ function numberValue2(value) {
21707
21871
  }
21708
21872
  return 0;
21709
21873
  }
21710
- function stringValue2(value) {
21874
+ function stringValue3(value) {
21711
21875
  return typeof value === "string" ? value : value == null ? "" : String(value);
21712
21876
  }
21713
21877
  function fileEntry(name, url2, baseUrl) {
@@ -21834,13 +21998,13 @@ var ForumModule = class {
21834
21998
  }
21835
21999
  };
21836
22000
  function shouldFallbackForumAjax(error51) {
21837
- if (!isRecord3(error51)) {
22001
+ if (!isRecord4(error51)) {
21838
22002
  return false;
21839
22003
  }
21840
22004
  const code = typeof error51.moodleErrorCode === "string" ? error51.moodleErrorCode : "";
21841
22005
  return code === "servicenotavailable" || code === "accessexception" || error51 instanceof Error && error51.message.includes("Web service is not available");
21842
22006
  }
21843
- function isRecord3(value) {
22007
+ function isRecord4(value) {
21844
22008
  return typeof value === "object" && value !== null && !Array.isArray(value);
21845
22009
  }
21846
22010
 
@@ -22074,7 +22238,7 @@ var MoodleClientCoreApiError = class extends MoodleClientCoreError {
22074
22238
  moodleErrorCode;
22075
22239
  constructor(message, moodleErrorCode) {
22076
22240
  const auth = isLoginErrorCode(moodleErrorCode);
22077
- const notFound = ["invalidrecord", "invalidcoursemodule"].includes(moodleErrorCode ?? "") || /^HTTP 404\b/.test(message);
22241
+ const notFound = ["invalidrecord", "invalidcoursemodule"].includes(moodleErrorCode ?? "") || /\bHTTP 404\b/.test(message);
22078
22242
  super(
22079
22243
  auth ? "auth" : notFound ? "not_found" : "upstream",
22080
22244
  message,
@@ -22141,7 +22305,7 @@ var MoodleClientCore = class {
22141
22305
  await this.ensureSession();
22142
22306
  try {
22143
22307
  const data = await this.call(FUNC_GET_SITE_INFO);
22144
- if (isRecord4(data) && "userid" in data) {
22308
+ if (isRecord5(data) && "userid" in data) {
22145
22309
  const info = parseUserInfo(data);
22146
22310
  this.sesskey = typeof data.sesskey === "string" ? data.sesskey : this.sesskey;
22147
22311
  this.userid = info.userid;
@@ -22226,9 +22390,9 @@ var MoodleClientCore = class {
22226
22390
  let type = "";
22227
22391
  try {
22228
22392
  const data = await this.call(FUNC_GET_COURSE_MODULE, { cmid: id });
22229
- const module = isRecord4(data) && isRecord4(data.cm) ? data.cm : data;
22230
- type = isRecord4(module) && typeof module.modname === "string" ? module.modname : "";
22231
- courseId = isRecord4(module) && typeof module.course === "number" ? module.course : void 0;
22393
+ const module = isRecord5(data) && isRecord5(data.cm) ? data.cm : data;
22394
+ type = isRecord5(module) && typeof module.modname === "string" ? module.modname : "";
22395
+ courseId = isRecord5(module) && typeof module.course === "number" ? module.course : void 0;
22232
22396
  } catch (error51) {
22233
22397
  if (!this.errors.isApi(error51) || error51.moodleErrorCode !== "servicenotavailable") {
22234
22398
  throw error51;
@@ -22261,7 +22425,7 @@ var MoodleClientCore = class {
22261
22425
  aftereventid: 0,
22262
22426
  limittononsuspendedevents: true
22263
22427
  });
22264
- const events = isRecord4(data) && Array.isArray(data.events) ? data.events : [];
22428
+ const events = isRecord5(data) && Array.isArray(data.events) ? data.events : [];
22265
22429
  return parseTodoItems(events);
22266
22430
  }
22267
22431
  async getAlerts(limit = 20) {
@@ -22505,7 +22669,15 @@ var MoodleClientCore = class {
22505
22669
  throw this.errors.api("Session expired", "servicerequireslogin");
22506
22670
  }
22507
22671
  if (!response.ok) {
22508
- throw this.errors.api(`HTTP ${response.status} loading ${safeUrl(url2)}`);
22672
+ const context = `HTTP ${response.status} loading ${safeUrl(url2)}`;
22673
+ const contentType2 = response.headers.get("content-type")?.toLowerCase() ?? "";
22674
+ if (contentType2.includes("text/html") || contentType2.includes("application/xhtml+xml")) {
22675
+ const moodleError = await response.text().then(parseMoodleErrorHtml).catch(() => null);
22676
+ if (moodleError) {
22677
+ throw this.errors.api(`${moodleError.message} (${context})`, moodleError.code);
22678
+ }
22679
+ }
22680
+ throw this.errors.api(context);
22509
22681
  }
22510
22682
  return response;
22511
22683
  }
@@ -22514,7 +22686,7 @@ var MoodleClientCore = class {
22514
22686
  let offset = 0;
22515
22687
  while (true) {
22516
22688
  const data = await this.call(FUNC_GET_COURSES_BY_TIMELINE, { classification: "all", limit: 100, offset });
22517
- if (!isRecord4(data) || !Array.isArray(data.courses) || !data.courses.length) {
22689
+ if (!isRecord5(data) || !Array.isArray(data.courses) || !data.courses.length) {
22518
22690
  break;
22519
22691
  }
22520
22692
  courses.push(...data.courses);
@@ -22679,7 +22851,7 @@ function queryMatches(text, query) {
22679
22851
  return needle ? haystack.includes(needle) || needle.split(" ").every((token) => haystack.includes(token)) : true;
22680
22852
  }
22681
22853
  function parseTodoPayload(value) {
22682
- return parseTodoItems(isRecord4(value) && Array.isArray(value.events) ? value.events : []);
22854
+ return parseTodoItems(isRecord5(value) && Array.isArray(value.events) ? value.events : []);
22683
22855
  }
22684
22856
  function errorMessage(value) {
22685
22857
  return value instanceof Error ? value.message : "Unknown Moodle error";
@@ -22691,7 +22863,7 @@ function chunks(values, size) {
22691
22863
  }
22692
22864
  return result;
22693
22865
  }
22694
- function isRecord4(value) {
22866
+ function isRecord5(value) {
22695
22867
  return !!value && typeof value === "object" && !Array.isArray(value);
22696
22868
  }
22697
22869
  function isLoginErrorCode(code) {
@@ -22714,6 +22886,8 @@ function safeUrl(value) {
22714
22886
  }
22715
22887
 
22716
22888
  // src/mcp/gateway.ts
22889
+ var import_node_html_parser3 = __toESM(require_dist(), 1);
22890
+ var MAX_MCP_FILE_BYTES = 16 * 1024 * 1024;
22717
22891
  var MoodleGatewayError = class extends Error {
22718
22892
  code;
22719
22893
  constructor(code, message) {
@@ -22749,95 +22923,211 @@ function createMoodleGateway(client) {
22749
22923
  return limit === void 0 ? forums : forums.slice(0, limit);
22750
22924
  },
22751
22925
  searchForums: ({ forumId, ...input }) => client.searchForumContent({ ...input, forumCmid: forumId }),
22752
- getThread: ({ discussionId }) => client.getForumDiscussion(discussionId)
22926
+ getThread: ({ discussionId }) => client.getForumDiscussion(discussionId),
22927
+ async getFile({ source }) {
22928
+ let target = await resolveFileTarget(client, source);
22929
+ let response = await client.requestAbsolute(target.url);
22930
+ if (isHtml(response)) {
22931
+ const html = await response.text();
22932
+ if (looksLikeLoginPage(html)) throw fileAuthenticationRequired();
22933
+ const links = resourceLinks(html, target.url);
22934
+ if (links.length !== 1) {
22935
+ const code = links.length ? "MOODLE_FILE_SOURCE_AMBIGUOUS" : "MOODLE_FILE_NOT_FOUND";
22936
+ throw new MoodleGatewayError(code, "The Moodle resource did not resolve to exactly one file.");
22937
+ }
22938
+ const resolved = await resolveFileTarget(client, links[0].url);
22939
+ target = { ...resolved, name: target.name || links[0].name || resolved.name };
22940
+ response = await client.requestAbsolute(target.url);
22941
+ if (isHtml(response)) {
22942
+ if (looksLikeLoginPage(await response.text())) throw fileAuthenticationRequired();
22943
+ throw new MoodleGatewayError("MOODLE_FILE_NOT_FOUND", "Moodle did not return a downloadable file.");
22944
+ }
22945
+ }
22946
+ const contentLength = Number(response.headers.get("content-length"));
22947
+ if (Number.isFinite(contentLength) && contentLength > MAX_MCP_FILE_BYTES) {
22948
+ throw fileTooLarge();
22949
+ }
22950
+ const content = await readBoundedBody(response, MAX_MCP_FILE_BYTES);
22951
+ const name = safeFilename(contentDispositionFilename(response.headers.get("content-disposition"))) ?? safeFilename(target.name) ?? safeFilename(urlFilename(response.url || target.url));
22952
+ if (!name) {
22953
+ throw new MoodleGatewayError("MOODLE_FILE_NAME_MISSING", "Moodle did not provide a safe filename.");
22954
+ }
22955
+ return {
22956
+ name,
22957
+ mimeType: contentType(response),
22958
+ bytes: content.byteLength,
22959
+ uri: publicFileUrl(response.url || target.url),
22960
+ blob: encodeBase64(content)
22961
+ };
22962
+ }
22753
22963
  };
22754
22964
  }
22755
-
22756
- // src/mcp/protocol.ts
22757
- var MODERN_PROTOCOL_VERSION = "2026-07-28";
22758
- var LEGACY_PROTOCOL_VERSION = "2025-11-25";
22759
- var SUPPORTED_PROTOCOL_VERSIONS = [MODERN_PROTOCOL_VERSION, LEGACY_PROTOCOL_VERSION];
22760
- var JsonRpcRequestSchema = external_exports.object({
22761
- jsonrpc: external_exports.literal("2.0"),
22762
- id: external_exports.union([external_exports.string(), external_exports.number(), external_exports.null()]).optional(),
22763
- method: external_exports.string().trim().min(1),
22764
- params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
22765
- }).strict();
22766
- var ModernClientMetadataSchema = external_exports.object({
22767
- "io.modelcontextprotocol/clientCapabilities": external_exports.record(external_exports.string(), external_exports.unknown()),
22768
- "io.modelcontextprotocol/clientInfo": external_exports.object({
22769
- name: external_exports.string().trim().min(1),
22770
- version: external_exports.string().trim().min(1)
22771
- }).passthrough(),
22772
- "io.modelcontextprotocol/protocolVersion": external_exports.literal(MODERN_PROTOCOL_VERSION)
22773
- }).passthrough();
22774
- var UnsupportedProtocolVersionError = class extends Error {
22775
- protocolVersion;
22776
- supportedVersions = SUPPORTED_PROTOCOL_VERSIONS;
22777
- constructor(protocolVersion) {
22778
- super(`Unsupported MCP protocol version: ${protocolVersion}`);
22779
- this.name = "UnsupportedProtocolVersionError";
22780
- this.protocolVersion = protocolVersion;
22965
+ async function resolveFileTarget(client, rawSource) {
22966
+ const source = String(rawSource).trim();
22967
+ if (/^\d+$/u.test(source)) {
22968
+ const activityId = Number(source);
22969
+ if (!Number.isSafeInteger(activityId) || activityId < 1) throw invalidFileSource();
22970
+ return validateFileTarget(client, await fileFromActivity(client, activityId));
22781
22971
  }
22782
- };
22783
- var RequestMetadataMismatchError = class extends Error {
22784
- field;
22785
- expected;
22786
- actual;
22787
- constructor(field, expected, actual) {
22788
- super(`MCP request metadata mismatch for ${field}.`);
22789
- this.name = "RequestMetadataMismatchError";
22790
- this.field = field;
22791
- this.expected = expected;
22792
- this.actual = actual;
22972
+ let url2;
22973
+ try {
22974
+ url2 = new URL(source);
22975
+ } catch {
22976
+ throw invalidFileSource();
22793
22977
  }
22794
- };
22795
- function parseJsonRpcRequest(input) {
22796
- return JsonRpcRequestSchema.parse(input);
22797
- }
22798
- function resolveProtocolVersion(request, context = {}) {
22799
- const meta3 = isRecord5(request.params?._meta) ? request.params._meta : void 0;
22800
- const initializeVersion = request.method === "initialize" && typeof request.params?.protocolVersion === "string" ? request.params.protocolVersion : void 0;
22801
- const requested = context.protocolVersion ?? stringValue3(meta3?.["io.modelcontextprotocol/protocolVersion"]) ?? initializeVersion ?? MODERN_PROTOCOL_VERSION;
22802
- if (!isSupportedProtocolVersion(requested)) {
22803
- throw new UnsupportedProtocolVersionError(requested);
22978
+ const site = new URL(client.baseUrl);
22979
+ const sitePath = site.pathname.replace(/\/$/u, "");
22980
+ if (url2.origin !== site.origin) throw invalidFileSource();
22981
+ if (url2.pathname === `${sitePath}/mod/resource/view.php`) {
22982
+ const activityId = Number(url2.searchParams.get("id"));
22983
+ if (!Number.isSafeInteger(activityId) || activityId < 1) throw invalidFileSource();
22984
+ return validateFileTarget(client, await fileFromActivity(client, activityId));
22804
22985
  }
22805
- return requested;
22986
+ if (!url2.pathname.startsWith(`${sitePath}/pluginfile.php/`)) throw invalidFileSource();
22987
+ return { url: url2.toString(), name: urlFilename(url2.toString()) };
22806
22988
  }
22807
- function assertRequestMetadata(request, protocolVersion, context = {}) {
22808
- const meta3 = isRecord5(request.params?._meta) ? request.params._meta : void 0;
22809
- const metaVersion = stringValue3(meta3?.["io.modelcontextprotocol/protocolVersion"]);
22810
- if (protocolVersion === MODERN_PROTOCOL_VERSION) {
22811
- const parsed = ModernClientMetadataSchema.safeParse(meta3);
22812
- if (!parsed.success) {
22813
- throw new RequestMetadataMismatchError("_meta", "valid modern client metadata");
22814
- }
22815
- }
22816
- if (context.protocolVersion && metaVersion && context.protocolVersion !== metaVersion) {
22817
- throw new RequestMetadataMismatchError("protocolVersion", context.protocolVersion, metaVersion);
22818
- }
22819
- if (context.method && context.method !== request.method) {
22820
- throw new RequestMetadataMismatchError("method", context.method, request.method);
22989
+ function validateFileTarget(client, target) {
22990
+ let url2;
22991
+ try {
22992
+ url2 = new URL(target.url);
22993
+ } catch {
22994
+ throw invalidFileSource();
22995
+ }
22996
+ const site = new URL(client.baseUrl);
22997
+ const sitePath = site.pathname.replace(/\/$/u, "");
22998
+ const supportedPath = url2.pathname === `${sitePath}/mod/resource/view.php` || url2.pathname.startsWith(`${sitePath}/pluginfile.php/`);
22999
+ if (url2.origin !== site.origin || !supportedPath) throw invalidFileSource();
23000
+ return target;
23001
+ }
23002
+ async function fileFromActivity(client, activityId) {
23003
+ const activity = await client.getActivity(activityId);
23004
+ if (activity.type !== "resource" || !("file_entries" in activity) || !Array.isArray(activity.file_entries)) {
23005
+ throw new MoodleGatewayError(
23006
+ "MOODLE_FILE_SOURCE_INVALID",
23007
+ `Activity ${activityId} is not a downloadable resource.`
23008
+ );
22821
23009
  }
22822
- const toolName = typeof request.params?.name === "string" ? request.params.name : void 0;
22823
- if (context.toolName && context.toolName !== toolName) {
22824
- throw new RequestMetadataMismatchError("name", context.toolName, toolName);
23010
+ if (activity.file_entries.length !== 1) {
23011
+ if (activity.file_entries.length === 0) {
23012
+ const resource = activity;
23013
+ const url2 = resource.target_url || resource.url;
23014
+ if (url2) return { url: url2, name: resource.target_name || void 0 };
23015
+ }
23016
+ throw new MoodleGatewayError(
23017
+ "MOODLE_FILE_SOURCE_AMBIGUOUS",
23018
+ `Activity ${activityId} did not resolve to exactly one file. Inspect file_entries and request one URL.`
23019
+ );
22825
23020
  }
23021
+ const [entry] = activity.file_entries;
23022
+ return { url: entry.url, name: entry.name };
23023
+ }
23024
+ function resourceLinks(html, baseUrl) {
23025
+ const root = (0, import_node_html_parser3.parse)(html);
23026
+ const entries = root.querySelectorAll(".resourceworkaround a[href], .resourcecontent a[href], a.resourceworkaround[href]").map((link) => ({
23027
+ name: link.textContent.trim(),
23028
+ url: new URL(link.getAttribute("href") ?? "", baseUrl).toString()
23029
+ })).filter((entry) => entry.url !== baseUrl);
23030
+ return entries.filter((entry, index) => entries.findIndex((candidate) => candidate.url === entry.url) === index);
23031
+ }
23032
+ async function readBoundedBody(response, maxBytes) {
23033
+ if (!response.body) return new Uint8Array();
23034
+ const reader = response.body.getReader();
23035
+ const chunks2 = [];
23036
+ let bytes = 0;
23037
+ while (true) {
23038
+ const { done, value } = await reader.read();
23039
+ if (done) break;
23040
+ bytes += value.byteLength;
23041
+ if (bytes > maxBytes) {
23042
+ await reader.cancel().catch(() => void 0);
23043
+ throw fileTooLarge();
23044
+ }
23045
+ chunks2.push(value);
23046
+ }
23047
+ const content = new Uint8Array(bytes);
23048
+ let offset = 0;
23049
+ for (const chunk of chunks2) {
23050
+ content.set(chunk, offset);
23051
+ offset += chunk.byteLength;
23052
+ }
23053
+ return content;
23054
+ }
23055
+ function invalidFileSource() {
23056
+ return new MoodleGatewayError(
23057
+ "MOODLE_FILE_SOURCE_INVALID",
23058
+ "Use a positive resource activity ID, a same-site resource URL, or a same-site pluginfile URL."
23059
+ );
22826
23060
  }
22827
- function isSupportedProtocolVersion(value) {
22828
- return SUPPORTED_PROTOCOL_VERSIONS.some((version2) => version2 === value);
23061
+ function fileTooLarge() {
23062
+ return new MoodleGatewayError(
23063
+ "MOODLE_FILE_TOO_LARGE",
23064
+ `Moodle files returned through MCP cannot exceed ${MAX_MCP_FILE_BYTES / 1024 / 1024} MiB.`
23065
+ );
22829
23066
  }
22830
- function jsonRpcSuccess(id, result) {
22831
- return { jsonrpc: "2.0", id, result };
23067
+ function fileAuthenticationRequired() {
23068
+ return new MoodleGatewayError(
23069
+ "MOODLE_AUTH_REQUIRED",
23070
+ "Moodle returned a login page instead of the requested file."
23071
+ );
22832
23072
  }
22833
- function jsonRpcFailure(id, error51) {
22834
- return { jsonrpc: "2.0", id, error: error51 };
23073
+ function isHtml(response) {
23074
+ if (/\battachment\b/iu.test(response.headers.get("content-disposition") ?? "")) return false;
23075
+ const type = response.headers.get("content-type")?.toLowerCase() ?? "";
23076
+ return type.includes("text/html") || type.includes("application/xhtml+xml");
22835
23077
  }
22836
- function isRecord5(value) {
22837
- return typeof value === "object" && value !== null && !Array.isArray(value);
23078
+ function looksLikeLoginPage(html) {
23079
+ const root = (0, import_node_html_parser3.parse)(html);
23080
+ return root.querySelector('form[action*="/login/"], input[name="password"], #page-login-index') !== null || /<title>\s*(?:log in|login)/iu.test(html);
22838
23081
  }
22839
- function stringValue3(value) {
22840
- return typeof value === "string" ? value : void 0;
23082
+ function contentType(response) {
23083
+ return response.headers.get("content-type")?.split(";", 1)[0]?.trim() || "application/octet-stream";
23084
+ }
23085
+ function contentDispositionFilename(value) {
23086
+ if (!value) return void 0;
23087
+ const extended = value.match(/filename\*\s*=\s*([^;]+)/iu)?.[1]?.trim().replace(/^"|"$/gu, "");
23088
+ if (extended) {
23089
+ const encoded = extended.replace(/^[^']*'[^']*'/u, "");
23090
+ try {
23091
+ return decodeURIComponent(encoded);
23092
+ } catch {
23093
+ return encoded;
23094
+ }
23095
+ }
23096
+ const quoted = value.match(/filename\s*=\s*"((?:\\.|[^"])*)"/iu)?.[1];
23097
+ if (quoted !== void 0) return quoted.replace(/\\([\\"])/gu, "$1");
23098
+ return value.match(/filename\s*=\s*([^;]+)/iu)?.[1]?.trim();
23099
+ }
23100
+ function urlFilename(value) {
23101
+ try {
23102
+ const encoded = new URL(value).pathname.split("/").at(-1) ?? "";
23103
+ try {
23104
+ return decodeURIComponent(encoded);
23105
+ } catch {
23106
+ return encoded;
23107
+ }
23108
+ } catch {
23109
+ return void 0;
23110
+ }
23111
+ }
23112
+ function safeFilename(value) {
23113
+ const name = value?.split(/[\\/]/u).at(-1)?.replace(/[\u0000-\u001f\u007f]/gu, "").trim();
23114
+ return name && name !== "." && name !== ".." ? name : void 0;
23115
+ }
23116
+ function publicFileUrl(value) {
23117
+ const url2 = new URL(value);
23118
+ url2.username = "";
23119
+ url2.password = "";
23120
+ for (const key of [...url2.searchParams.keys()]) {
23121
+ if (key !== "forcedownload" && key !== "download") url2.searchParams.delete(key);
23122
+ }
23123
+ return url2.toString();
23124
+ }
23125
+ function encodeBase64(content) {
23126
+ let binary = "";
23127
+ for (let offset = 0; offset < content.byteLength; offset += 32768) {
23128
+ binary += String.fromCharCode(...content.subarray(offset, offset + 32768));
23129
+ }
23130
+ return btoa(binary);
22841
23131
  }
22842
23132
 
22843
23133
  // src/mcp/server.ts
@@ -22850,14 +23140,122 @@ var READ_ONLY_ANNOTATIONS = {
22850
23140
  };
22851
23141
  var emptyInput = external_exports.object({}).strict();
22852
23142
  var positiveId = external_exports.number().int().positive();
22853
- var objectOutput = external_exports.looseObject({});
22854
- var listOutput = external_exports.array(objectOutput);
23143
+ var integer2 = external_exports.number().int();
23144
+ var userValue = external_exports.looseObject({
23145
+ userid: integer2,
23146
+ username: external_exports.string(),
23147
+ fullname: external_exports.string(),
23148
+ sitename: external_exports.string(),
23149
+ siteurl: external_exports.string(),
23150
+ lang: external_exports.string().optional()
23151
+ });
23152
+ var courseValue = external_exports.looseObject({
23153
+ id: integer2,
23154
+ shortname: external_exports.string(),
23155
+ fullname: external_exports.string(),
23156
+ category: external_exports.number().int(),
23157
+ visible: external_exports.boolean(),
23158
+ startdate: external_exports.number(),
23159
+ enddate: external_exports.number().optional()
23160
+ });
23161
+ var activityValue = external_exports.looseObject({
23162
+ id: integer2,
23163
+ name: external_exports.string(),
23164
+ modname: external_exports.string(),
23165
+ url: external_exports.string(),
23166
+ visible: external_exports.boolean(),
23167
+ description: external_exports.string()
23168
+ });
23169
+ var fileEntryValue = external_exports.looseObject({
23170
+ name: external_exports.string(),
23171
+ url: external_exports.string(),
23172
+ requires_authentication: external_exports.boolean()
23173
+ });
23174
+ var activityDetailValue = external_exports.looseObject({
23175
+ id: positiveId,
23176
+ name: external_exports.string(),
23177
+ type: external_exports.string(),
23178
+ url: external_exports.string().optional(),
23179
+ target_name: external_exports.string().optional(),
23180
+ target_url: external_exports.string().optional(),
23181
+ file_entries: external_exports.array(fileEntryValue).optional()
23182
+ });
23183
+ var sectionValue = external_exports.looseObject({
23184
+ id: external_exports.number().int(),
23185
+ name: external_exports.string(),
23186
+ section: external_exports.number().int(),
23187
+ visible: external_exports.boolean(),
23188
+ summary: external_exports.string(),
23189
+ activities: external_exports.array(activityValue)
23190
+ });
23191
+ var todoValue = external_exports.looseObject({
23192
+ id: external_exports.number().int(),
23193
+ name: external_exports.string(),
23194
+ course_id: external_exports.number().int(),
23195
+ course_name: external_exports.string(),
23196
+ due_at: external_exports.number(),
23197
+ url: external_exports.string()
23198
+ });
23199
+ var gradeItemValue = external_exports.looseObject({
23200
+ name: external_exports.string(),
23201
+ item_type: external_exports.string(),
23202
+ grade: external_exports.string(),
23203
+ range: external_exports.string(),
23204
+ percentage: external_exports.string(),
23205
+ feedback: external_exports.string(),
23206
+ url: external_exports.string()
23207
+ });
23208
+ var gradesValue = external_exports.looseObject({
23209
+ course_id: integer2,
23210
+ course_name: external_exports.string(),
23211
+ learner_name: external_exports.string(),
23212
+ total_grade: external_exports.string(),
23213
+ total_range: external_exports.string(),
23214
+ total_percentage: external_exports.string(),
23215
+ items: external_exports.array(gradeItemValue)
23216
+ });
23217
+ var forumValue = external_exports.looseObject({
23218
+ id: integer2,
23219
+ name: external_exports.string(),
23220
+ course_id: integer2,
23221
+ course_name: external_exports.string(),
23222
+ url: external_exports.string()
23223
+ });
23224
+ var forumSearchValue = external_exports.looseObject({
23225
+ course_id: integer2,
23226
+ course_name: external_exports.string(),
23227
+ forum_id: integer2,
23228
+ forum_name: external_exports.string(),
23229
+ discussion_id: integer2,
23230
+ discussion_subject: external_exports.string(),
23231
+ post_id: integer2,
23232
+ snippet: external_exports.string(),
23233
+ url: external_exports.string()
23234
+ });
23235
+ var forumPostValue = external_exports.looseObject({
23236
+ id: integer2,
23237
+ discussion_id: integer2,
23238
+ subject: external_exports.string(),
23239
+ message_text: external_exports.string(),
23240
+ author: external_exports.looseObject({ id: integer2, fullname: external_exports.string() }),
23241
+ url: external_exports.string()
23242
+ });
23243
+ var threadValue = external_exports.looseObject({
23244
+ id: integer2,
23245
+ subject: external_exports.string(),
23246
+ course_id: integer2,
23247
+ forum_id: integer2,
23248
+ group_id: external_exports.number().int(),
23249
+ group_name: external_exports.string(),
23250
+ url: external_exports.string(),
23251
+ posts: external_exports.array(forumPostValue)
23252
+ });
22855
23253
  var TOOL_REGISTRATIONS = [
22856
23254
  {
22857
23255
  name: "get_user",
22858
23256
  description: "Get the authenticated Moodle user and site.",
22859
23257
  input: emptyInput,
22860
- output: external_exports.object({ user: objectOutput })
23258
+ output: external_exports.object({ user: userValue })
22861
23259
  },
22862
23260
  {
22863
23261
  name: "get_overview",
@@ -22867,19 +23265,31 @@ var TOOL_REGISTRATIONS = [
22867
23265
  todoDays: external_exports.number().int().min(1).max(365).optional(),
22868
23266
  alertsLimit: external_exports.number().int().min(1).max(100).optional().default(5)
22869
23267
  }).strict(),
22870
- output: external_exports.object({ overview: objectOutput })
23268
+ output: external_exports.object({
23269
+ overview: external_exports.looseObject({
23270
+ user: userValue,
23271
+ courses: external_exports.array(courseValue),
23272
+ todo: external_exports.array(todoValue),
23273
+ errors: external_exports.array(external_exports.string())
23274
+ })
23275
+ })
22871
23276
  },
22872
23277
  {
22873
23278
  name: "list_courses",
22874
23279
  description: "List the authenticated user's Moodle courses.",
22875
23280
  input: external_exports.object({ limit: external_exports.number().int().min(1).max(200).optional().default(100) }).strict(),
22876
- output: external_exports.object({ courses: listOutput })
23281
+ output: external_exports.object({ courses: external_exports.array(courseValue) })
22877
23282
  },
22878
23283
  {
22879
23284
  name: "get_course",
22880
23285
  description: "Get one Moodle course and its sections.",
22881
23286
  input: external_exports.object({ courseId: positiveId }).strict(),
22882
- output: external_exports.object({ course: objectOutput })
23287
+ output: external_exports.object({
23288
+ course: external_exports.looseObject({
23289
+ course: courseValue,
23290
+ sections: external_exports.array(sectionValue)
23291
+ })
23292
+ })
22883
23293
  },
22884
23294
  {
22885
23295
  name: "list_activities",
@@ -22888,19 +23298,19 @@ var TOOL_REGISTRATIONS = [
22888
23298
  courseId: positiveId,
22889
23299
  limit: external_exports.number().int().min(1).max(200).optional().default(100)
22890
23300
  }).strict(),
22891
- output: external_exports.object({ activities: listOutput })
23301
+ output: external_exports.object({ activities: external_exports.array(activityValue) })
22892
23302
  },
22893
23303
  {
22894
23304
  name: "get_activity",
22895
23305
  description: "Get the supported details for one Moodle activity.",
22896
23306
  input: external_exports.object({ activityId: positiveId }).strict(),
22897
- output: external_exports.object({ activity: objectOutput })
23307
+ output: external_exports.object({ activity: activityDetailValue })
22898
23308
  },
22899
23309
  {
22900
23310
  name: "get_grades",
22901
23311
  description: "Get the authenticated user's grades for one Moodle course.",
22902
23312
  input: external_exports.object({ courseId: positiveId }).strict(),
22903
- output: external_exports.object({ grades: objectOutput })
23313
+ output: external_exports.object({ grades: gradesValue })
22904
23314
  },
22905
23315
  {
22906
23316
  name: "list_forums",
@@ -22909,7 +23319,7 @@ var TOOL_REGISTRATIONS = [
22909
23319
  courseId: positiveId.optional(),
22910
23320
  limit: external_exports.number().int().min(1).max(100).optional().default(50)
22911
23321
  }).strict(),
22912
- output: external_exports.object({ forums: listOutput })
23322
+ output: external_exports.object({ forums: external_exports.array(forumValue) })
22913
23323
  },
22914
23324
  {
22915
23325
  name: "search_forums",
@@ -22925,13 +23335,28 @@ var TOOL_REGISTRATIONS = [
22925
23335
  maxForums: external_exports.number().int().min(1).max(50).optional(),
22926
23336
  maxDiscussionsPerForum: external_exports.number().int().min(1).max(100).optional()
22927
23337
  }).strict(),
22928
- output: external_exports.object({ results: listOutput })
23338
+ output: external_exports.object({ results: external_exports.array(forumSearchValue) })
22929
23339
  },
22930
23340
  {
22931
23341
  name: "get_thread",
22932
23342
  description: "Get one Moodle forum discussion and its posts.",
22933
23343
  input: external_exports.object({ discussionId: positiveId }).strict(),
22934
- output: external_exports.object({ thread: objectOutput })
23344
+ output: external_exports.object({ thread: threadValue })
23345
+ },
23346
+ {
23347
+ name: "get_file",
23348
+ description: "Fetch one authenticated Moodle file and return its content directly (maximum 16 MiB).",
23349
+ input: external_exports.object({
23350
+ source: external_exports.union([positiveId, external_exports.string().trim().min(1).max(2048)])
23351
+ }).strict(),
23352
+ output: external_exports.object({
23353
+ file: external_exports.object({
23354
+ name: external_exports.string(),
23355
+ mime_type: external_exports.string(),
23356
+ bytes: external_exports.number().int().nonnegative(),
23357
+ uri: external_exports.string()
23358
+ })
23359
+ })
22935
23360
  }
22936
23361
  ];
22937
23362
  var TOOL_CATALOG = TOOL_REGISTRATIONS.map(({ name, description, input, output }) => ({
@@ -22999,12 +23424,11 @@ function createMoodleMcpServer(gateway, options = {}) {
22999
23424
  }
23000
23425
  if (error51 instanceof UnsupportedProtocolVersionError) {
23001
23426
  return jsonRpcFailure(id, {
23002
- code: -32602,
23003
- message: error51.message,
23427
+ code: -32022,
23428
+ message: "Unsupported protocol version",
23004
23429
  data: {
23005
- type: "UNSUPPORTED_PROTOCOL_VERSION",
23006
- protocolVersion: error51.protocolVersion,
23007
- supportedVersions: [...error51.supportedVersions]
23430
+ supported: [...error51.supportedVersions],
23431
+ requested: error51.protocolVersion
23008
23432
  }
23009
23433
  });
23010
23434
  }
@@ -23052,7 +23476,7 @@ async function callTool(gateway, params) {
23052
23476
  const payload = await runGatewayTool(gateway, name, input);
23053
23477
  const structuredContent = registration.output.parse(wrapToolOutput(name, payload));
23054
23478
  return {
23055
- content: [{ type: "text", text: summarizeToolOutput(name, payload) }],
23479
+ content: toolContent(name, payload),
23056
23480
  structuredContent,
23057
23481
  resultType: "complete",
23058
23482
  _meta: RESULT_META
@@ -23113,6 +23537,8 @@ async function runGatewayTool(gateway, name, input) {
23113
23537
  });
23114
23538
  case "get_thread":
23115
23539
  return gateway.getThread({ discussionId: numberValue3(input.discussionId) });
23540
+ case "get_file":
23541
+ return gateway.getFile({ source: fileSource(input.source) });
23116
23542
  default:
23117
23543
  throw new McpCallError("TOOL_NOT_FOUND", `Unknown Moodle tool: ${name}`);
23118
23544
  }
@@ -23130,8 +23556,33 @@ function wrapToolOutput(name, payload) {
23130
23556
  search_forums: "results",
23131
23557
  get_thread: "thread"
23132
23558
  };
23559
+ if (name === "get_file" && isMoodleFile(payload)) {
23560
+ return {
23561
+ file: {
23562
+ name: payload.name,
23563
+ mime_type: payload.mimeType,
23564
+ bytes: payload.bytes,
23565
+ uri: payload.uri
23566
+ }
23567
+ };
23568
+ }
23133
23569
  return { [keys[name] ?? "result"]: payload };
23134
23570
  }
23571
+ function toolContent(name, payload) {
23572
+ const text = { type: "text", text: summarizeToolOutput(name, payload) };
23573
+ if (name !== "get_file" || !isMoodleFile(payload)) return [text];
23574
+ return [
23575
+ text,
23576
+ {
23577
+ type: "resource",
23578
+ resource: {
23579
+ uri: payload.uri,
23580
+ mimeType: payload.mimeType,
23581
+ blob: payload.blob
23582
+ }
23583
+ }
23584
+ ];
23585
+ }
23135
23586
  function summarizeToolOutput(name, payload) {
23136
23587
  if (Array.isArray(payload)) {
23137
23588
  const labels = {
@@ -23163,6 +23614,9 @@ function summarizeToolOutput(name, payload) {
23163
23614
  if (name === "get_thread" && isRecord6(payload)) {
23164
23615
  return `Loaded forum thread ${stringValue4(payload.subject) || numberValue3(payload.id)}.`;
23165
23616
  }
23617
+ if (name === "get_file" && isMoodleFile(payload)) {
23618
+ return `Loaded Moodle file ${payload.name} (${payload.bytes} bytes).`;
23619
+ }
23166
23620
  return "Moodle request completed.";
23167
23621
  }
23168
23622
  function mapMoodleError(error51) {
@@ -23198,6 +23652,9 @@ function optionalNumber(value) {
23198
23652
  function stringValue4(value) {
23199
23653
  return typeof value === "string" ? value : "";
23200
23654
  }
23655
+ function fileSource(value) {
23656
+ return typeof value === "number" || typeof value === "string" ? value : "";
23657
+ }
23201
23658
  function booleanValue2(value) {
23202
23659
  return value === true;
23203
23660
  }
@@ -23207,6 +23664,9 @@ function enumValue(value, values) {
23207
23664
  function isRecord6(value) {
23208
23665
  return typeof value === "object" && value !== null && !Array.isArray(value);
23209
23666
  }
23667
+ function isMoodleFile(value) {
23668
+ return isRecord6(value) && typeof value.name === "string" && typeof value.mimeType === "string" && typeof value.bytes === "number" && typeof value.uri === "string" && typeof value.blob === "string";
23669
+ }
23210
23670
 
23211
23671
  // src/worker/moodle-upstream.ts
23212
23672
  var DASHBOARD_PATH2 = "/my/";