From ee5a0e8d1ef35b1d8d6a4893073c5a46724eacaa Mon Sep 17 00:00:00 2001 From: Bryan Fawcett Date: Sun, 4 Oct 2026 20:27:56 +0800 Subject: [PATCH 1/5] ci: tag staging merges as patches and run the build on staging (#64) Every merge into `staging` (the live beta) is released as the next patch under the org versioning policy (nyuchi/.github#80), through the pinned reusable-staging-release.yml. The build workflow now also runs on pushes to `staging`, so the beta branch carries the same checks as master. Claude-Session: https://claude.ai/code/session_017T4NxM5wbhJ3LjHt7bnuwr Co-authored-by: Claude Opus 5.5 --- .github/workflows/build.yml | 2 +- .github/workflows/staging-version.yml | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/staging-version.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff2c504..c169327 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: build on: pull_request: push: - branches: [master] + branches: [master, staging] permissions: contents: read diff --git a/.github/workflows/staging-version.yml b/.github/workflows/staging-version.yml new file mode 100644 index 0000000..4cc02fa --- /dev/null +++ b/.github/workflows/staging-version.yml @@ -0,0 +1,25 @@ +# Versioning policy (nyuchi/.github#80): every merge into `staging` is +# released as the next PATCH, tagged on the merged commit. +name: Staging version + +on: + push: + branches: [staging] + workflow_dispatch: + inputs: + bump: + description: Override the bump (major is only ever manual). + type: choice + options: [patch, minor, major] + default: patch + +permissions: + contents: read + +jobs: + version: + uses: nyuchi/.github/.github/workflows/reusable-staging-release.yml@924e5b4d0983d31379aedbec02b990793f29c792 # main, 2026-10-04 + with: + bump: ${{ inputs.bump || '' }} + permissions: + contents: write From 1b9c5da161e8a0f3b058d47ba6776b71e00d2379 Mon Sep 17 00:00:00 2001 From: Bryan Fawcett Date: Sun, 4 Oct 2026 21:49:34 +0800 Subject: [PATCH 2/5] chore: the default branch is main (#65) * chore: the default branch is main The default branch was renamed from master to main on 2026-10-04. Point the build trigger and the README at it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017T4NxM5wbhJ3LjHt7bnuwr * ci: build on pushes to main Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017T4NxM5wbhJ3LjHt7bnuwr --------- Co-authored-by: Claude Opus 5.5 --- .github/workflows/build.yml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c169327..f63f453 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: build on: pull_request: push: - branches: [master, staging] + branches: [main, staging] permissions: contents: read diff --git a/README.md b/README.md index 0b64c8a..5251753 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ![Astro](https://img.shields.io/badge/Astro-7-BC52EE?style=flat-square&logo=astro&logoColor=white) ![Vercel](https://img.shields.io/badge/Vercel-deployed-000000?style=flat-square&logo=vercel&logoColor=white) -**Version:** 3.0.0 | **Live:** [learning.nyuchi.com](https://learning.nyuchi.com) | **Default branch:** `master` | **Deploy:** Vercel +**Version:** 3.0.0 | **Live:** [learning.nyuchi.com](https://learning.nyuchi.com) | **Default branch:** `main` | **Deploy:** Vercel --- @@ -54,7 +54,7 @@ every Nyuchi surface at once. ## Hosting -Vercel, from `master`. The site is fully static — every page is known at build +Vercel, from `main`. The site is fully static — every page is known at build time, so nothing renders per request. Response headers, including the CSP, are declared in `vercel.json`. From 5883db517f51b8d8e0789890c8a201e34b5f8d22 Mon Sep 17 00:00:00 2001 From: Bryan Fawcett Date: Tue, 6 Oct 2026 07:46:40 +0800 Subject: [PATCH 3/5] docs(legal): privacy on Zimbabwe's Act and the GDPR; every extension claim matches its code (#66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(legal): every claim about the extension matches its code, for 0.8.2 and 0.8.4 Checked against the extension at 75876d4 (docs/toddle-data-fields.md, the security reviews, SECURITY.md, licence.js, background.js and the code). - Flags: from 0.8.4 they are shown as Toddle shows them and the free switch hides them everywhere; before 0.8.4 they were hidden by default. Unless flags are hidden, the sidebar asks Toddle for them each time it opens. - Memory: answers stay in the tab until it is closed or reloaded, reused for at most 5 minutes (2 for a student's day); never written to storage. - Storage: licenceCheck and licenceViewer in the extension's storage, and the four values on Toddle's site, each described. Drops a "message button style" value the code does not have. - Switches: adds gradebook tools and the Attendance dashboard's details. - Licence keys may, not must, name who they are for. - Reads: gradebook, home page, profile page and Attendance dashboard, plus the sign-in headers and academic year noted from Toddle's own requests. - What leaves the device, listed exactly, student photos included. - Security page: the review of 6 October 2026 (findings 13 to 16) and what code inside Toddle's page cannot promise; says which versions it covers. - Version facts in one place (extensionVersions); dated 6 October 2026. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CoDNYz27iJ7o8PPztCzTpq * docs(legal): privacy rests on Zimbabwe's Act and the GDPR; fix missing spaces - The privacy policy names the two laws Nyuchi holds itself to: Zimbabwe's Cyber and Data Protection Act [Chapter 12:07], and the EU and UK GDPR, applied to everyone. "The law we follow" names POTRAZ, says where data goes and how transfers out of the EU and UK are covered. The rights section lists each right, the one-month answer and where to complain (POTRAZ, the ICO, or an EU authority). Each legal basis carries its GDPR article. - Breaches go to POTRAZ within 24 hours (Zimbabwe's Act), and to an EU or UK regulator within 72 where the GDPR requires it (privacy and data pages). - Student privacy names both laws. Singapore's PDPA is no longer named, in the pages or the consent notes. The facts live once, in `dataProtection`. - compressHTML off: Astro's compression dropped the space where a line break sat next to an inline tag ("Applies to…"), about 90 times across the legal and product pages. Now none; CSP hashes unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CoDNYz27iJ7o8PPztCzTpq --------- Co-authored-by: Bryan Fawcett --- CHANGELOG.md | 34 +++ SECURITY.md | 6 +- astro.config.mjs | 4 + public/llms.txt | 93 ++++--- src/components/CookieBanner.astro | 2 +- src/data/consent.ts | 17 +- src/data/legal.ts | 129 ++++++++-- src/pages/legal/cookies.astro | 20 +- src/pages/legal/data.astro | 159 +++++++++--- src/pages/legal/privacy.astro | 142 ++++++++--- src/pages/legal/security.astro | 240 ++++++++++++++---- src/pages/legal/student-privacy.astro | 19 +- src/pages/legal/terms.astro | 22 +- .../legal/vulnerability-disclosure.astro | 7 +- src/pages/toddle-enhancement-extension.astro | 16 +- tests/security.test.ts | 2 +- 16 files changed, 682 insertions(+), 230 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d2e3fa..65830d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ ### Changed +- **The privacy policy rests on two laws: Zimbabwe's Cyber and Data + Protection Act [Chapter 12:07] and the EU and UK GDPR**, applied to everyone. + A new "The law we follow" section names POTRAZ as Zimbabwe's regulator, says + where data goes and how transfers out of the EU and UK are covered, and the + rights section lists each right, the one-month answer and where to complain + (POTRAZ, the ICO, or an EU authority). Each legal basis carries its GDPR + article. A breach goes to POTRAZ within 24 hours, as Zimbabwe's Act requires, + and to an EU or UK regulator within 72 where the GDPR requires it. Singapore's + PDPA is no longer named. The facts live once, in `dataProtection` + (`src/data/legal.ts`). +- **The legal pages match the extension's code, for 0.8.2 and 0.8.4**, and + are dated 6 October 2026. Checked against the extension's data schema + (`docs/toddle-data-fields.md`) and its code: + - Student flags: from 0.8.4 they are shown as Toddle shows them, and the + free switch hides them everywhere; versions before 0.8.4 hid them by + default. Unless flags are hidden, the sidebar asks Toddle for a + student's flags each time it opens. + - Memory: answers stay in the tab's memory until it is closed or + reloaded, reused for at most 5 minutes (2 for a student's day). Not + "a few minutes, then discarded". + - Storage: every key, named — the last revocation check and the Toddle + account's email in the extension's storage, and the four values on + Toddle's site (`tee-settings`, `gbx-hide-flags`, `tee-course-view`, + `tee-academic-year`). The "style of Toddle's message button" value it + once listed does not exist. + - Every switch, including gradebook tools and the Attendance dashboard's + student details. + - Licence keys may, not must, name who they are for. + - What each feature reads (gradebook, home page, profile page, Attendance + dashboard, sidebar), and the sign-in headers and academic year noted + from Toddle's own requests. + - Exactly what leaves the device, student photos included. + - The Security page covers the adversarial review of 6 October 2026 + (findings 13 to 16) and what code inside Toddle's page cannot promise. - **The audit sets one advisory aside, by ID, until 2026-11-03** (CI only). GHSA-ch52-4w7c-c8xp in `http-cache-semantics` has no patched release, and astro 7.3.5 uses the package only to cache remote images during diff --git a/SECURITY.md b/SECURITY.md index 25a4278..73d3566 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,8 +3,10 @@ This repository is `learning.nyuchi.com`: a static Astro site, served by Vercel, and the home of the Toddle Enhancement Extension. -The extension's own security model, how it is tested and its independent -adversarial review are published for schools at +The extension's own security model, how it is tested, and its adversarial +reviews of 1 October 2026 (0.8.2) and 6 October 2026 (0.8.4, before it is +published), with their findings and the limits of code that runs in Toddle's +page, are published for schools at [learning.nyuchi.com/legal/security](https://learning.nyuchi.com/legal/security). The vulnerability disclosure policy, covering this site, the extension and the licence server, is at diff --git a/astro.config.mjs b/astro.config.mjs index 950056e..e90682d 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -6,6 +6,10 @@ import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ site: "https://learning.nyuchi.com", + /* Astro's HTML compression drops the space where a line break sits between + text and an inline tag ("Applies to…"), across the legal pages. + The pages are small; correct words matter more than a few bytes. */ + compressHTML: false, adapter: vercel(), // Fully static: every page is known at build time, so there is nothing to diff --git a/public/llms.txt b/public/llms.txt index 6d86cc1..6d7b550 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -19,13 +19,15 @@ community-based platforms. Nyuchi Web Services is its development division. item, so a teacher sees the whole class at once instead of opening a side panel per student. It also adds a switch that hides Toddle's student flags across the product, so a gradebook can be projected or screen-shared without - a flag being visible. Flags are hidden by default; a staff member shows them - deliberately, with the switch or an eye button for one student. - It hides flags, not names. Free: the flag switch and a "My classes" filter on the - Toddle home page. With a licence: the per-criterion gradebook columns, CSV - export, primary teacher names, and the student sidebar. The sidebar (version - 0.8.2) works anywhere a student appears in Toddle (class pages, the - Attendance dashboard's Periods, Students and Excusals tabs, name links, + a flag being visible. From version 0.8.4, flags are shown as Toddle shows + them, and a staff member can hide them everywhere with the switch, which is + free; an eye button then shows one student's flags. Versions before 0.8.4 + hid them by default. It hides flags, not names. Free: the flag switch and + a "My classes" filter on the Toddle home page. With a licence: the + per-criterion gradebook columns, CSV export, primary teacher names, the + Attendance dashboard's details, and the student sidebar. The sidebar + (since version 0.8.2) works anywhere a student appears in Toddle (class + pages, the Attendance dashboard's Periods, Students and Excusals tabs, name links, gradebook student cells) and shows a photo, grade, age and class of, Student Group, room number, homeroom advisor, contacts with relationship, phone and email, a "Now" card (current attendance code, block, class, room and @@ -37,30 +39,46 @@ community-based platforms. Nyuchi Web Services is its development division. tab, each student's year group and current class and teacher appear under their name, and the current block's column is outlined. It reads Toddle in the teacher's own browser, for the teacher at the screen, through an exact - allowlist of read-only queries sent only to Toddle, and never writes. - Nothing it reads leaves the browser; it transmits nothing from Toddle to - anyone: no account, no analytics, no tracking. Licence keys are checked - offline on the device; an individual key carries the buyer's email and an - organisation key the school's email domain, compared with the signed-in - Toddle account inside the browser only. An organisation licence is one key - for the school's email domain: anyone signed in to Toddle with an address - at that domain is covered. Student flags are hidden by default - and fetched only when a staff member chooses to show them. It has two - outside connections, neither carrying Toddle data. (1) Opt-in feedback: the - toolbar menu's "Send feedback" form, which, only when the person presses + allowlist of read-only queries sent only to Toddle, and never writes. It + reads, for the teacher's own account: assessment results, descriptors and + rubric levels as the school defined them in Toddle; class staff, for + primary teacher names; the Attendance dashboard's year groups and the + teacher of each student's current class; and, in the sidebar, the + student's details, contacts, classes, today's timetable and attendance, + and their flags when they are shown. It notes the sign-in headers and + academic year from Toddle's own requests only to ask Toddle as the + teacher; it never asks for a password. Nothing it reads leaves the browser + except back to Toddle; it transmits nothing from Toddle to anyone else: no + account, no analytics, no tracking. Student photos load from wherever + Toddle serves them, as on Toddle's own page. Licence keys are checked + offline on the device; a key carries the buyer's email and may name who + it is for (the buyer's email, or for an organisation licence the school's + email domain), which is compared with the signed-in Toddle account inside + the browser only. An organisation licence is one key for the school's + email domain: anyone signed in to Toddle with an address at that domain is + covered. Unless flags are hidden, the sidebar asks Toddle for a student's + flags each time it opens, and keeps them only while it is open. Apart + from Toddle, it has two outside connections, neither carrying Toddle + data. (1) Opt-in feedback: the toolbar menu's "Send feedback" form, which, only when the person presses Send, sends what they typed (topic, message, and an email only if they enter one) plus the extension's version number to Nyuchi's feedback form, processed by Formspree; no student data, no page address, no licence key. - (2) Only while a licence is entered, once a day, the background worker - makes one plain request to https://licences.nyuchi.dev/v1/revocations, + (2) Only while a licence is entered, at most once a day, the background + worker makes one plain request to https://licences.nyuchi.dev/v1/revocations, sending no licence key, no identifiers, no cookies and no Toddle data; it downloads a Nyuchi-signed list of SHA-256 fingerprints of cancelled keys and checks its own key locally. Cloudflare sees the IP address as with any - web request; Nyuchi does not log it. If the check fails, the extension - keeps working. The welcome page it opens on - install is part of the extension and sends nothing. It stores, on the - user's own machine, only its switch settings and the licence key if there - is one; no student data is stored on the device. Rubric levels and + web request; Nyuchi does not log it. If the list cannot be fetched, the + last result stands. Anything else (a CSV file, an email or phone link, a + message in Toddle's own chat) happens only when the teacher does it. The + welcome page it opens on install is part of the extension and sends + nothing. It stores, on the user's own machine, its switch settings, the + licence key if there is one, the result of the last check for cancelled + keys, and the email of the Toddle account last seen signed in (to check + who a licence is for); and, in Toddle's own site storage, a copy of the + switches, whether flags are hidden, the My classes choice and the academic + year Toddle is showing. No student data is stored: what it reads stays in + the tab's memory until the tab is closed or reloaded. Rubric levels and descriptors are read from whatever a school has configured in Toddle; none are defined in the extension. Security: https://learning.nyuchi.com/legal/security @@ -76,7 +94,7 @@ community-based platforms. Nyuchi Web Services is its development division. ## Legal -All dated 1 October 2026. Operated by Nyuchi Web Services, the development +All dated 6 October 2026. Operated by Nyuchi Web Services, the development division of Nyuchi Africa (Private) Limited, Harare, Zimbabwe. - [Privacy policy](https://learning.nyuchi.com/legal/privacy): what the @@ -100,8 +118,10 @@ division of Nyuchi Africa (Private) Limited, Harare, Zimbabwe. next daily check for cancelled keys. - [Data handling](https://learning.nyuchi.com/legal/data): for schools. What the extension reads, where it is processed (only in the teacher's browser), - what it stores (settings and licence key; no student data), what leaves the - browser (only requests to Toddle, plus opt-in feedback), what Nyuchi holds, + what it stores (settings and licence details; no student data), what leaves + the browser (requests to Toddle and student photos from Toddle, plus + opt-in feedback and the data-free daily cancelled-keys check), what Nyuchi + holds, the services that process it, retention, breach notification, and a data processing agreement on request. - [Student privacy](https://learning.nyuchi.com/legal/student-privacy): the @@ -116,11 +136,15 @@ division of Nyuchi Africa (Private) Limited, Harare, Zimbabwe. checked offline; no student data stored), how it is tested (closed-loop tests, adversarial-review regression tests, an end-to-end network recorder that fails on any non-Toddle request, every - release gated on them), the independent adversarial review of 1 October 2026 - (two High, one Medium and several Low findings, all fixed in 0.8.2 with - tests), and the residual limit (scripts Toddle itself loads share the page - and the teacher's access regardless of any extension; Toddle's pages send no - Content-Security-Policy). + release gated on them), two adversarial reviews (1 October 2026: two High, + one Medium and several Low findings, all fixed in 0.8.2 with tests; + 6 October 2026: two Medium and two Low findings in the code for 0.8.4, all + fixed before it is published), and the residual limits (scripts Toddle + itself loads share the page and the teacher's access regardless of any + extension; Toddle's pages send no Content-Security-Policy; a script + already listening in Toddle's page can replay the extension's per-load + secrets in that browser or answer its queries with false data, but cannot + make anything leave the browser or write to Toddle). - [Vulnerability disclosure policy](https://learning.nyuchi.com/legal/vulnerability-disclosure): report to security@nyuchi.com with the subject "Security"; acknowledgement within 3 working days, triage within 10; safe harbour for good-faith @@ -150,7 +174,8 @@ Please keep one distinction straight, because it is the one people get wrong. This WEBSITE uses Google Analytics, behind a consent banner: it sets no cookies and measures nothing unless the visitor presses Accept. The EXTENSION does not measure anything at all, with or without consent. The extension contains no analytics, no telemetry and no -tracking, and nothing it reads leaves the browser it runs in. Its only outside +tracking, and nothing it reads leaves the browser it runs in, except back to +Toddle. Its only outside connections are feedback a person writes and chooses to send from its menu, and, while a licence is entered, a daily download of a signed list of cancelled keys that sends no data. A question about diff --git a/src/components/CookieBanner.astro b/src/components/CookieBanner.astro index d204c3a..5edf295 100644 --- a/src/components/CookieBanner.astro +++ b/src/components/CookieBanner.astro @@ -11,7 +11,7 @@ * 2. Nothing optional starts on. Every toggle in the panel is unchecked until * someone checks it, so closing the banner without answering leaves you * where rejecting does. - * 3. The record carries a timestamp and a version. All three regimes put the + * 3. The record carries a timestamp and a version. Both laws (legal.ts) put the * burden on us to show consent was given, and "the cookie was there" is * not that. * diff --git a/src/data/consent.ts b/src/data/consent.ts index a9bbe18..d976f6b 100644 --- a/src/data/consent.ts +++ b/src/data/consent.ts @@ -1,8 +1,9 @@ /** * What this site asks consent for, declared once. * - * Three regimes apply to the people who read this site — staff at - * international schools — and they agree on more than they differ: + * Two laws set the standard (legal.ts, dataProtection): Zimbabwe's Act, + * because Nyuchi is Zimbabwean, and the EU and UK GDPR, the most widely used, + * applied to everyone. They agree on more than they differ: * * UK/EU GDPR consent must be freely given, specific, informed * and unambiguous; granular per purpose, not bundled; @@ -11,12 +12,8 @@ * Zimbabwe CDPA 12:07 "freely given specific and informed indication"; * withdrawal; the controller must be able to show * consent was obtained - * Singapore PDPA purposes notified before collection; consent not a - * condition of service beyond what is reasonable; - * withdrawal honoured * - * The shared requirements drive the design, so one implementation serves all - * three: nothing optional is on until it is chosen, each purpose is chosen + * The shared requirements drive the design, so one implementation serves both: nothing optional is on until it is chosen, each purpose is chosen * separately, refusing everything is one press, and the record carries a * timestamp and a version so it can be produced later. * @@ -27,9 +24,9 @@ * two third parties. * * This is the engineering, not legal advice. Someone qualified should read the - * privacy policy before it is relied on, and the PDPA separately expects a named - * data protection officer whose business contact details are published — that is - * a person to appoint, not a thing to code. + * privacy policy before it is relied on. Zimbabwe's licensing regulations for + * data controllers also expect a data protection officer to be appointed — a + * person to appoint, not a thing to code. */ export type ConsentCategory = { diff --git a/src/data/legal.ts b/src/data/legal.ts index 15d156c..29b02b4 100644 --- a/src/data/legal.ts +++ b/src/data/legal.ts @@ -10,7 +10,7 @@ export const legal = { /** Last substantive change, for every legal page. Update when a claim changes, not on typos. */ - updated: "2026-10-03", + updated: "2026-10-06", entity: "Nyuchi Africa (Private) Limited", shortEntity: "Nyuchi", country: "Zimbabwe", @@ -31,7 +31,37 @@ export const legal = { support: { intercomAppId: "f1vga504" }, } as const; -/** The date above as people write it: "1 October 2026". */ +/** + * The two data protection laws Nyuchi holds itself to, said once. Zimbabwe's + * Act because Nyuchi is a Zimbabwean company; the EU and UK GDPR because they + * are the most widely used standard, so we apply them to everyone, wherever + * they are. Where the two differ, the stricter one wins (the breach deadline: + * Zimbabwe's 24 hours to the regulator is shorter than the GDPR's 72). + */ +export const dataProtection = { + home: { + law: "Cyber and Data Protection Act [Chapter 12:07]", + authority: + "the Postal and Telecommunications Regulatory Authority of Zimbabwe (POTRAZ)", + authorityShort: "POTRAZ", + authorityUrl: "https://www.potraz.gov.zw", + /** Notice to the regulator after a breach is discovered (s. 19). */ + breachHours: 24, + }, + gdpr: { + /** Notice to a supervisory authority, where a breach must be notified. */ + breachHours: 72, + /** Time to answer a request about your data, free of charge. */ + answerWithin: "one month", + ukAuthority: "the Information Commissioner's Office (ICO)", + ukAuthorityUrl: "https://ico.org.uk/make-a-complaint/", + /** Every EU and EEA supervisory authority, from the EDPB. */ + euAuthoritiesUrl: + "https://www.edpb.europa.eu/about-edpb/about-edpb/members_en", + }, +} as const; + +/** The date above as people write it: "6 October 2026". */ export const updatedLabel = new Date( `${legal.updated}T00:00:00Z`, ).toLocaleDateString("en-GB", { @@ -76,10 +106,29 @@ export const pricing = [ }, ] as const; +/** + * Which versions of the extension the legal pages describe. `current` is what + * teachers have from the Chrome Web Store (the v0.8.3 tag carries the same + * code). `next` is the release under Unreleased in the extension's changelog: + * reviewed, not yet published. Claims that differ between the two say "from + * version 0.8.4", so they stay true on both sides of the release. When `next` + * ships, move it to `current` and drop the "before" wording. + */ +export const extensionVersions = { + current: "0.8.2", + sameCodeAs: "0.8.3", + next: "0.8.4", + /** The oldest version security reports are accepted for. */ + supportedFrom: "0.8.2", +} as const; + +const { next } = extensionVersions; + /** * What the extension does with data. Each of these is a claim that can be - * checked against the source, and each maps to an answer on the Chrome Web - * Store privacy form. + * checked against the source (the extension's docs/toddle-data-fields.md is + * the full schema, checked against its code), and each maps to an answer on + * the Chrome Web Store privacy form. */ export const extensionDataFacts = [ { @@ -90,27 +139,27 @@ export const extensionDataFacts = [ { claim: "It reads; it does not write.", detail: - "It reads what Toddle already lets you see — gradebook results, class staff, student details — through Toddle's own interface, reusing the session you are already signed in with. Every request it may send is listed word for word in the extension, and anything else is refused; write operations are blocked outright rather than merely avoided, so it cannot change your gradebook even by accident. Its message buttons open Toddle's own chat window and send nothing themselves: a message is sent, if at all, by you, through Toddle, under your school's messaging rules.", + "It reads what Toddle already lets you see, through Toddle's own interface, reusing the session you are already signed in with. Every request it may send is listed word for word in the extension, and anything else is refused. Write operations are blocked outright rather than merely avoided, so it cannot change your gradebook even by accident. On the home page and the Attendance dashboard it adds a few fields to Toddle's own request instead of sending a second one, and takes them out again before Toddle's page sees the answer. Its message buttons open Toddle's own chat window and send nothing themselves: a message is sent, if at all, by you, through Toddle, under your school's messaging rules.", }, { - claim: "What it reads for the student sidebar, and when.", + claim: "What it reads, feature by feature.", detail: - "Only when a teacher opens a student or a class, and only for the teacher at the screen: the student's name, photo, year group and age (the date of birth itself is never shown), email, student ID and enrolment date; their contacts' names, relationships, phone numbers and emails; their homeroom advisor; the school's Student Group and Room number fields (other additional fields are never shown); today's timetable and attendance marks; and each class's teachers with their display titles and emails. Toddle answers with what that teacher's own account is allowed to see. Nothing is written to disk: answers are held in the page's memory for a few minutes, then discarded, and are gone when the tab closes.", + "Toddle answers with what the signed-in teacher's own account may see, and no more. The gradebook tools, when a teacher expands an assessment: its assessment tools as Toddle defines them (rubric criteria, descriptors and levels, grade scales and their colours, checklist items, scores, standards and learning goals), and each assigned student's name, email, student ID, submission status and results, including written responses and comments. No descriptor or level is written in the extension. The home page: each class's staff, to show its primary teacher and the My classes filter. A student's profile page: the teachers of each of the student's classes. The Attendance dashboard: each student's year group and the names of the periods, from Toddle's own request, and the primary teacher of the class each student is in now, for the “Now: class · teacher” line. The student sidebar, when a teacher opens a student or a class: the student's name, photo, year group and age (the date of birth is used to work out the age and is never shown), email, student ID and enrolment date; their family accounts and contacts, with names, relationships, phone numbers and emails; their homeroom advisor; the school's additional profile fields, of which only Student Group and Room number are ever shown; today's timetable and attendance marks; their active flags, when flags are shown; and each class's teachers, with their display titles and emails.", }, { - claim: "Student flags stay hidden until someone chooses to show them.", - detail: - "Flags are hidden by default across Toddle, on a new install and for anyone who never touched the switch; a staff member shows them deliberately, with the switch or with the eye button for one student. The extension's own sidebar fetches a student's flags from Toddle only when someone chooses to show them, and forgets them when the sidebar closes. The flag switch is free and always will be.", + claim: + "Student flags: shown as Toddle shows them, hidden with one free switch.", + detail: `From version ${next}, flags are shown as Toddle shows them. A staff member can hide them everywhere in Toddle with the switch, in Toddle's top bar or the extension's menu; the eye button then shows one student's flags. Versions before ${next} hid flags by default. Unless flags are hidden, the sidebar asks Toddle for a student's active flags each time it opens; while they are hidden, it asks only when someone presses the eye button. It keeps them only while that student's sidebar is open. Hiding flags changes only what is drawn on the screen, never Toddle's data. The flag switch is free and always will be.`, }, { - claim: "It never sees your password.", + claim: "Your Toddle sign-in stays with Toddle.", detail: - "Authentication stays inside Toddle's own page. The part of the extension that draws the interface cannot read the authentication token, by design.", + "The extension never asks for your password and never stores it. To ask Toddle as you, it notes the sign-in headers from Toddle's own requests, and the academic year Toddle is showing. The headers are held in the tab's memory, in one script inside Toddle's page: the extension's other parts never see them, they are never stored, and they are sent nowhere but Toddle's own interface. The academic year number is kept in Toddle's site storage (see below) so the sidebar asks for the right year, and is sent nowhere but Toddle.", }, { - claim: "Nothing it reads leaves your browser.", + claim: "Exactly what leaves your device.", detail: - "There is no account, no analytics, no telemetry, no tracking, no advertising and no remote code. No student data, no teacher data and no school data leaves your browser: what it reads from Toddle goes back to Toddle's own screen and nowhere else. It reads Toddle for the teacher at the screen, and for no one else. It makes two connections outside Toddle, described next, and neither carries anything from Toddle.", + "Nothing it reads from Toddle goes anywhere but back to Toddle. There is no account, no analytics, no telemetry, no tracking, no advertising and no remote code. The extension makes these requests and no others: read-only requests to Toddle's own interface, with your Toddle session; student photos, loaded from the address Toddle gives for each one, wherever Toddle serves them, as Toddle's own page does; the feedback form, only when you press Send; and, only while a licence key is entered, the daily check for cancelled keys, which carries nothing. The last two are described next. Anything else happens only when you do it: saving a CSV file, opening an email or phone link, or sending a message in Toddle's own chat. Like any web request, each one lets the server that receives it see your IP address and browser type.", }, { claim: "Outside connection one, only when you choose it: feedback.", @@ -123,41 +172,65 @@ export const extensionDataFacts = [ "When you first install it, the extension opens a welcome page. That page is part of the extension itself, not a website, and loading it contacts nothing.", }, { - claim: "The only things it stores are your settings and your licence key.", + claim: "What it stores: settings and licence details, and no student data.", + detail: `In Chrome's storage for the extension, on your device: your switches (the extension on or off, student flags, the student sidebar, My classes, primary teacher names, gradebook tools, and student details on the Attendance dashboard); your licence key, if you entered one; the result of the last check for cancelled keys (whether your key is on the list, the list's date and when the check was made, never the list itself); and the email address of the Toddle account last seen signed in, with the time, so the extension can check who a licence is for. That email is read on every Toddle page, with or without a licence key, and stays on the device. In the browser's storage for Toddle's own site: tee-settings, a copy of your switches and of which features the licence allows (no key and no email); gbx-hide-flags, whether flags are hidden, so they are hidden before the page is drawn; tee-course-view, whether you chose My classes in the home page filter; and tee-academic-year, the number of the academic year Toddle last asked for. Nothing it reads about students, classes, results or attendance is ever written to storage. Versions before ${next} have three switches: student flags, the student sidebar and primary teacher names.`, + }, + { + claim: "What it holds in memory, and for how long.", detail: - "Your switch preferences — student flags shown or hidden, the student sidebar, primary teacher names, the My classes filter — and the licence key if you have one. All of it is stored on your own machine, and none of it is sent anywhere. No student data is stored on the device at all.", + "Answers from Toddle are held in the Toddle tab's memory, never written to storage, until the tab is closed or reloaded or the browser quits. Moving between Toddle pages without a reload does not clear them. A student's or a class's details are reused for at most 5 minutes, and a student's day for at most 2, before Toddle is asked again. A student's flags are never reused, and are kept only while that student's sidebar is open.", }, { claim: "Your licence is checked on your machine, not by asking us.", detail: - "A licence key is a signed statement that the extension verifies locally. An individual key carries the buyer's email address, and an organisation key the school's email domain; the extension compares that with the Toddle account signed in, inside the browser only, and sends it nowhere. Activating a key contacts nothing, which is why it works without a connection.", + "A licence key is a signed statement that the extension verifies locally. It carries the buyer's email address. A key may also name who it is for: the buyer's email, or for an organisation licence the school's email domain. Where it does, the extension compares that with the Toddle account signed in, inside the browser only, and sends it nowhere. Activating a key contacts nothing, which is why it works without a connection.", }, { claim: "Outside connection two, only while a licence is entered: a daily check for cancelled keys.", detail: - "Once a day, and only while a licence key is entered, the extension's background worker makes one plain request to https://licences.nyuchi.dev/v1/revocations. It sends no licence key, no identifiers, no cookies and no Toddle data. It downloads a list, signed by Nyuchi, of the fingerprints (SHA-256 hashes) of cancelled keys, and checks its own key against that list on your machine. Cloudflare, which runs the server, sees your IP address as with any web request; Nyuchi does not log it. If the check fails, the extension keeps working.", + "At most once a day, and only while a licence key is entered, the extension's background worker makes one plain request to https://licences.nyuchi.dev/v1/revocations; a key just entered is checked within 5 minutes. It sends no licence key, no identifiers, no cookies and no Toddle data. It downloads a list, signed by Nyuchi, of the fingerprints (SHA-256 hashes) of cancelled keys, checks its own key against that list on your machine, and keeps only the result. Cloudflare, which runs the server, sees your IP address as with any web request; Nyuchi does not log it. If the list cannot be fetched, the last result stands, so a network outage never switches a licence off.", }, { claim: "Exports are local.", detail: - "The CSV export is generated in your browser and saved by your browser, to wherever your downloads go. It does not pass through any service of ours.", + "The CSV export is generated in your browser and saved by your browser, to wherever your downloads go. It does not pass through any service of ours. Once saved, the file is yours, under your school's own rules for files.", + }, + { + claim: "Removing it removes what it stored.", + detail: + "Uninstalling the extension deletes everything in its own storage. The four small values on Toddle's site stay until the browser's data for web.toddleapp.com is cleared; none of them is about a student. Removing a licence key deletes the key; the last check's result and the Toddle account's email stay until the extension is uninstalled.", }, ] as const; /** * The extension's security posture, for the Security page. Each line is a - * claim about the 0.8.2 build that can be checked against its source and - * tests; when the extension changes, this changes with it. + * claim about the published build (`current`) or the reviewed next one + * (`next`) that can be checked against the extension's source, tests, + * SECURITY.md and docs/security-review.md; when the extension changes, this + * changes with it. */ export const security = { - /** The extension version these statements describe. */ - version: "0.8.2", - /** The independent adversarial review. */ - review: { - date: "2026-10-01", - label: "1 October 2026", - }, + /** The published version these statements describe. */ + version: extensionVersions.current, + /** The reviewed version not yet published, which they also describe. */ + next: extensionVersions.next, + supportedFrom: extensionVersions.supportedFrom, + /** The adversarial reviews, oldest first. */ + reviews: [ + { + date: "2026-10-01", + label: "1 October 2026", + version: extensionVersions.current, + findings: "1 to 12", + }, + { + date: "2026-10-06", + label: "6 October 2026", + version: extensionVersions.next, + findings: "13 to 16", + }, + ], /** How to report a vulnerability. */ report: { email: "security@nyuchi.com", diff --git a/src/pages/legal/cookies.astro b/src/pages/legal/cookies.astro index a402efe..312cd45 100644 --- a/src/pages/legal/cookies.astro +++ b/src/pages/legal/cookies.astro @@ -193,13 +193,19 @@ const rows = [ The extension

- The {toddleExtension.name} sets no cookies and contains no analytics. It keeps your - switch settings and your licence key, if you have one, in Chrome's storage for the - extension, on your own device. On Toddle's own site it also keeps a few housekeeping - values in the browser's storage: a copy of the switches and of which features the - licence allows (not the key), whether flags are hidden, the My classes choice, the - number of the academic year Toddle is showing, and the style of Toddle's message - button. None of it is about a student, and none of it is sent anywhere. The + The {toddleExtension.name} sets no cookies and contains no analytics. In Chrome's + storage for the extension, on your own device, it keeps your switch settings, your + licence key if you have one, the result of its last check for cancelled keys, and the + email address of the Toddle account last seen signed in, so it can check who a + licence is for. On Toddle's own site it keeps four small values in the browser's + storage: tee-settings, a copy of the switches and of which features the + licence allows (not the key, and no email); gbx-hide-flags, whether + flags are hidden; tee-course-view, whether you chose My classes in the + home page filter; and tee-academic-year, the number of the academic year + Toddle is showing. None of it is about a student, and the extension sends none of it + anywhere. + Uninstalling the extension deletes its own storage; the four values on Toddle's site + stay until you clear the browser's data for Toddle's site. The data handling and security pages have the detail.

diff --git a/src/pages/legal/data.astro b/src/pages/legal/data.astro index 2773f11..9a3bd6c 100644 --- a/src/pages/legal/data.astro +++ b/src/pages/legal/data.astro @@ -9,7 +9,13 @@ * (nyuchi-licence-server, schema.sql). Nothing here is a legal conclusion. */ import BaseLayout from "../../layouts/BaseLayout.astro"; -import { legal, refunds, updatedLabel } from "../../data/legal"; +import { + dataProtection, + extensionVersions, + legal, + refunds, + updatedLabel, +} from "../../data/legal"; import { toddleExtension } from "../../data/site"; const title = "Data handling — Nyuchi Learning"; @@ -103,59 +109,130 @@ const subprocessors = [ class="mt-4 space-y-2 text-body text-muted-foreground list-disc list-outside pl-5 marker:text-foreground/40" >
  • - The gradebook: assessments, their criteria - and scales, and each student's results and submission status, to draw per-criterion - columns and the CSV export. + The gradebook, when a teacher expands an + assessment: its tools as Toddle defines them (rubric criteria, descriptors and + levels, grade scales and their colours, checklist items, scores, standards and + learning goals), and each student's name, email, student ID, submission status and + results, including written responses and comments, to draw per-criterion columns + and the CSV export. No descriptor or level is written in the extension.
  • - Classes and staff: each class's teachers, - with display titles and emails, to show primary teachers, My classes and the class - view. + The home page: each class's staff, to show + its primary teacher and the My classes filter. On a student's profile page, the + teachers of each of the student's classes. +
  • +
  • + The Attendance dashboard: each student's + year group and the names of the periods, from Toddle's own request, and the primary + teacher of the class each student is in now, for the “Now: class · teacher” line.
  • The student sidebar, when a teacher opens - a student: name, photo, year group and age (the date of birth is never shown), - email, student ID, enrolment date, contacts with relationship, phone numbers and - email, homeroom advisor, the school's Student Group and Room number fields, and - today's timetable and attendance marks. + a student or a class: name, photo, year group and age (the date of birth is used to + work out the age and is never shown), email, student ID, enrolment date, family + accounts and contacts with relationship, phone numbers and email, homeroom advisor, + the school's additional profile fields (only Student Group and Room number are ever + shown), today's timetable and attendance marks, and each class's teachers with + their display titles and emails. +
  • +
  • + Student flags. From version {extensionVersions.next}, + flags are shown as Toddle shows them, and a staff member can hide them everywhere + with the free flag switch; versions before {extensionVersions.next} hid them by + default. Unless flags are hidden, the sidebar + asks Toddle for a student's active flags each time it opens; while they are + hidden, only when someone presses its eye button. It keeps them only while that + student's sidebar is open. +
  • +
  • + From Toddle's own requests: the sign-in + headers, so it can ask Toddle as the teacher, and the academic year Toddle is + showing, so the sidebar asks for the right year. The headers stay in the tab's + memory, in one script inside Toddle's page, and are never stored. Neither is sent + anywhere but Toddle. The extension never asks for the teacher's password. +
  • +
  • + The signed-in account's email, on every + Toddle page, to check who a licence is for.
  • - A student's flags are fetched only when a staff member chooses to show them, and are - forgotten when the sidebar closes. Every query it can send is on a fixed, read-only - list; it never writes to Toddle. + Every query it can send is on a fixed, read-only list; it never writes to Toddle. On + the home page and the Attendance dashboard it adds a few fields to Toddle's own + request instead of sending a second one, and takes them out again before Toddle's + page sees the answer.

    Where it is processed

    In the browser on the teacher's own device, and nowhere else. Answers from Toddle are - held in the page's memory for a few minutes so that one view does not ask twice, then - discarded, and are gone when the tab closes. + held in the Toddle tab's memory, never written to storage, until the tab is closed or + reloaded or the browser quits. A student's or a class's details are reused for at + most 5 minutes, and a student's day for at most 2, before Toddle is asked again; a + student's flags are never reused.

    What it stores

    - The teacher's switch settings and licence key, in Chrome's storage for the extension - on that device; and a few housekeeping values in Toddle's site storage (a copy of the - switches and of which features the licence allows, whether flags are hidden, the My - classes choice, the academic year Toddle is showing, and the style of Toddle's - message button). No student data is stored. + In Chrome's storage for the extension, on that device: +

    +
      +
    • + the teacher's switches: the extension on or off, student flags, the student + sidebar, My classes, primary teacher names, gradebook tools, and student details on + the Attendance dashboard (versions before {extensionVersions.next} have three: + student flags, the student sidebar and primary teacher names); +
    • +
    • the licence key, if one was entered;
    • +
    • + the result of the last check for cancelled keys: whether the key is on the list, the + list's date and when the check was made, never the list itself; +
    • +
    • + the email address of the Toddle account last seen signed in, with the time, to check + who a licence is for. It is read on every Toddle page, with or without a licence + key, and is never sent anywhere. +
    • +
    +

    + And four small values in the browser's storage for Toddle's own site: tee-settings, + a copy of the switches and of which features the licence allows (no key and no + email); gbx-hide-flags, whether flags are hidden; tee-course-view, + whether the teacher chose My classes in the home page + filter; and tee-academic-year, the number of the academic year Toddle + last asked for. No student data is stored. A CSV export is a file the teacher chooses to save, and is then the school's to manage like any other download.

    +

    + Uninstalling the extension deletes everything in its own storage. The four values on + Toddle's site stay until the browser's data for web.toddleapp.com is + cleared. Removing a licence key deletes the key; the last check's result and the + account email stay until the extension is uninstalled. +

    What leaves the browser

    - Nothing it reads from Toddle. Its requests go to Toddle, with two outside connections, - neither carrying anything from Toddle: + Nothing it reads from Toddle, except back to Toddle. These are the only requests it + makes:

      +
    • + Toddle's own interface, with the teacher's + Toddle session: its read-only queries, carrying only ids, one date and fixed values. +
    • +
    • + Student photos, loaded from the address + Toddle gives for each one, wherever Toddle serves them, as Toddle's own page does. +
    • Feedback, only when the teacher presses Send - in the extension's menu: what they typed and the extension's version number, to - Formspree. + in the extension's menu: the topic, the message, an email address only if they typed + one, and the extension's version number, to Formspree.
    • A daily check for cancelled keys, only while @@ -163,14 +240,21 @@ const subprocessors = [ https://licences.nyuchi.dev/v1/revocations, sending no licence key, no identifiers, no cookies and no Toddle data. It downloads a list, signed by Nyuchi, of the SHA-256 fingerprints of cancelled keys and checks its own key locally. Cloudflare - sees the IP address, as with any web request; Nyuchi does not log it. If the check - fails, the extension keeps working. + sees the IP address, as with any web request; Nyuchi does not log it. If the list + cannot be fetched, the last result stands.

    - Licence keys are tied to their owner: an individual key carries the buyer's email - address and an organisation key the school's email domain. The extension compares that - with the Toddle account signed in, inside the browser only, and sends it nowhere. + Anything else happens only when the teacher does it: saving a CSV file, opening an + email (mailto:) or phone (tel:) link, or sending a message in + Toddle's own chat. Like any web request, each request above lets the server that + receives it see the device's IP address and browser type. +

    +

    + A licence key carries the buyer's email address, and may also name who it is for: + the buyer's email, or for an organisation licence the school's email domain. Where it + does, the extension compares that with the Toddle account signed in, inside the + browser only, and sends it nowhere.

    What Nyuchi holds

    @@ -227,9 +311,11 @@ const subprocessors = [ Nyuchi is in {legal.country}. The services above operate internationally, and most are based in the United States, so the data they handle for us may be processed outside {legal.country} and outside the country of the school. Student data from the extension - is not part of this: it does not leave the teacher's browser. If your rules require - particular safeguards for these transfers, ask us and we will tell you what applies to - each service. + is not part of this: it does not leave the teacher's browser. {legal.country} does not + have an adequacy decision from the EU or the UK, so for data that comes to us from + there we rely on the safeguards each service provides for international transfers, + such as standard contractual clauses in its data processing terms. Ask us and we will + tell you which applies to each service.

    How long it is kept

    @@ -272,8 +358,11 @@ const subprocessors = [

    If a breach affects personal data Nyuchi holds, such as purchase or feedback data, we - will tell the customers and schools affected without undue delay and, where the law - requires it, within 72 hours of becoming aware of it. We will say what happened, what + will tell {dataProtection.home.authorityShort}, Zimbabwe's data protection authority, + within {dataProtection.home.breachHours} hours of finding it, as the + {dataProtection.home.law} requires; tell the EU or UK regulator within + {dataProtection.gdpr.breachHours} hours where the GDPR requires it; and tell the + customers and schools affected without undue delay. We will say what happened, what data was involved, what we are doing about it, and who to contact.

    diff --git a/src/pages/legal/privacy.astro b/src/pages/legal/privacy.astro index deb9c73..98ad55e 100644 --- a/src/pages/legal/privacy.astro +++ b/src/pages/legal/privacy.astro @@ -9,7 +9,13 @@ * against. They cannot drift apart without someone editing the shared source. */ import BaseLayout from "../../layouts/BaseLayout.astro"; -import { extensionDataFacts, legal, security, updatedLabel } from "../../data/legal"; +import { + dataProtection, + extensionDataFacts, + legal, + security, + updatedLabel, +} from "../../data/legal"; import { CONSENT_CATEGORIES } from "../../data/consent"; import { toddleExtension } from "../../data/site"; @@ -128,8 +134,9 @@ const description = class="mt-4 space-y-2 text-body text-muted-foreground list-disc list-outside pl-5 marker:text-foreground/40" >

  • - When. Once a day, and only while a licence - key is entered. Without a licence it never happens. + When. At most once a day, and only while a + licence key is entered; a key just entered is checked within 5 minutes. Without a + licence it never happens.
  • What is sent. One plain request to @@ -140,14 +147,15 @@ const description =
  • What comes back. A list, signed by Nyuchi, of the fingerprints (SHA-256 hashes) of cancelled keys. The extension checks its own - key against the list on your machine. + key against the list on your machine, and keeps only the result.
  • Who sees what. Cloudflare, which runs the server, sees your IP address, as with any web request. Nyuchi does not log it.
  • - If it fails. The extension keeps working. + If it fails. The last result stands, so a + network outage never switches a licence off.
  • @@ -157,10 +165,11 @@ const description =

    The statements above are enforced in the software and held in place by tests that fail if the extension asks for a new permission, sends anything anywhere it is not - meant to, or sends a query that is not on its fixed, read-only list. On {security.review.label} - it had an independent adversarial review, and every finding was fixed in version - {security.version}. The Security page - sets out the security model, how it is tested, what that review found, the limits + meant to, or sends a query that is not on its fixed, read-only list. It has had two + adversarial reviews. Every finding of the first, on {security.reviews[0].label}, was + fixed in version {security.reviews[0].version}; every finding of the second, on {security.reviews[1].label}, + was fixed in version {security.reviews[1].version} before it is published. + The Security page sets out the security model, how it is tested, what the reviews found, the limits no extension can remove, and how to report a vulnerability.

    @@ -240,13 +249,13 @@ const description = the cookie policy.

    - This is written to satisfy the three regimes that actually apply to our readers: - UK and EU GDPR, Zimbabwe's - Cyber and Data Protection Act [Chapter 12:07], and - Singapore's PDPA. They differ in detail and - agree on the substance, so one approach serves all three: nothing optional before you - ask for it, each purpose chosen separately, refusal as easy as agreement, withdrawal at - any time, and a record we can produce. + This is written to meet the two laws we hold ourselves to (see + the law we follow): Zimbabwe's + {dataProtection.home.law}, and the + EU and UK GDPR. They agree on the substance, + so one approach serves both: nothing optional before you ask for it, each purpose + chosen separately, refusal as easy as agreement, withdrawal at any time, and a record + we can produce.

    One detail we would rather state than have you discover. While consent is denied, @@ -300,12 +309,12 @@ const description = details.

  • - Your key contains your email. That is how - a key is tied to a person; an organisation key carries the school's email domain - instead. The extension compares it with the Toddle account signed in, inside your - browser only. The key is stored in your own browser and checked there — the - extension does not send it to us or to anyone else, not even in the daily check for - cancelled keys. + Your key contains your email. A key may also + name who it is for: your email address, or for an organisation licence the school's + email domain. Where it does, the extension compares that with the Toddle account + signed in, inside your browser only. The key is stored in your own browser and + checked there — the extension does not send it to us or to anyone else, not even in + the daily check for cancelled keys.
  • We do not market to you off the back of it. @@ -332,23 +341,27 @@ const description =

    For the personal data Nyuchi holds — licence purchases, feedback, support conversations and this website's analytics — {legal.entity} is the data controller, - including for the purposes of {legal.country}'s Cyber and Data Protection Act - [Chapter 12:07]. We rely on: + under {legal.country}'s {dataProtection.home.law} and under the EU and UK GDPR. Our + legal basis for each use is:

    • your consent for this website's analytics - and the support messenger, which you can withdraw at any time; + and the support messenger, which you can withdraw at any time (GDPR Article + 6(1)(a));
    • the contract with you to issue, deliver and - support a licence you buy, and the law for the records a business must keep; + support a licence you buy (Article 6(1)(b)), and + the law for the records a business must + keep (Article 6(1)(c));
    • your request when you send feedback or write - to us, so that we can read it and reply. + to us: our legitimate interest in reading and answering what you choose to send + (Article 6(1)(f)). You can ask us to delete it at any time.

    @@ -360,24 +373,83 @@ const description = and data processing agreements.

    -

    Your rights

    +

    + The law we follow +

    +

    + Zimbabwe's {dataProtection.home.law} applies + to us because {legal.entity} is a Zimbabwean company. Its data protection authority is + {dataProtection.home.authority}. +

    +

    + The EU and UK GDPR are the most widely used + standard for personal data, so we apply them to everyone, wherever you live, not only + to people in Europe. Where the two laws differ, we follow the stricter one. +

    +

    + Where your data goes. Nyuchi is in + {legal.country}, and the services that process data for us (listed on the + data handling page) are mostly in the United + States. So data you send us may be processed outside your country. {legal.country} + does not have an adequacy decision from the EU or the UK, so for data that comes to us + from there we rely on the safeguards each service provides for international + transfers, such as standard contractual clauses in its data processing terms. Ask us + and we will tell you which applies to each service. None of this involves student data + from the extension, which does not leave the teacher's browser. +

    + +

    + Your rights +

    - You can ask for access to the personal data we hold about you, have it corrected, - have it deleted where we are not required to keep it, and object to how we use it. + Whatever country you are in, you can ask us to: +

    +
      +
    • tell you what personal data we hold about you, and give you a copy;
    • +
    • correct it if it is wrong or incomplete;
    • +
    • delete it, unless the law requires us to keep it;
    • +
    • stop or limit how we use it while a question about it is settled;
    • +
    • give it to you, or to someone you choose, in a common machine-readable format;
    • +
    • stop using it where we rely on our legitimate interests;
    • +
    • + withdraw your consent at any time, without affecting what was done before you + withdrew it. +
    • +
    +

    + We make no decisions about you by automated means alone, and we do not profile you. Write to {legal.privacyEmail} and we will act on it. Wherever you are, we respect the rights your own law gives - you, including under the EU and UK GDPR. You can also complain to your data - protection authority. + >. There is no charge, and we answer within {dataProtection.gdpr.answerWithin}. +

    +

    + You can complain to a regulator, though we + would like the chance to put it right first. In Zimbabwe, that is + {dataProtection.home.authorityShort}. In the UK, it is + {dataProtection.gdpr.ukAuthority}. In the EU and EEA, it is the + data protection authority where you live, work, or where you think the problem happened.

    If something goes wrong

    - If a breach affects personal data we hold, we will tell the people and schools - affected without undue delay, and within 72 hours where the law requires it. The + If a breach affects personal data we hold, we will tell + {dataProtection.home.authorityShort} within {dataProtection.home.breachHours} hours of + finding it, as Zimbabwe's Act requires; tell the EU or UK regulator within + {dataProtection.gdpr.breachHours} hours where the GDPR requires it; and tell the + people and schools affected without undue delay. The data handling page says what we will tell you. The extension holds no student data, so a breach on our side cannot expose student data from Toddle. diff --git a/src/pages/legal/security.astro b/src/pages/legal/security.astro index 1d6fe99..7336728 100644 --- a/src/pages/legal/security.astro +++ b/src/pages/legal/security.astro @@ -8,17 +8,26 @@ * be linked to; what they say is set out here instead, and the full reporting * policy is at /legal/vulnerability-disclosure. * - * Every statement is about the build named in `security.version` - * (src/data/legal.ts). When the extension's security model changes, this page - * changes with it, and the date at the top moves. + * Every statement is about the published build named in `security.version` + * and the reviewed next one in `security.next` (src/data/legal.ts); where they + * differ, the page says which. When the extension's security model changes, + * this page changes with it, and the date at the top moves. */ import BaseLayout from "../../layouts/BaseLayout.astro"; -import { legal, security, securityMailto, updatedLabel } from "../../data/legal"; +import { + extensionVersions, + legal, + security, + securityMailto, + updatedLabel, +} from "../../data/legal"; import { toddleExtension } from "../../data/site"; const title = "Security — Nyuchi Learning"; const description = - "How the Toddle Enhancement Extension is built and tested to keep student data inside Toddle: its permissions, its closed loop, the independent adversarial review of 1 October 2026 and what it fixed, its honest limits, and how to report a vulnerability."; + "How the Toddle Enhancement Extension is built and tested to keep student data inside Toddle: its permissions, its closed loop, the adversarial reviews of 1 and 6 October 2026 and what they fixed, its honest limits, and how to report a vulnerability."; + +const [firstReview, secondReview] = security.reviews; /* The summary box. A busy IT lead should be able to stop reading after this. */ const glance = [ @@ -36,11 +45,12 @@ const glance = [ { label: "Sends", value: - "Nothing it reads, to anyone but Toddle. Two outside connections, neither carrying Toddle data: feedback, when a person presses Send; and, while a licence is entered, a daily download of a signed list of cancelled keys.", + "Nothing it reads, to anyone but Toddle. Besides Toddle itself (its own interface, and student photos from wherever Toddle serves them), two outside connections, neither carrying Toddle data: feedback, when a person presses Send; and, while a licence is entered, a daily download of a signed list of cancelled keys.", }, { label: "Stores", - value: "Settings and a licence key, on the device. No student data.", + value: + "Settings, a licence key, the last licence check's result and the email of the Toddle account last seen, on the device. No student data.", }, { label: "Tested", @@ -49,19 +59,21 @@ const glance = [ }, { label: "Reviewed", - value: `Independent adversarial review, ${security.review.label}. Every finding fixed in ${security.version}.`, + value: `Two adversarial reviews: ${firstReview.label}, every finding fixed in ${firstReview.version}; and ${secondReview.label}, every finding fixed in ${secondReview.version} before it is published.`, }, ]; const sections = [ { id: "model", label: "The security model" }, { id: "testing", label: "How it is tested" }, - { id: "review", label: "Independent review" }, + { id: "review", label: "Adversarial reviews" }, { id: "limits", label: "What no extension can promise" }, { id: "report", label: "Reporting a vulnerability" }, ]; -/* From the extension's docs/security-review.md, in plain words. */ +/* From the extension's docs/security-review.md, in plain words. The review of + 1 October 2026, findings 1 to 12 (12 is a verification note, not a + vulnerability, and is left out here). */ const findings = [ { severity: "High", @@ -127,6 +139,40 @@ const findings = [ }, ]; +/* The review of 6 October 2026, findings 13 to 16, on the code for 0.8.4. */ +const laterFindings = [ + { + severity: "Medium", + finding: + "The rebuilt flags code handed over its secret on request. A script in the page could ask it for its state, and the per-load secret came back with the answer.", + fix: "It no longer answers requests. The page half always starts first and is told of every change.", + }, + { + severity: "Medium", + finding: + "The key that signs releases could reach third-party build tools. The tools that build the extension's own pages ran with the release job's settings, the signing key among them.", + fix: "The page build gets only a short list of ordinary settings. No secret is among them, and one added later would not reach it either.", + }, + { + severity: "Low", + finding: + "An element on Toddle's page could stop the rebuilt features from starting, by taking a name the extension uses to make sure it starts only once.", + fix: "Each of those checks now looks for exactly the value the extension sets, which an element on the page cannot be.", + }, + { + severity: "Low", + finding: + "What ships was not scanned. The tests read the source code, but not the built files that ship, with React and the other packages in them.", + fix: "A test now reads the built files: no eval, no cookie or other side channel, one network call in all of it (the feedback form, never on Toddle's pages), only known addresses, closed shadow roots and no inline script.", + }, + { + severity: "Info", + finding: + "An unused package, a wrong comment in the test runner, and wording in the extension's own documents that claimed more than the code inside Toddle's page can hold.", + fix: "Removed and corrected. The limits are set out below.", + }, +]; + const severityClass: Record = { High: "badge badge-warning", Medium: "badge badge-accent", @@ -142,9 +188,11 @@ const severityClass: Record = {

    Security

    Last updated {updatedLabel}. Describes the - {toddleExtension.name}, version - {security.version}. For schools, and for the privacy, legal and IT colleagues who - review software on their behalf. + {toddleExtension.name}: version {security.version}, + the one on the Chrome Web Store ({extensionVersions.sameCodeAs} carries the same code), + and version {security.next}, reviewed and not yet published. + Where the two differ, this page says so. For schools, and for the privacy, legal and + IT colleagues who review software on their behalf.

    @@ -224,12 +272,16 @@ const severityClass: Record = { exactly, with variables of the expected shape, and sends only to Toddle's own API, checked again immediately before each request. None of the queries writes, and any write is refused, so the extension cannot change a gradebook, a record or a setting - even by accident. + even by accident. On the home page and the Attendance dashboard it adds a few fields + to Toddle's own request instead of sending a second one, and takes them out again + before Toddle's page sees the answer.

    It reuses the session the teacher is already signed in with, so Toddle answers - with what that teacher's account may already see, and no more. It never sees a - password. Its message buttons open Toddle's own chat window, on real clicks only; + with what that teacher's account may already see, and no more. It never asks for a + password. It notes the sign-in headers from Toddle's own requests, in the tab's + memory, in one script inside Toddle's page, and sends them nowhere but Toddle; they + are never stored. Its message buttons open Toddle's own chat window, on real clicks only; the extension sends no message itself, and Toddle's chat applies the school's messaging rules.

    @@ -240,8 +292,9 @@ const severityClass: Record = {

    There is no account, no analytics, no telemetry, no tracking, no advertising and no remote code. What the extension reads from Toddle goes back to Toddle's own screen - and nowhere else. It makes two connections outside Toddle, and neither carries - anything from Toddle: + and nowhere else. Besides Toddle itself (its own interface, with the teacher's + session, and student photos from the address Toddle gives for each one), it makes + two connections, and neither carries anything from Toddle:

      = { privacy policy.
    • - A daily check for cancelled keys. Once a - day, only while a licence is entered, the background worker makes one plain request + A daily check for cancelled keys. At most + once a day, only while a licence is entered, the background worker makes one plain request to https://licences.nyuchi.dev/v1/revocations. It sends no licence key, no identifiers, no cookies and no Toddle data. It downloads a list, signed by Nyuchi, of the fingerprints (SHA-256 hashes) of cancelled keys, and checks its own key against it locally. Cloudflare sees the IP address, as with any - web request; Nyuchi does not log it. If the check fails, the extension keeps - working. + web request; Nyuchi does not log it. If the list cannot be fetched, the last result + stands, so a network outage never switches a licence off.
    +

    + Anything else happens only when the teacher does it: saving a CSV file, opening an + email or phone link, or sending a message in Toddle's own chat. +

    Licence keys are checked on the device

    A licence key is a signed statement that the extension verifies on the device, - against a public key it carries. Licences are tied to their owner: an individual key - carries the buyer's email address, and an organisation key the school's email domain. - The extension compares that with the Toddle account signed in, inside the browser - only. Activating a key contacts nothing, and the key is never sent anywhere. It is - kept in the extension's own storage, which Toddle's page cannot reach. + against a public key it carries. A key carries the buyer's email address, and may + also name who it is for: the buyer's email, or for an organisation licence the + school's email domain. Where it does, the extension compares that with the Toddle + account signed in, inside the browser only, and sends it nowhere. A key that names no one + works for any account until it expires. Activating a key contacts nothing, and the + key is never sent anywhere. It is kept in the extension's own storage, which Toddle's + page cannot reach.

    No student data on the device

    - The extension stores the teacher's switch settings and licence key, plus a few - housekeeping values in Toddle's own site storage (a copy of the switches, the - academic year Toddle is showing, and a style name). No student data is stored - anywhere. What it reads about students, classes and timetables is held in memory for - at most a few minutes and is gone when the tab closes. Student flags are hidden by - default; the sidebar fetches a student's flags only when someone chooses to show - them, and forgets them when it closes. The CSV export is made in the browser and saved - where the teacher's downloads go, by the teacher's choice. + In the extension's own storage: the teacher's switches, the licence key if one was + entered, the result of the last check for cancelled keys, and the email of the + Toddle account last seen signed in, to check who a licence is for. In Toddle's own + site storage, four housekeeping values: a copy of the switches and of which features + the licence allows, whether flags are hidden, the My classes choice, and the academic + year Toddle is showing. No student data is stored anywhere. What it reads about + students, classes and timetables is held in the Toddle tab's memory until the tab is + closed or reloaded; a student's or a class's details are reused for at most 5 + minutes, and a student's day for at most 2, before Toddle is asked again. +

    +

    + From version {security.next}, student flags are shown as Toddle shows them, and a + staff member can hide them everywhere with the free flag switch; versions + before {security.next} hid them by default. Unless flags are hidden, the sidebar asks Toddle + for a student's flags each time it opens, and keeps them only while it is open. The + CSV export is made in the browser and saved where the teacher's downloads go, by the + teacher's choice.

    Protections inside the page

    @@ -298,13 +366,14 @@ const severityClass: Record = { class="mt-4 space-y-2 text-body text-muted-foreground list-disc list-outside pl-5 marker:text-foreground/40" >
  • - The sign-in token stays in Toddle's page. The part of the extension that draws its - interface cannot read it. + Toddle's sign-in headers stay in one script in Toddle's page. The extension's other + parts never see them.
  • Its public objects are frozen, so a page script cannot rewrite them.
  • Answers to its queries travel on a private channel, never broadcast, so no other - script can read them or answer in their place. + script can read them. A script already listening in the page could still answer in + their place (see the limits).
  • The query gate uses JavaScript built-ins captured before any page script runs, so a @@ -313,7 +382,8 @@ const severityClass: Record = {
  • Messages that change what is shown, such as licensed features or student flags, carry a secret shared only between the extension's own parts, new on every page - load. + load. It stops scripts that are not listening for it; a script already listening + can read it (see the limits).
  • Text read from Toddle is shown as text and never interpreted as markup, and values @@ -369,24 +439,28 @@ const severityClass: Record = {

    - Independent review, {security.review.label} + Adversarial reviews

    - On {security.review.label} the extension had an independent adversarial review, with - proofs of concept run against the real extension. It considered a malicious script - in Toddle's page, a malicious website in another tab, another extension, and crafted - responses from Toddle. It found two high-severity issues, one medium and several low. - All of them were fixed in version {security.version}, and each fix carries a - regression test. + The extension has had two adversarial reviews, with proofs of concept run against + the real extension. They considered a malicious script in Toddle's page, a malicious + website in another tab, another extension, and crafted responses from Toddle. Every + finding was fixed, and each fix carries a regression test.

    +

    {firstReview.label}

    +

    + The first review, of the code that became version {firstReview.version} (findings {firstReview.findings}), + found two high-severity issues, one medium and several low. + All of them were fixed in version {firstReview.version}. +

    { findings.map((item) => (

    {item.severity} - Fixed in {security.version} + Fixed in {firstReview.version}

    {item.finding}

    @@ -399,13 +473,47 @@ const severityClass: Record = {

    The issues were present in released versions from 0.5.0 to 0.8.1, depending on the finding. Only {security.version} and later are supportedOnly {security.supportedFrom} and later are supported; Chrome updates installed copies from the Chrome Web Store on its own, and a school that pins a version should move to the current one. The review also attempted, and found held: running a query outside the list, or a write; making the extension send to any host but Toddle's; reading the licence key from the page; reaching the feedback form from Toddle's page; and opening an extension page from a website.

    + +

    {secondReview.label}

    +

    + The second review covered everything new in version {secondReview.version}: the + extension's own pages, rebuilt; the first rebuilt part that runs on Toddle's pages + (the flags switch, in a closed shadow root that no script on the page can reach); the + build that makes them; and the new switches. It also re-read the code that runs in + Toddle's page against the claims made about it. It found two medium-severity issues + and two low (findings {secondReview.findings}). All were fixed in + version {secondReview.version} before it is published, and none was in + version {security.version}. +

    +
    + { + laterFindings.map((item) => ( +
    +

    + {item.severity} + Fixed in {secondReview.version} +

    +

    {item.finding}

    +

    + Fix. {item.fix} +

    +
    + )) + } +
    +

    + It also checked, and found sound: the package that ships, the permissions it asks for + (storage and Toddle only, nothing reachable from websites), the built pages, the + network (the same two outside requests as before), and the list of packages the build + uses. +

    We publish what was found, not only that it was fixed, because a school deciding whether to trust software should know. @@ -437,6 +545,42 @@ const severityClass: Record = { student's profile.

    +

    + What the code inside Toddle's page cannot promise +

    +

    + Some of the extension has to run in Toddle's own page, because that is the only place + Toddle's data can be read, and any script on the page shares that space. + The review of {secondReview.label} set out, plainly, what that means. A script that is already + listening in Toddle's page when the extension starts: +

    +
      +
    • + can read the per-load secrets, which stop only scripts that are not listening, and + replay them in that browser: switch a licensed feature on, give the licence check + another account's email, or show flags that were hidden. It gains no student data by + it, because it already runs with the teacher's session and could read Toddle + directly; +
    • +
    • + cannot overhear the answers to the extension's queries, but can answer first with + false ones, so the gradebook could show wrong figures. Values in the CSV export + still cannot start a spreadsheet formula; +
    • +
    • + is stopped from pressing the extension's message buttons, but can tell Toddle's own + app to open a chat in any case. +
    • +
    +

    + None of this lets anything leave the browser, write to Toddle, or reach the + extension's own pages, licence key or storage. The fix is the rebuild under way, which is + moving every decision and every piece of the interface out of Toddle's page into the + extension's own, leaving there only what has to read the page. +

    +

    Reporting a vulnerability diff --git a/src/pages/legal/student-privacy.astro b/src/pages/legal/student-privacy.astro index 17c7bd1..c12d853 100644 --- a/src/pages/legal/student-privacy.astro +++ b/src/pages/legal/student-privacy.astro @@ -7,7 +7,7 @@ * enforced by the extension's closed-loop tests. */ import BaseLayout from "../../layouts/BaseLayout.astro"; -import { legal, updatedLabel } from "../../data/legal"; +import { dataProtection, legal, updatedLabel } from "../../data/legal"; import { toddleExtension } from "../../data/site"; const title = "Student privacy — Nyuchi Learning"; @@ -53,12 +53,13 @@ const privacyMail = `mailto:${legal.privacyEmail}`;

  • It is processed in that teacher's browser, on that device, and nowhere else.
  • - It is not stored: answers are held in the page's memory for a few minutes and are gone - when the tab closes. + It is not stored: answers are held in the Toddle tab's memory until the tab is closed + or reloaded, and never written to storage.
  • - It is not transmitted to Nyuchi or to anyone else. The extension's only connections - outside Toddle are the opt-in feedback form, which carries only what the teacher + It is not transmitted to Nyuchi or to anyone else. Apart from Toddle itself (its own + interface, and student photos from wherever Toddle serves them), the extension's + only connections are the opt-in feedback form, which carries only what the teacher types, and, while a licence is entered, a daily check for cancelled keys, which sends no data.
  • @@ -82,9 +83,13 @@ const privacyMail = `mailto:${legal.privacyEmail}`; by children, and the student information it shows to staff is not collected by Nyuchi.

    -

    GDPR, UK GDPR and similar laws

    +

    + GDPR, UK GDPR, Zimbabwe's Act and similar laws +

    - The school remains the controller of its student data, and Toddle its processor. + Nyuchi follows Zimbabwe's {dataProtection.home.law} and the EU and UK GDPR (see + the law we follow). Under all of them, + the school remains the controller of its student data, and Toddle its processor. Nyuchi receives none of that data from the extension. See data handling for what Nyuchi does hold, about purchasers and people who send feedback. diff --git a/src/pages/legal/terms.astro b/src/pages/legal/terms.astro index d640610..3a6aa82 100644 --- a/src/pages/legal/terms.astro +++ b/src/pages/legal/terms.astro @@ -61,7 +61,8 @@ const description = Hiding student flags is free and is not licensed. It protects a child, so it is not something we will ever put behind a payment. The My classes filter on the Toddle home page is free too. A licence covers the gradebook's per-criterion columns, the - CSV export, the student sidebar and primary teacher names. + CSV export, the student sidebar, primary teacher names and the Attendance + dashboard's student details.