neuro-canvas-lms 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -30,6 +30,7 @@ Code map:
30
30
  - `owner.mjs`: owner binding, allowed OAuth clients. `i18n.mjs` + `pages.mjs`: web pages.
31
31
  - `update-check.mjs`: once a day reads the newest version number from npm; when it is newer the AI
32
32
  tells the user how to update. Also the `canvas_connector_version` tool.
33
- - `installer/`: the setup wizard (re-running it asks once, y/n, then updates with the saved
34
- settings; without a terminal it does not ask). `version.mjs`: shown on
35
- the home page and /healthz, compared with npm by update-check.mjs.
33
+ - `installer/`: the setup wizard. Re-running it asks once (y/n), then updates with the saved
34
+ settings (without a terminal it does not ask). `installer/verify.mjs`: the post-deploy online
35
+ check, also run alone by `npx -y neuro-canvas-lms check`.
36
+ - `version.mjs`: shown on the home page and /healthz, compared with npm by update-check.mjs.
package/installer/cli.mjs CHANGED
@@ -1,11 +1,13 @@
1
1
  // Neuro-CanvasLMS installer. Deploys THIS copy of the code to your own Cloudflare account.
2
2
  // npx -y neuro-canvas-lms install, or update an existing install
3
3
  // npx -y neuro-canvas-lms uninstall remove it
4
+ // npx -y neuro-canvas-lms check only check an installed connector (deploys nothing)
4
5
  // (from a clone: npm run setup / npm run setup -- uninstall)
5
6
  // Running it again updates the connector to the code you are running and keeps its storage, so
6
7
  // existing Claude/ChatGPT connections keep working.
7
8
  // Language: your system language if supported (en, zh, es, ja, hi), else English; --lang to choose.
8
9
  // Automation/tests: --name, --canvas-host, --school, --account-id, --yes; env NCLMS_HOME, NCLMS_LANG.
10
+ // Exit codes: 0 done, 1 failed, 2 deployed but not confirmed online yet (run `check` later).
9
11
  import { spawnSync } from 'node:child_process';
10
12
  import { createRequire } from 'node:module';
11
13
  import fs from 'node:fs';
@@ -32,6 +34,7 @@ import {
32
34
  subdomainLink,
33
35
  } from './lib.mjs';
34
36
  import { msg } from './messages.mjs';
37
+ import { verifyDeployment } from './verify.mjs';
35
38
 
36
39
  const ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
37
40
  // wrangler's package "exports" hide bin/; find the package root from its main entry instead.
@@ -227,27 +230,6 @@ async function runningVersion(url) {
227
230
  }
228
231
  }
229
232
 
230
- async function healthCheck(url) {
231
- let detail = 'no answer';
232
- for (let i = 0; i < 20; i++) {
233
- if (i) await new Promise(r => setTimeout(r, 3000));
234
- try {
235
- const h = await fetch(`${url}/healthz`, { headers: { 'Cache-Control': 'no-cache' } });
236
- const j = h.ok ? await h.json() : null;
237
- if (!j?.ok) detail = `/healthz HTTP ${h.status}`;
238
- else if (j.version !== VERSION) detail = `still ${j.version}`;
239
- else {
240
- const mcp = await fetch(`${url}/mcp`, { method: 'POST', body: '{}' });
241
- if (mcp.status === 401) return { ok: true, connected: j.connected === true };
242
- detail = `/mcp HTTP ${mcp.status}`;
243
- }
244
- } catch (e) {
245
- detail = e.message;
246
- }
247
- }
248
- return { ok: false, detail };
249
- }
250
-
251
233
  async function install() {
252
234
  console.log(`\n ${msg(lang, 'title')}\n`);
253
235
  const [major] = process.versions.node.split('.').map(Number);
@@ -302,8 +284,8 @@ async function install() {
302
284
  save({ url, version: VERSION });
303
285
 
304
286
  say('check');
305
- const check = await healthCheck(url);
306
- if (!check.ok) fail(msg(lang, 'check_bad', { detail: check.detail }));
287
+ const check = await verifyDeployment(url, VERSION, { onWarmup: () => say('cert_wait') });
288
+ if (!check.ok) return notConfirmed(name, url, check.detail);
307
289
  ok('check_ok', { version: VERSION });
308
290
 
309
291
  // Already connected: an update needs nothing from the user, so don't send them to make a token,
@@ -316,6 +298,22 @@ async function install() {
316
298
  console.log('');
317
299
  return;
318
300
  }
301
+ nextSteps(url, host);
302
+ }
303
+
304
+ // The deploy worked but the address did not answer correctly in time (usually a brand-new
305
+ // workers.dev address still getting its certificate). Not a failure: say where it is and how to
306
+ // check again without deploying. Exit code 2 tells scripts and agents exactly that.
307
+ function notConfirmed(name, url, detail) {
308
+ const cmd = 'npx -y neuro-canvas-lms check' + (name === DEFAULT_NAME ? '' : ` --name ${name}`);
309
+ console.log('');
310
+ say('check_pending', { url, detail, cmd });
311
+ console.log('');
312
+ process.exitCode = 2;
313
+ }
314
+
315
+ // What the user does next, once the connector is online and not yet connected to an AI app.
316
+ function nextSteps(url, host) {
319
317
  copy(`${url}/mcp`);
320
318
  console.log(`\n ${msg(lang, 'done')}`);
321
319
  say('done_1', { canvas: `https://${host}` });
@@ -330,6 +328,28 @@ async function install() {
330
328
  if (!args.yes) openUrl(`https://${host}/profile/settings`);
331
329
  }
332
330
 
331
+ // Only check an installed connector (after "not confirmed online yet"): nothing is deployed.
332
+ async function check() {
333
+ console.log(`\n ${msg(lang, 'title')}\n`);
334
+ const name = args.name ?? DEFAULT_NAME;
335
+ if (!validWorkerName(name)) fail('invalid --name');
336
+ const statePath = path.join(HOME, name, 'state.json');
337
+ const state = fs.existsSync(statePath) ? JSON.parse(fs.readFileSync(statePath, 'utf8')) : {};
338
+ if (!state.url) {
339
+ console.error(' ✗ ' + msg(lang, 'check_none', { name }));
340
+ process.exitCode = 1;
341
+ return;
342
+ }
343
+ say('check_only', { url: state.url });
344
+ const result = await verifyDeployment(state.url, state.version, {
345
+ onWarmup: () => say('cert_wait'),
346
+ });
347
+ if (!result.ok) return notConfirmed(name, state.url, result.detail);
348
+ ok('check_ok', { version: result.version });
349
+ if (result.connected) return ok('check_connected');
350
+ nextSteps(state.url, state.host);
351
+ }
352
+
333
353
  async function uninstall() {
334
354
  const name = args.name ?? DEFAULT_NAME;
335
355
  const dir = path.join(HOME, name);
@@ -369,11 +389,11 @@ const help = async () =>
369
389
  fs
370
390
  .readFileSync(fileURLToPath(import.meta.url), 'utf8')
371
391
  .split('\n')
372
- .slice(0, 8)
392
+ .slice(0, 10)
373
393
  .join('\n'),
374
394
  );
375
- ({ install, uninstall, help })[args.command]().then(
376
- () => process.exit(0),
395
+ ({ install, uninstall, help, check })[args.command]().then(
396
+ () => process.exit(process.exitCode ?? 0),
377
397
  e => {
378
398
  console.error('\n ✗ ' + msg(lang, 'failed', { detail: e?.message ?? e }) + '\n');
379
399
  process.exit(1);
package/installer/lib.mjs CHANGED
@@ -68,7 +68,7 @@ export function parseArgs(argv) {
68
68
  const out = { command: 'install' };
69
69
  for (let i = 0; i < argv.length; i++) {
70
70
  const a = argv[i];
71
- if (a === 'install' || a === 'uninstall' || a === 'help') out.command = a;
71
+ if (['install', 'uninstall', 'help', 'check'].includes(a)) out.command = a;
72
72
  else if (a === '--yes' || a === '-y') out.yes = true;
73
73
  else if (a.startsWith('--')) {
74
74
  const [k, v] = a.includes('=') ? a.slice(2).split(/=(.*)/s) : [a.slice(2), argv[++i]];
@@ -108,3 +108,12 @@ export const isYes = answer => /^(?:y|yes)$/i.test(String(answer ?? '').trim());
108
108
  export const needsSubdomain = out => /register a workers\.dev subdomain/i.test(out ?? '');
109
109
  export const subdomainLink = accountId =>
110
110
  `https://dash.cloudflare.com/${encodeURIComponent(accountId)}/workers/onboarding`;
111
+
112
+ // A new workers.dev address answers only once its DNS and HTTPS certificate are ready, which can
113
+ // take minutes on a brand-new subdomain. These errors mean "not ready yet", not "broken".
114
+ export const isWarmingUp = detail =>
115
+ /SSL|TLS|CERT|UNABLE_TO_VERIFY|SELF_SIGNED|EPROTO|handshake|ENOTFOUND|EAI_AGAIN|ECONNRESET|ECONNREFUSED|certificate/i.test(
116
+ detail ?? '',
117
+ );
118
+ export const CHECK_WAIT = 60 * 1000; // a normal deploy answers within a minute
119
+ export const WARMUP_WAIT = 5 * 60 * 1000; // a new address may need a few minutes for its certificate
@@ -23,7 +23,6 @@ const M = {
23
23
  deploy_ok: 'Connector is live: {url}',
24
24
  check: 'Step 4/4 · Checking the connector…',
25
25
  check_ok: 'Everything works (version {version}).',
26
- check_bad: 'The connector did not pass its check: {detail}',
27
26
  done: 'Done! Last step, in your AI:',
28
27
  done_1:
29
28
  '1. Create a Canvas access token: {canvas}/profile/settings → "+ New Access Token" (copy it; Canvas shows it once).',
@@ -47,6 +46,14 @@ const M = {
47
46
  un_done:
48
47
  'Removed. Also delete the Canvas token (Canvas → Account → Settings) and the connector in your AI app.',
49
48
  cancelled: 'Cancelled.',
49
+ check_pending:
50
+ 'Deployed to {url}, but it could not be confirmed online yet ({detail}). A new address can take a few more minutes the first time. Nothing needs to be deployed again: in a few minutes, check it with\n {cmd}',
51
+ check_none:
52
+ 'No install named "{name}" was found (installed under another name? add --name <name>). Install it first with npx -y neuro-canvas-lms.',
53
+ check_only: 'Checking the connector at {url}…',
54
+ check_connected: 'It is connected to your AI apps and ready to use.',
55
+ cert_wait:
56
+ 'The new workers.dev address is still getting its HTTPS certificate. The first time this can take a few minutes; waiting…',
50
57
  pick_account_flag:
51
58
  'This Cloudflare login has several accounts. Run the same command again with one of these added:\n{list}',
52
59
  canvas_flag:
@@ -87,7 +94,6 @@ const M = {
87
94
  deploy_ok: '连接器已上线:{url}',
88
95
  check: '第 4/4 步 · 检查连接器…',
89
96
  check_ok: '一切正常(版本 {version})。',
90
- check_bad: '连接器没通过检查:{detail}',
91
97
  done: '装好了!最后一步,在你的 AI 里:',
92
98
  done_1:
93
99
  '1. 生成 Canvas 访问令牌:{canvas}/profile/settings →「+ 新建访问许可证」(立刻复制,Canvas 只显示一次)。',
@@ -105,6 +111,13 @@ const M = {
105
111
  un_failed: '没能删除{what}:{detail}\n 本地安装记录已保留,再运行一次同一条命令即可重试。',
106
112
  un_done: '已删除。另外请删掉 Canvas 令牌(Canvas → 账户 → 设置)和 AI 应用里的连接器。',
107
113
  cancelled: '已取消。',
114
+ check_pending:
115
+ '已部署到 {url},但还没能确认它已上线({detail})。新地址第一次可能还要几分钟。不需要重新部署:过几分钟用这条命令只做检查:\n {cmd}',
116
+ check_none:
117
+ '没找到名为「{name}」的安装(装的时候用了别的名字?加上 --name <名字>)。请先用 npx -y neuro-canvas-lms 安装。',
118
+ check_only: '检查连接器 {url}…',
119
+ check_connected: '已经连接到你的 AI 应用,可以直接用。',
120
+ cert_wait: '新的 workers.dev 地址还在申请 HTTPS 证书,第一次可能要几分钟,正在等待…',
108
121
  pick_account_flag:
109
122
  '这个 Cloudflare 登录下有多个账号。请在同一条命令后加上其中一个,再运行一次:\n{list}',
110
123
  canvas_flag:
@@ -147,7 +160,6 @@ const M = {
147
160
  deploy_ok: 'El conector está activo: {url}',
148
161
  check: 'Paso 4/4 · Comprobando el conector…',
149
162
  check_ok: 'Todo funciona (versión {version}).',
150
- check_bad: 'El conector no pasó la comprobación: {detail}',
151
163
  done: '¡Listo! Último paso, en tu IA:',
152
164
  done_1:
153
165
  '1. Crea un token de acceso de Canvas: {canvas}/profile/settings → "+ Nuevo token de acceso" (cópialo; Canvas lo muestra una sola vez).',
@@ -172,6 +184,14 @@ const M = {
172
184
  un_done:
173
185
  'Eliminado. Borra también el token de Canvas (Canvas → Cuenta → Configuración) y el conector en tu app de IA.',
174
186
  cancelled: 'Cancelado.',
187
+ check_pending:
188
+ 'Desplegado en {url}, pero aún no se pudo confirmar que está en línea ({detail}). Una dirección nueva puede tardar unos minutos más la primera vez. No hace falta volver a desplegar: dentro de unos minutos, compruébalo con\n {cmd}',
189
+ check_none:
190
+ 'No se encontró ninguna instalación llamada "{name}" (¿la instalaste con otro nombre? añade --name <nombre>). Instálala primero con npx -y neuro-canvas-lms.',
191
+ check_only: 'Comprobando el conector en {url}…',
192
+ check_connected: 'Está conectado a tus apps de IA y listo para usar.',
193
+ cert_wait:
194
+ 'La nueva dirección workers.dev todavía está obteniendo su certificado HTTPS. La primera vez puede tardar unos minutos; esperando…',
175
195
  pick_account_flag:
176
196
  'Este inicio de sesión de Cloudflare tiene varias cuentas. Ejecuta el mismo comando otra vez añadiendo una de estas:\n{list}',
177
197
  canvas_flag:
@@ -215,7 +235,6 @@ const M = {
215
235
  deploy_ok: 'コネクタが公開されました:{url}',
216
236
  check: 'ステップ 4/4 · コネクタを確認しています…',
217
237
  check_ok: 'すべて正常です(バージョン {version})。',
218
- check_bad: 'コネクタが確認に失敗しました:{detail}',
219
238
  done: '完了!最後のステップ(AI 側):',
220
239
  done_1:
221
240
  '1. Canvas アクセストークンを作成:{canvas}/profile/settings →「+ 新しいアクセストークン」(すぐコピー。表示は一度きりです)。',
@@ -239,6 +258,14 @@ const M = {
239
258
  un_done:
240
259
  '削除しました。Canvas トークン(Canvas → アカウント → 設定)と AI アプリのコネクタも削除してください。',
241
260
  cancelled: 'キャンセルしました。',
261
+ check_pending:
262
+ '{url} へのデプロイは完了しましたが、まだオンラインであることを確認できていません({detail})。新しいアドレスは初回にさらに数分かかることがあります。再デプロイは不要です。数分後に次のコマンドで確認だけ行ってください:\n {cmd}',
263
+ check_none:
264
+ '「{name}」という名前のインストールが見つかりません(別の名前でインストールした場合は --name <名前> を付けてください)。先に npx -y neuro-canvas-lms でインストールしてください。',
265
+ check_only: '{url} のコネクタを確認しています…',
266
+ check_connected: 'AI アプリに接続済みで、すぐに使えます。',
267
+ cert_wait:
268
+ '新しい workers.dev アドレスの HTTPS 証明書を準備しています。初回は数分かかることがあります。待機しています…',
242
269
  pick_account_flag:
243
270
  'この Cloudflare ログインには複数のアカウントがあります。同じコマンドに次のどれかを付けて、もう一度実行してください:\n{list}',
244
271
  canvas_flag:
@@ -282,7 +309,6 @@ const M = {
282
309
  deploy_ok: 'कनेक्टर चालू है: {url}',
283
310
  check: 'चरण 4/4 · कनेक्टर की जाँच हो रही है…',
284
311
  check_ok: 'सब कुछ ठीक है (संस्करण {version})।',
285
- check_bad: 'कनेक्टर जाँच में विफल रहा: {detail}',
286
312
  done: 'हो गया! आख़िरी कदम, अपने AI में:',
287
313
  done_1:
288
314
  '1. Canvas एक्सेस टोकन बनाएँ: {canvas}/profile/settings → "+ New Access Token" (तुरंत कॉपी करें; Canvas इसे केवल एक बार दिखाता है)।',
@@ -306,6 +332,14 @@ const M = {
306
332
  un_done:
307
333
  'हटा दिया गया। Canvas टोकन (Canvas → Account → Settings) और अपने AI ऐप में कनेक्टर भी हटाएँ।',
308
334
  cancelled: 'रद्द किया गया।',
335
+ check_pending:
336
+ '{url} पर डिप्लॉय हो गया, लेकिन अभी पुष्टि नहीं हो पाई कि यह ऑनलाइन है ({detail})। नए पते को पहली बार कुछ और मिनट लग सकते हैं। दोबारा डिप्लॉय करने की ज़रूरत नहीं: कुछ मिनट बाद इस कमांड से सिर्फ़ जाँच करें:\n {cmd}',
337
+ check_none:
338
+ '"{name}" नाम का कोई इंस्टॉल नहीं मिला (किसी और नाम से इंस्टॉल किया था? --name <नाम> जोड़ें)। पहले npx -y neuro-canvas-lms से इंस्टॉल करें।',
339
+ check_only: '{url} पर कनेक्टर की जाँच हो रही है…',
340
+ check_connected: 'यह आपके AI ऐप से जुड़ा है और इस्तेमाल के लिए तैयार है।',
341
+ cert_wait:
342
+ 'नए workers.dev पते का HTTPS प्रमाणपत्र अभी तैयार हो रहा है। पहली बार इसमें कुछ मिनट लग सकते हैं; प्रतीक्षा हो रही है…',
309
343
  pick_account_flag:
310
344
  'इस Cloudflare लॉगिन में कई खाते हैं। वही कमांड इनमें से एक जोड़कर फिर से चलाएँ:\n{list}',
311
345
  canvas_flag:
@@ -0,0 +1,61 @@
1
+ // Is a freshly deployed connector online and running the expected version?
2
+ // Ready means: /healthz answers ok with that version AND /mcp refuses an unauthenticated request
3
+ // with 401 (the OAuth layer is in front). Each request has its own timeout and the whole check a
4
+ // deadline, so it always ends. A new workers.dev address can take minutes for its DNS and HTTPS
5
+ // certificate: for those "not ready yet" errors the deadline is longer. Everything that touches
6
+ // the network or the clock can be replaced, so the behaviour is tested (verify.test.mjs).
7
+ import { isWarmingUp, CHECK_WAIT, WARMUP_WAIT } from './lib.mjs';
8
+
9
+ // fetch reports only "fetch failed"; the cause names the real problem (a TLS alert, DNS, …).
10
+ export function errorDetail(e) {
11
+ if (e?.name === 'TimeoutError' || e?.cause?.name === 'TimeoutError') return 'no answer (timeout)';
12
+ return String(e?.cause?.code ?? e?.cause?.message ?? e?.message ?? e);
13
+ }
14
+
15
+ export async function verifyDeployment(
16
+ url,
17
+ version,
18
+ {
19
+ fetcher = fetch,
20
+ sleep = ms => new Promise(r => setTimeout(r, ms)),
21
+ now = Date.now,
22
+ requestTimeout = 10 * 1000,
23
+ interval = 3 * 1000,
24
+ normalWait = CHECK_WAIT,
25
+ warmupWait = WARMUP_WAIT,
26
+ onWarmup = () => {},
27
+ } = {},
28
+ ) {
29
+ let detail = 'no answer';
30
+ let told = false;
31
+ const start = now();
32
+ for (let i = 0; now() - start < (isWarmingUp(detail) ? warmupWait : normalWait); i++) {
33
+ if (i) await sleep(interval);
34
+ try {
35
+ const h = await fetcher(`${url}/healthz`, {
36
+ headers: { 'Cache-Control': 'no-cache' },
37
+ signal: AbortSignal.timeout(requestTimeout),
38
+ });
39
+ const j = h.ok ? await h.json() : null;
40
+ if (!j?.ok) detail = `/healthz HTTP ${h.status}`;
41
+ else if (version && j.version !== version) detail = `still version ${j.version}`;
42
+ else {
43
+ const mcp = await fetcher(`${url}/mcp`, {
44
+ method: 'POST',
45
+ body: '{}',
46
+ signal: AbortSignal.timeout(requestTimeout),
47
+ });
48
+ if (mcp.status === 401)
49
+ return { ok: true, version: j.version, connected: j.connected === true };
50
+ detail = `/mcp HTTP ${mcp.status} (expected 401)`;
51
+ }
52
+ } catch (e) {
53
+ detail = errorDetail(e);
54
+ if (isWarmingUp(detail) && !told) {
55
+ told = true;
56
+ onWarmup(detail);
57
+ }
58
+ }
59
+ }
60
+ return { ok: false, detail };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuro-canvas-lms",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Neuro-CanvasLMS: read-only connector that lets Claude or ChatGPT read any Canvas LMS (remote MCP server on your own Cloudflare account)",
5
5
  "keywords": [
6
6
  "canvas",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/BloodyReginaa/Neuro-CanvasLMS.git"
23
+ "url": "git+https://github.com/BloodyReginaa/Neuro-CanvasLMS.git"
24
24
  },
25
25
  "license": "MIT",
26
26
  "author": "BloodyReginaa",
package/version.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  // The connector's version, shown on its home page and at /healthz (the installer checks that the
2
2
  // deployed version answers). Bump it together with package.json when something changes.
3
- export const VERSION = '2.10.0';
3
+ export const VERSION = '2.11.0';