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 @@


-**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}.
+
- 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:
- 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.
- 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 = {
- 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.
How licence keys work
A licence key is a signed statement that the extension checks on your own device, so
- activating it contacts nothing and works offline. An individual key carries the
- buyer's email address and works for the Toddle account with that address; an
- organisation key carries the school's email domain and works for Toddle accounts on
- that domain. The comparison happens in your browser only. While a key is entered, the
- extension downloads once a day a signed list of cancelled keys, sending nothing about
- you; a key we have cancelled stops unlocking the licensed features once the extension
- has seen that list. If the check cannot be made, the extension keeps
- working. When a key expires the licensed features stop, and the free flag switch and
- My classes filter carry on working.
+ activating it contacts nothing and works offline. A key carries the buyer's email
+ address, and may also name who it is for: the buyer's email, for the Toddle account
+ with that address, or for an organisation licence the school's email domain, for
+ Toddle accounts on that domain. Where it does, the comparison happens in your browser
+ only. While a key is entered, the extension downloads once a day a signed list of
+ cancelled keys, sending nothing about you; a key we have cancelled stops unlocking the
+ licensed features once the extension has seen that list. If the check cannot be made,
+ the last result stands, so a network outage never switches a licence off. When a key
+ expires the licensed features stop, and the free flag switch and My classes filter
+ carry on working.
- The {toddleExtension.name}, version 0.8.2 and later from the Chrome Web Store
+ The {toddleExtension.name}, version {security.supportedFrom} and later from the Chrome Web Store
(earlier versions are not supported; extension ID
{toddleExtension.extensionId}).
- Acknowledge your report
- {security.report.acknowledge}.
+ Acknowledge your report {security.report.acknowledge}.
Triage it within 10 working days: confirm
@@ -145,7 +144,7 @@ const description =
>
Security — the extension's security model,
- testing and independent review.
+ testing and adversarial reviews.
security.txt.
diff --git a/src/pages/toddle-enhancement-extension.astro b/src/pages/toddle-enhancement-extension.astro
index c69ebad..bdc30fe 100644
--- a/src/pages/toddle-enhancement-extension.astro
+++ b/src/pages/toddle-enhancement-extension.astro
@@ -12,7 +12,7 @@ import { Picture } from "astro:assets";
import type { ImageOutputFormat } from "astro";
import BaseLayout from "../layouts/BaseLayout.astro";
import { toddleExtension } from "../data/site";
-import { pricing, refunds, toddleNotice } from "../data/legal";
+import { extensionVersions, pricing, refunds, toddleNotice } from "../data/legal";
import gradebookShot from "../assets/screenshots/01-gradebook.jpg";
import sidebarShot from "../assets/screenshots/02-student-sidebar.jpg";
import todayShot from "../assets/screenshots/03-today.jpg";
@@ -52,7 +52,7 @@ const features = [
mineral: "malachite",
free: true,
title: "Hide student flags",
- body: "Flags are hidden across Toddle by default: the gradebook, Attendance, the class portfolio, submissions, search results, profile headers and the student sidebar. Showing them is a deliberate choice — the switch in the Toddle header, or an eye button for one student. It stays as you left it, so it is armed before a lesson.",
+ body: `One switch in the Toddle header hides flags across Toddle: the gradebook, Attendance, the class portfolio, submissions, search results, profile headers and the student sidebar. An eye button shows one student's flags when you need them. It stays as you left it, so you can arm it before a lesson. From version ${extensionVersions.next}, flags show as Toddle shows them until you switch them off; earlier versions hid them from the start.`,
},
{
mineral: "gold",
@@ -164,7 +164,7 @@ const facts = [
{
label: "What it reads",
value:
- "What Toddle already lets you see — gradebook results, class staff, student details, today's timetable and attendance — through an exact list of read-only queries to Toddle's own API, for you at the screen, reusing the session you are already signed in with. It fetches a student's flags only when someone chooses to show them. It never sees a password, and the auth token stays in the page — the extension's own interface code cannot read it.",
+ "What Toddle already lets you see — gradebook results, rubric descriptors and levels as your school set them up, class staff, student details, today's timetable and attendance, and a student's flags when they are shown — through an exact list of read-only queries to Toddle's own API, for you at the screen, reusing the session you are already signed in with. It never asks for your password. Toddle's sign-in headers stay in the tab's memory, in one script in Toddle's page, and go nowhere but Toddle.",
},
{
label: "What it writes",
@@ -174,17 +174,17 @@ const facts = [
{
label: "What it sends",
value:
- "Nothing it reads, anywhere. No account, no analytics, no tracking, no remote code. Licence keys are checked on your machine: an individual key carries your email and an organisation key your school's email domain, compared with your Toddle account in the browser only. The welcome page it opens on install is part of the extension and contacts nothing.",
+ "Nothing it reads, anywhere but back to Toddle. No account, no analytics, no tracking, no remote code. Student photos load from wherever Toddle serves them, as on Toddle's own page. Licence keys are checked on your machine: a key may name your email or your school's email domain, and where it does, that is compared with your Toddle account in the browser only. The welcome page it opens on install is part of the extension and contacts nothing.",
},
{
label: "Its two outside connections",
value:
- "The Send feedback form, only when you press Send: what you typed and the version number, to our feedback form, processed by Formspree. And, only while a licence is entered, one request a day to licences.nyuchi.dev for a signed list of cancelled keys' fingerprints, which sends no key, no identifiers and no Toddle data; if it fails, the extension keeps working.",
+ "The Send feedback form, only when you press Send: what you typed and the version number, to our feedback form, processed by Formspree. And, only while a licence is entered, at most one request a day to licences.nyuchi.dev for a signed list of cancelled keys' fingerprints, which sends no key, no identifiers and no Toddle data; if it fails, the last result stands.",
},
{
label: "What it stores",
value:
- "On your own machine only: your switch settings (flags shown or hidden, the sidebar, teacher names, My classes) and your licence key, if you have one. No student data: what the sidebar reads is held in the page's memory for a few minutes and gone when the tab closes.",
+ "On your own machine only: your switch settings, your licence key if you have 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, your My classes choice and the academic year Toddle is showing. No student data: what it reads is held in the tab's memory until the tab is closed or reloaded, and never written to storage.",
},
];
@@ -470,8 +470,8 @@ const schema = [
The flag switch protects a student, so it is not behind a price and never will
be. The My classes filter is free too. The gradebook's per-criterion columns, the
- CSV export, the student sidebar and primary teacher names are the time saving,
- and that is what a licence pays for.
+ CSV export, the student sidebar, primary teacher names and the Attendance
+ dashboard's details are the time saving, and that is what a licence pays for.
diff --git a/tests/security.test.ts b/tests/security.test.ts
index 6f3c418..9daddce 100644
--- a/tests/security.test.ts
+++ b/tests/security.test.ts
@@ -345,7 +345,7 @@ describe("consent", () => {
});
it("records a version and a timestamp, not just a boolean", () => {
- /* All three regimes put the burden of demonstrating consent on us. */
+ /* Both laws put the burden of demonstrating consent on us. */
expect(banner).toMatch(/v: version/);
expect(banner).toMatch(/at: new Date\(\)\.toISOString\(\)/);
});
From 0666bf715e182dffb247ddb10b0cebfe0bd48601 Mon Sep 17 00:00:00 2001
From: Bryan Fawcett
Date: Tue, 6 Oct 2026 11:14:57 +0800
Subject: [PATCH 4/5] docs: the extension page and legal pages describe 0.9.0,
with flags concealed, not hidden (#67)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* docs: the extension page and legal pages describe 0.9.0, with flags concealed, not hidden
The pages described 0.8.4 as the next release and said the flag switch
hides flags. Patches are now test releases that do not go to the Chrome Web
Store, so the next release teachers get is 0.9.0, and from it the extension
no longer hides Toddle's own flags: a teacher can conceal them for a shared
screen.
- Flags, said once in `flags` (src/data/legal.ts): "conceal" and "flag
visibility", never "hide"; "Show flags" in the sidebar for one student;
concealing is a screen, not a lock. A test fails on "hide flags".
- The rebuild in closed shadow roots, the student sidebar's timetable,
email and admin-portal changes, a class's teachers by email, and My
classes switching at once.
- For schools: the rollout steps and the organisation key on the extension
page, a help centre link, and the data schema, described and on request.
- Data handling: the licence server keeps a hash of each key, not the key.
- The Zimbabwe CDPA and GDPR framing is unchanged.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01CoDNYz27iJ7o8PPztCzTpq
* fix: sitemap lastmod is an ISO string, and source-map-js 1.2.2 for GHSA-68fv-2mgg-jv7q
- astro.config.mjs: the sitemap's serialize hook set `lastmod` to a Date,
but a sitemap item's `lastmod` is a string (TS2322 in `npm run lint`).
It is now `new Date().toISOString()`; the sitemap output is the same
ISO 8601 timestamp.
- package-lock.json: source-map-js 1.2.1 → 1.2.2, the patched release for
GHSA-68fv-2mgg-jv7q (high), which failed "Audit what ships". Every
dependent's range already allows 1.2.2, so it is `npm update
source-map-js`, run with the repo's npm (11.12.1); no override needed.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01CoDNYz27iJ7o8PPztCzTpq
* docs: the newest patch is 0.8.9
Extension PR #38 (My classes switches at once; docs/data-schema.md) merged
into staging and shipped in the v0.8.9 patch.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01CoDNYz27iJ7o8PPztCzTpq
---------
Co-authored-by: Bryan Fawcett
---
CHANGELOG.md | 29 +++
astro.config.mjs | 3 +-
package-lock.json | 6 +-
public/llms.txt | 73 +++++---
src/data/legal.ts | 67 +++++--
src/data/site.ts | 8 +
src/pages/index.astro | 4 +-
src/pages/legal/cookies.astro | 8 +-
src/pages/legal/data.astro | 36 ++--
src/pages/legal/privacy.astro | 4 +-
src/pages/legal/security.astro | 81 ++++++---
src/pages/legal/student-privacy.astro | 23 ++-
src/pages/legal/terms.astro | 6 +-
src/pages/toddle-enhancement-extension.astro | 177 +++++++++++++------
tests/links.test.ts | 14 +-
tests/seo.test.ts | 3 +-
16 files changed, 391 insertions(+), 151 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65830d4..1073ede 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,35 @@
### Changed
+- **The extension page and legal pages describe 0.9.0, the next Chrome Web
+ Store release**, and say where 0.8.2, on the store today, differs. Patches
+ (0.8.3 to 0.8.9) are test releases that do not go to the store, so
+ `extensionVersions.next` is now 0.9.0, not 0.8.4.
+ - Student flags: the extension no longer hides Toddle's own flags. A
+ teacher can conceal them for a shared screen with the free switch, and
+ "Show flags" in the sidebar shows one student's. The wording is
+ "conceal" and "flag visibility", said once in `flags`
+ (`src/data/legal.ts`); a test fails on "hide flags" on the extension
+ page, the home page and in llms.txt. The sidebar asks Toddle for a
+ student's flags each time it opens. The value on Toddle's site is
+ `tee-blur-flags` (`gbx-hide-flags` in 0.8.2).
+ - The rebuild: the flag switch, the student sidebar and the home page's
+ additions run in the extension's isolated world, in closed shadow roots
+ (product page, Security page, llms.txt). The Security page adds the home
+ page channel's limit.
+ - The student sidebar: today's timetable as its own part, the student's
+ email, a class's teachers to email at once, and stepping aside in
+ Toddle's admin portal. My classes switches on and off at once.
+ - For schools: the Admin console, Group Policy, Intune and macOS steps,
+ the organisation key pasted in once by each teacher, and allowing
+ `licences.nyuchi.dev`, summarised on the extension page; the full guide
+ on request. A link to the help centre's "How to use" collection.
+ - The data schema (the extension's `docs/data-schema.md`) is described on
+ the data handling page and the extension page, available on request.
+ - Data handling: the licence server keeps a SHA-256 of each key, never
+ the key itself, and the school's email domains for an organisation key.
+ - The second security review was of 0.8.4, a test release; its fixes are
+ in 0.9.0.
- **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
diff --git a/astro.config.mjs b/astro.config.mjs
index e90682d..a9b8591 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -35,7 +35,8 @@ export default defineConfig({
*/
serialize(item) {
item.url = item.url.replace(/(.+)\/$/, "$1");
- item.lastmod = new Date();
+ // The sitemap item takes an ISO 8601 string, not a Date.
+ item.lastmod = new Date().toISOString();
return item;
},
}),
diff --git a/package-lock.json b/package-lock.json
index c71d146..94891ba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7313,9 +7313,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
diff --git a/public/llms.txt b/public/llms.txt
index 6d7b550..334a496 100644
--- a/public/llms.txt
+++ b/public/llms.txt
@@ -17,25 +17,36 @@ community-based platforms. Nyuchi Web Services is its development division.
score-based rubrics, single-point rubrics, checklists, scores, comments,
grade scales, learning goals — into one column per standard, criterion or
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. 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
- timetabled teacher), today's daily attendance code, and the student's
- classes in Today and All tabs. Clicking a class opens a class view with its
- current block, primary teacher, parent course, room and every teacher with
- their display title and email. Message buttons open Toddle's own chat; the
- extension sends no message itself. On the Attendance dashboard's Students
+ panel per student. It also gives teachers control of flag visibility on a
+ shared screen. From version 0.9.0 (the next Chrome Web Store release),
+ the extension no longer hides Toddle's own student flags; a teacher can
+ conceal them with one free switch, in Toddle's header or the extension's
+ menu, so a gradebook can be projected or screen-shared without what a flag
+ says being readable from across the room. In the student sidebar, "Show flags" shows
+ one student's flags. Concealing is a screen, not a lock: a concealed flag
+ can still show that a student has a flag. In version 0.8.2, on the store
+ today, flags are hidden from the start and the switch shows or hides them.
+ It conceals flags, not names. Free: the flag switch and a "My classes"
+ filter on the Toddle home page. With a licence (US$5 a year for one
+ teacher, US$149.99 a year for a school's email domain): the per-criterion
+ gradebook columns, CSV export, primary teacher names, the Attendance
+ dashboard's details, and the student sidebar. The sidebar works anywhere a
+ student appears in Toddle (class pages, the Attendance dashboard's Periods,
+ Students and Excusals tabs, name links, gradebook student cells), except
+ in School setup, Toddle's admin portal, where it steps aside. It shows a
+ photo, grade, age and class of, the student's email (click to copy),
+ Student Group, room number, homeroom advisor, contacts with relationship,
+ phone and email, a "Now" card (current attendance code, block, class, room
+ and timetabled teacher), and today's timetable as its own part, which
+ shows today's periods, "No classes today", or "Couldn't load timetable"
+ with a Retry button, without holding up the rest of the panel. Clicking a
+ class opens a class view with its current block, primary teacher, parent
+ course, room and every teacher with their display title and email; the
+ teachers can be emailed together. Message buttons open Toddle's own chat;
+ the extension sends no message itself. From version 0.9.0 the flag switch,
+ the student sidebar and the home page's additions run in the extension's
+ own isolated world, drawn in closed shadow roots that no script on
+ Toddle's page can read or click into. On the Attendance dashboard's Students
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
@@ -56,8 +67,8 @@ community-based platforms. Nyuchi Web Services is its development division.
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
+ covered. 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
@@ -76,11 +87,21 @@ community-based platforms. Nyuchi Web Services is its development division.
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
+ switches, whether flags are concealed, 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
+ are defined in the extension. Every query, stored value and request is
+ listed field by field in the extension's data schema, which is checked
+ against its code on every change; schools can ask privacy@nyuchi.com for
+ a copy. Schools deploy it from the Chrome Web Store by extension ID, with
+ a Google Admin console force-install (or Group Policy, Intune or a macOS
+ profile); an organisation key is pasted in once by each teacher, since it
+ cannot be pushed by policy. Teachers get each minor release (0.9.0 next)
+ from the Chrome Web Store; patch releases between them are for testing
+ and do not go to the store. Help:
+ https://support.nyuchi.com/en/collections/19764027-how-to-use-the-toddle-enhancement-extension
+ Security: https://learning.nyuchi.com/legal/security
## Where the learning content went
@@ -138,8 +159,8 @@ division of Nyuchi Africa (Private) Limited, Harare, Zimbabwe.
end-to-end network recorder that fails on any non-Toddle request, every
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
+ 6 October 2026: two Medium and two Low findings in the code for 0.8.4, a
+ test release, all fixed there and in 0.9.0), 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
diff --git a/src/data/legal.ts b/src/data/legal.ts
index 29b02b4..d583e10 100644
--- a/src/data/legal.ts
+++ b/src/data/legal.ts
@@ -87,8 +87,8 @@ export const pricing = [
includes: [
"Every assessment tool expanded into per-criterion columns",
"CSV export",
- "The student sidebar, with where they are now and today's classes",
- "The class view, and message buttons into Toddle's chat",
+ "The student sidebar, with where they are now and today's timetable",
+ "The class view, a class's teachers to email at once, and message buttons into Toddle's chat",
"Primary teacher names, and the Attendance dashboard's Now line",
],
},
@@ -107,28 +107,57 @@ 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.
+ * Which versions of the extension the legal pages describe.
+ *
+ * How the extension is released: patches (0.8.3, 0.8.4, …) are GitHub
+ * releases, for testing, and never go to the Chrome Web Store; each minor
+ * (0.9.0, …) is released from the extension's `main` branch and published to
+ * the store. Teachers get minors.
+ *
+ * `current` is what teachers have from the Chrome Web Store (the v0.8.3 tag
+ * carries the same code). `next` is the next minor: everything under
+ * Unreleased in the extension's changelog, reviewed, already in the patch
+ * releases up to `latestPatch`, and not yet on the store. Claims that differ
+ * between the two say "from version 0.9.0" and say what `current` does, 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",
+ next: "0.9.0",
+ /** The newest patch release (GitHub only, for testing). */
+ latestPatch: "0.8.9",
/** The oldest version security reports are accepted for. */
supportedFrom: "0.8.2",
} as const;
-const { next } = extensionVersions;
+const { current, next } = extensionVersions;
+
+/**
+ * Student flags, said once. From 0.9.0 the extension never hides Toddle's own
+ * flags: a teacher can conceal them for a shared screen. The wording is
+ * "conceal" and "flag visibility", never "hide", and it promises no more than
+ * the extension does: how a concealed flag looks is still being refined, so
+ * this says only that what a flag says cannot be read from across a room.
+ * Checked against the extension's SECURITY.md and docs/data-schema.md.
+ */
+export const flags = {
+ summary:
+ "The extension never hides Toddle's own student flags: they show as Toddle shows them. When your screen is shown to a class, a parent or a visitor, one free switch conceals them across Toddle, so what a flag says cannot be read from across the room.",
+ reveal:
+ 'In the student sidebar, "Show flags" shows one student\'s flags when you need them.',
+ limit:
+ "Concealing is a screen, not a lock. A concealed flag can still show that a student has a flag. Concealing changes only what is drawn on the screen, never Toddle's data, and does not stop anyone at the computer opening a student's flags in Toddle.",
+ before: `In version ${current}, on the Chrome Web Store today, the extension hides flags from the start, and the switch shows or hides them.`,
+} as const;
/**
* What the extension does with data. Each of these is a claim that can be
- * 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.
+ * checked against the source (the extension's docs/data-schema.md, formerly
+ * docs/toddle-data-fields.md, is the full schema, checked against its code on
+ * every change), and each maps to an answer on the Chrome Web Store privacy
+ * form.
*/
export const extensionDataFacts = [
{
@@ -144,12 +173,12 @@ export const extensionDataFacts = [
{
claim: "What it reads, feature by feature.",
detail:
- "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.",
+ "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 (see the next point); and each class's teachers, with their display titles and emails.",
},
{
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.`,
+ "Student flags: Toddle's own are never hidden, and a teacher can conceal them for a shared screen.",
+ detail: `From version ${next}, the extension no longer hides Toddle's own student flags: they show as Toddle shows them, including in Toddle's student popover, with their links and documents. A teacher can choose to conceal flags, for a screen a class or a visitor can see, with the free flag switch in Toddle's top bar or the extension's menu. It is off until they switch it on; a teacher who had chosen to hide flags in an earlier version keeps that choice, as concealing. What a concealed flag says cannot be read from across a room, though it can still show that a student has a flag. ${flags.reveal} The sidebar asks Toddle for a student's active flags each time it opens, and keeps them only while that student's sidebar is open. Concealing changes only what is drawn on the screen, never Toddle's data. ${flags.before} In that version, while flags are hidden, the sidebar asks for a student's flags only when someone presses its eye button. The flag switch is free and always will be.`,
},
{
claim: "Your Toddle sign-in stays with Toddle.",
@@ -173,7 +202,7 @@ export const extensionDataFacts = [
},
{
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.`,
+ 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); tee-blur-flags (gbx-hide-flags in version ${current}), whether flags are concealed, so they are concealed 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. Version ${current} has three switches: student flags, the student sidebar and primary teacher names.`,
},
{
claim: "What it holds in memory, and for how long.",
@@ -227,7 +256,9 @@ export const security = {
{
date: "2026-10-06",
label: "6 October 2026",
- version: extensionVersions.next,
+ /** The code reviewed: the 0.8.4 patch, the rebuild's first steps. Its
+ fixes are in every later release, and in `next`. */
+ version: "0.8.4",
findings: "13 to 16",
},
],
diff --git a/src/data/site.ts b/src/data/site.ts
index 78c5a9a..ff8ec01 100644
--- a/src/data/site.ts
+++ b/src/data/site.ts
@@ -169,6 +169,14 @@ export const toddleExtension = {
supportEmail: "support@nyuchi.com",
/** Mailto used while there is no store listing, and for school enquiries. */
enquiry: "mailto:support@nyuchi.com?subject=Toddle%20Enhancement%20Extension",
+ /** "How to use the Toddle Enhancement Extension" in Nyuchi's help centre:
+ the page the extension's own Help button opens. */
+ help: "https://support.nyuchi.com/en/collections/19764027-how-to-use-the-toddle-enhancement-extension",
+ /** The Chrome Web Store's update URL, for force-install policies set
+ outside the Google Admin console. */
+ storeUpdateUrl: "https://clients2.google.com/service/update2/crx",
+ /** The licence server's host, which a school's network should allow. */
+ licenceHost: "licences.nyuchi.dev",
} as const;
/**
diff --git a/src/pages/index.astro b/src/pages/index.astro
index aa7ff93..896dbf1 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -79,8 +79,8 @@ const description =
A Chrome extension for the Toddle gradebook. It expands every rubric
- into per-criterion columns, exports the lot to CSV, and hides student
- flags with one switch so you can share your screen safely.
+ into per-criterion columns, exports the lot to CSV, and conceals student
+ flags with one free switch when you share your screen.
diff --git a/src/pages/legal/cookies.astro b/src/pages/legal/cookies.astro
index 312cd45..f642e74 100644
--- a/src/pages/legal/cookies.astro
+++ b/src/pages/legal/cookies.astro
@@ -11,7 +11,7 @@
* document for their own products; they set them, and they can change them.
*/
import BaseLayout from "../../layouts/BaseLayout.astro";
-import { legal, updatedLabel } from "../../data/legal";
+import { extensionVersions, legal, updatedLabel } from "../../data/legal";
import { CONSENT_KEY } from "../../data/consent";
import { analytics, toddleExtension } from "../../data/site";
@@ -199,8 +199,10 @@ const rows = [
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
+ licence allows (not the key, and no email); tee-blur-flags (gbx-hide-flags in version {extensionVersions.current}), whether flags are concealed;
+ 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.
diff --git a/src/pages/legal/data.astro b/src/pages/legal/data.astro
index 9a3bd6c..d9adc27 100644
--- a/src/pages/legal/data.astro
+++ b/src/pages/legal/data.astro
@@ -12,6 +12,7 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
import {
dataProtection,
extensionVersions,
+ flags,
legal,
refunds,
updatedLabel,
@@ -29,7 +30,7 @@ const held = [
what: "Licence purchases",
where: "Nyuchi's licence server and its database, on Cloudflare",
fields:
- "The email address the key was issued to, and the address that paid if different; the plan; how many were bought; for an organisation licence, the seat count and the buyer's answer to the order question (the school and roughly how many teachers); Buy Me a Coffee's purchase and item references; the key itself; when it was issued, emailed and refunded, its expiry, and its status.",
+ "The email address the key was issued to, and the address that paid if different; the plan; how many were bought; for an organisation licence, the school's email domains, the seat count and the buyer's answer to the order question (the school and roughly how many teachers); Buy Me a Coffee's purchase and item references; a fingerprint (SHA-256 hash) of the key, never the key itself; when it was issued, emailed and refunded, its expiry, and its status.",
},
{
what: "The order and payment",
@@ -136,13 +137,12 @@ const subprocessors = [
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.
+ Student flags. From version {extensionVersions.next}, the extension no longer hides Toddle's own flags, and a staff member can choose
+ to conceal them for a shared screen with the free flag switch. The sidebar asks Toddle for a
+ student's active flags (title, description, type and date) each time it opens,
+ and keeps them only while that student's sidebar is open. {flags.before} In that
+ version, while flags are hidden, the sidebar asks for them only when someone
+ presses its eye button.
From Toddle's own requests: the sign-in
@@ -182,8 +182,8 @@ const subprocessors = [
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 Attendance dashboard (version {extensionVersions.current} has three: student
+ flags, the student sidebar and primary teacher names);
the licence key, if one was entered;
@@ -199,7 +199,7 @@ const subprocessors = [
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,
+ email); tee-blur-flags (gbx-hide-flags in version {extensionVersions.current}), whether flags are concealed; 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.
@@ -257,6 +257,20 @@ const subprocessors = [
browser only, and sends it nowhere.
+
+ The full data schema
+
+
+ This page is the summary. The extension's data schema is the detail: every query it
+ can send to Toddle, with every field it asks for and what each is used for; every
+ value it stores, with its shape and when it is deleted; and every request that leaves
+ the device. It is written for a school's data protection lead or IT team, and for
+ Toddle. It is checked against the code on every change, so a query the code allows
+ that the schema does not describe fails the build, and where something could not be
+ confirmed from the code it is listed as an open question rather than guessed. Ask
+ {legal.privacyEmail} for a copy.
+
+
What Nyuchi holds
None of this comes from Toddle, and none of it is about students.
diff --git a/src/pages/legal/privacy.astro b/src/pages/legal/privacy.astro
index 98ad55e..aefd5c5 100644
--- a/src/pages/legal/privacy.astro
+++ b/src/pages/legal/privacy.astro
@@ -168,7 +168,9 @@ const description =
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.
+ of the code for version {security.reviews[1].version}, a test release, was fixed in
+ that release, and the fixes are in version {security.next}, the next on the Chrome
+ Web Store.
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.
diff --git a/src/pages/legal/security.astro b/src/pages/legal/security.astro
index 7336728..2150214 100644
--- a/src/pages/legal/security.astro
+++ b/src/pages/legal/security.astro
@@ -16,6 +16,7 @@
import BaseLayout from "../../layouts/BaseLayout.astro";
import {
extensionVersions,
+ flags,
legal,
security,
securityMailto,
@@ -190,8 +191,10 @@ const severityClass: Record = {
Last updated {updatedLabel}. Describes the
{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
+ and version {security.next}, the next release on the store, reviewed and not yet
+ published. Between store releases, fixes are tested as patch releases (the newest is
+ {extensionVersions.latestPatch}), which do not go to the store. Where {security.version}
+ and {security.next} differ, this page says so. For schools, and for the privacy, legal and
IT colleagues who review software on their behalf.
@@ -341,26 +344,42 @@ const severityClass: Record = {
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
+ the licence allows, whether flags are concealed, 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.
+ From version {security.next}, the extension no longer hides Toddle's own student flags; a
+ staff member can choose to conceal them for a shared screen with the free flag
+ switch. The sidebar asks Toddle for a student's flags each time it opens, and keeps
+ them only while it is open. {flags.before} The CSV export is made in the browser and
+ saved where the teacher's downloads go, by the teacher's choice.
+
+
+
+ Out of Toddle's page, into closed shadow roots
+
+
+ The extension is being rebuilt so that as little of it as possible runs where
+ Toddle's own scripts run. From version {security.next}, the flag switch, the student
+ sidebar with a class's teachers, and the home page's My classes option and teacher
+ names run in the extension's own isolated part of the browser. What they draw on
+ Toddle's pages sits inside closed shadow roots, which no script on Toddle's page can
+ read or click into, and whether each one runs is decided by the extension's own
+ switches and licence, never by Toddle's page. What only Toddle's page holds (who was
+ clicked, the course list as it loads, Toddle's chat and icons) is read by small parts
+ with no interface of their own, which talk to the extension on private channels set
+ up before Toddle's own scripts run. The gradebook and the Attendance dashboard are
+ moving the same way.
Protections inside the page
- Part of the extension has to run inside Toddle's page to read the gradebook, which
- means it shares that page with Toddle's own scripts. It is protected as far as a page
- allows:
+ Part of the extension still has to run inside Toddle's page, to read the gradebook and
+ what Toddle draws, which means it shares that page with Toddle's own scripts. It is
+ protected as far as a page allows:
= {
{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}.
+ The second review covered everything new in version {secondReview.version}, a test
+ release: 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}, and the fixes are in every release after it,
+ including {security.next}. None was in version {security.version}. Each later
+ step of the rebuild (the student sidebar, the home page) was reviewed the same way as
+ it was made, and what those reviews found about the page's limits is set out below.
{
@@ -540,9 +561,9 @@ const severityClass: Record = {
that wants that risk reduced further should raise it with Toddle.
- The flag switch is a privacy aid, not a security control. It hides flags from the
+ The flag switch is a privacy aid, not a security control. It conceals flags on the
screen; it does not hide names, and does not stop anyone deliberately opening a
- student's profile.
+ student's flags or profile. {flags.limit}
@@ -560,9 +581,9 @@ const severityClass: Record = {
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;
+ another account's email, or clear flags that were concealed. 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
@@ -571,7 +592,13 @@ const severityClass: Record = {
is stopped from pressing the extension's message buttons, but can tell Toddle's own
- app to open a chat in any case.
+ app to open a chat in any case;
+
+
+ if it was in the page before Toddle's own scripts (a page that opened Toddle in a
+ new window can be), can take the private channel the home page's part uses, and so
+ show a wrong teacher's name, as text, or offer My classes where it should not.
+ Whether a feature runs is still never decided from the page.
diff --git a/src/pages/legal/student-privacy.astro b/src/pages/legal/student-privacy.astro
index c12d853..b3dce6b 100644
--- a/src/pages/legal/student-privacy.astro
+++ b/src/pages/legal/student-privacy.astro
@@ -7,7 +7,13 @@
* enforced by the extension's closed-loop tests.
*/
import BaseLayout from "../../layouts/BaseLayout.astro";
-import { dataProtection, legal, updatedLabel } from "../../data/legal";
+import {
+ dataProtection,
+ extensionVersions,
+ flags,
+ legal,
+ updatedLabel,
+} from "../../data/legal";
import { toddleExtension } from "../../data/site";
const title = "Student privacy — Nyuchi Learning";
@@ -70,6 +76,21 @@ const privacyMail = `mailto:${legal.privacyEmail}`;
who can see what is still decided by the school's Toddle permissions.
+
Student flags
+
+ Toddle lets a school mark students with flags, and those flags are the most sensitive
+ thing a teacher's screen can show. From version {extensionVersions.next}, the
+ extension no longer hides Toddle's own flags, so staff see what the school has recorded.
+ What it adds is flag visibility for a shared screen: a teacher can conceal flags
+ with one free switch before the screen is shown to a class, a parent or a visitor,
+ so what a flag says cannot be read from across the room. {flags.reveal}
+
+
+ The sidebar reads a student's active flags from Toddle each time it opens, shows them
+ in that teacher's browser only, and forgets them when it closes. They are never
+ stored and never sent anywhere. {flags.limit}
+
+
FERPA
Because no student data is collected, stored or transmitted by Nyuchi, Nyuchi does not
diff --git a/src/pages/legal/terms.astro b/src/pages/legal/terms.astro
index 3a6aa82..feba7dc 100644
--- a/src/pages/legal/terms.astro
+++ b/src/pages/legal/terms.astro
@@ -58,8 +58,8 @@ const description =
What it costs
- 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
+ Concealing student flags for a shared screen 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, primary teacher names and the Attendance
dashboard's student details.
@@ -156,7 +156,7 @@ const description =
works by reading the interface Toddle's own web app uses. Toddle does not publish
that interface and is under no obligation to keep it stable. A Toddle release can
change it, and when that happens a column may stop appearing or a flag may stop being
- hidden.
+ concealed.
We will fix breakages as quickly as we can, and the extension is built to fail
diff --git a/src/pages/toddle-enhancement-extension.astro b/src/pages/toddle-enhancement-extension.astro
index bdc30fe..9818b5c 100644
--- a/src/pages/toddle-enhancement-extension.astro
+++ b/src/pages/toddle-enhancement-extension.astro
@@ -7,12 +7,17 @@
* know what it can see before putting it on a hundred machines. The flag
* switch gets the most room because it is the part people will be asked to
* justify.
+ *
+ * It describes version 0.9.0 (`extensionVersions.next`), the next release on
+ * the Chrome Web Store, and says where 0.8.2, on the store today, differs.
+ * Flags are "concealed", never "hidden": the extension no longer hides
+ * Toddle's own flags (see `flags` in src/data/legal.ts).
*/
import { Picture } from "astro:assets";
import type { ImageOutputFormat } from "astro";
import BaseLayout from "../layouts/BaseLayout.astro";
import { toddleExtension } from "../data/site";
-import { extensionVersions, pricing, refunds, toddleNotice } from "../data/legal";
+import { extensionVersions, flags, pricing, refunds, toddleNotice } from "../data/legal";
import gradebookShot from "../assets/screenshots/01-gradebook.jpg";
import sidebarShot from "../assets/screenshots/02-student-sidebar.jpg";
import todayShot from "../assets/screenshots/03-today.jpg";
@@ -38,9 +43,10 @@ const primaryCta = live
? { label: "Add to Chrome", href: toddleExtension.webStore! }
: { label: "Tell me when it is live", href: toddleExtension.enquiry };
-/* `free` is the split the extension enforces: the flag switch and My classes
- for everyone, the rest with a licence. Keep it in step with `pricing` in
- src/data/legal.ts and the terms. */
+/* `free` is the split the extension enforces (its docs/licensing.md and
+ app/features/registry.ts): the flag switch and My classes for everyone, the
+ rest with a licence. Keep it in step with `pricing` in src/data/legal.ts,
+ the terms, and the licence email (nyuchi-licence-server, src/email.js). */
const features = [
{
mineral: "cobalt",
@@ -51,8 +57,8 @@ const features = [
{
mineral: "malachite",
free: true,
- title: "Hide student flags",
- body: `One switch in the Toddle header hides flags across Toddle: the gradebook, Attendance, the class portfolio, submissions, search results, profile headers and the student sidebar. An eye button shows one student's flags when you need them. It stays as you left it, so you can arm it before a lesson. From version ${extensionVersions.next}, flags show as Toddle shows them until you switch them off; earlier versions hid them from the start.`,
+ title: "Conceal student flags",
+ body: `${flags.summary} The switch sits in Toddle's header and the extension's menu, and stays as you left it, so you can set it before a lesson.`,
},
{
mineral: "gold",
@@ -64,25 +70,25 @@ const features = [
mineral: "tanzanite",
free: true,
title: "My classes",
- body: "The Toddle home page's course filter gains a third option beside All courses and My courses: only the classes you actually teach. It is remembered, and a tag by the heading says when it is on.",
+ body: "The Toddle home page's course filter gains a third option beside All courses and My courses: only the classes you actually teach. It switches on and off at once, it is remembered, and a tag by the heading says when it is on. It only appears where you are on a class.",
},
{
mineral: "terracotta",
free: false,
title: "The student sidebar",
- body: "Click a student anywhere they appear in Toddle — class pages, name links, gradebook student cells, every tab of the Attendance dashboard — and they dock to the side until you close it: one larger photo, grade · age · class of, Student Group, room number, homeroom advisor, and contacts with their relationship, tap-to-call phone and email.",
+ body: "Click a student anywhere they appear in Toddle — class pages, name links, gradebook student cells, every tab of the Attendance dashboard — and they dock to the side until you close it: one larger photo, grade · age · class of, their email (click to copy it, or the envelope to write), Student Group, room number, homeroom advisor, and contacts with their relationship, tap-to-call phone and email. In School setup, Toddle's admin portal, it steps aside, so a click there does what Toddle does.",
},
{
mineral: "copper",
free: false,
- title: "Now, and today",
- body: "Beside the photo, a Now card: the student's attendance code for the block happening now, the block, class, room and timetabled teacher — or their next class, or that they are finished or have no classes. Below, today's daily attendance code, and their classes in two tabs: Today, in time order with each period's attendance code and the current one first, and All.",
+ title: "Now, and today's timetable",
+ body: "Beside the photo, a Now card: the student's attendance code for the block happening now, the block, class, room and timetabled teacher. Today's timetable is its own part: today's periods in time order with each one's attendance code, “No classes today”, or “Couldn't load timetable” with a Retry button. The rest of the panel never waits for it.",
},
{
mineral: "sodalite",
free: false,
title: "The class view, and messages",
- body: "Click a class for the block it is in now, its primary teacher, parent course and room, and every teacher with their display title and email. Message buttons for staff, the student, the family channel and each family member open Toddle's own chat, under your school's messaging rules.",
+ body: "Click a class for the block it is in now, its primary teacher, parent course and room, and every teacher with their display title and email. Tick the teachers you want and email them all at once, or copy their addresses. Message buttons for staff, the student, the family and each family member open Toddle's own chat, under your school's messaging rules.",
},
{
mineral: "cobalt",
@@ -135,13 +141,17 @@ const shots = [
caption: "My classes · Free",
},
{
+ /* Taken in version 0.8.2, which hid flags; retake it on 0.9.0. The alt
+ text describes the picture as it is. */
src: flagsShot,
- alt: "A student's profile card in Toddle with a Show flags button where the flags would be, so they stay hidden until pressed; personal details are hidden behind grey bars.",
- caption: "Hide student flags · Free",
+ alt: "A student's profile card in Toddle with a Show flags button where the flags would be, as in version 0.8.2; personal details are covered by grey bars.",
+ caption: "The flag switch · Free",
},
{
+ /* Taken in version 0.8.2; 0.9.0 has a switch for every feature, grouped
+ by where it works. Retake it on 0.9.0. */
src: menuShot,
- alt: "The extension's toolbar menu: the organisation licence and version, a Show student flags switch under Free, Student sidebar and Primary teacher names switches under With a licence, and Send feedback and Help.",
+ alt: "The extension's toolbar menu in version 0.8.2: the organisation licence and version, a Show student flags switch under Free, Student sidebar and Primary teacher names switches under With a licence, and Send feedback and Help.",
caption: "The toolbar menu",
},
{
@@ -159,12 +169,13 @@ const shots = [
const facts = [
{
label: "Where it runs",
- value: "Only on web.toddleapp.com. It has no access to any other site.",
+ value:
+ "Only on web.toddleapp.com. It has no access to any other site. From version 0.9.0 the flag switch, the student sidebar and the home page's additions run in the extension's own isolated part of the browser, drawn inside closed shadow roots that no script on Toddle's page can read or click into; the gradebook and the Attendance dashboard are moving the same way.",
},
{
label: "What it reads",
value:
- "What Toddle already lets you see — gradebook results, rubric descriptors and levels as your school set them up, class staff, student details, today's timetable and attendance, and a student's flags when they are shown — through an exact list of read-only queries to Toddle's own API, for you at the screen, reusing the session you are already signed in with. It never asks for your password. Toddle's sign-in headers stay in the tab's memory, in one script in Toddle's page, and go nowhere but Toddle.",
+ "What Toddle already lets you see — gradebook results, rubric descriptors and levels as your school set them up, class staff, student details, today's timetable and attendance, and a student's flags each time the sidebar opens — through an exact list of read-only queries to Toddle's own API, for you at the screen, reusing the session you are already signed in with. It never asks for your password. Toddle's sign-in headers stay in the tab's memory, in one script in Toddle's page, and go nowhere but Toddle.",
},
{
label: "What it writes",
@@ -184,13 +195,13 @@ const facts = [
{
label: "What it stores",
value:
- "On your own machine only: your switch settings, your licence key if you have 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, your My classes choice and the academic year Toddle is showing. No student data: what it reads is held in the tab's memory until the tab is closed or reloaded, and never written to storage.",
+ "On your own machine only: your switch settings, your licence key if you have 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 concealed, your My classes choice and the academic year Toddle is showing. No student data: what it reads is held in the tab's memory until the tab is closed or reloaded, and never written to storage.",
},
];
const title = "Toddle Enhancement Extension — Nyuchi Learning";
const description =
- "A Chrome extension for the Toddle gradebook: every rubric expanded into per-criterion columns, CSV export, and a switch that hides student flags so you can share your screen without compromising privacy.";
+ "A Chrome extension for the Toddle gradebook: every rubric expanded into per-criterion columns, CSV export, a student sidebar, and a free switch that conceals student flags when your screen is shared.";
/* The claims here are the ones a search result or an assistant will repeat, so
they mirror the privacy policy rather than the marketing copy. */
@@ -213,10 +224,10 @@ const schema = [
publisher: { "@id": "https://learning.nyuchi.com/#organization" },
featureList: [
"Expands every Toddle assessment tool into per-criterion columns",
- "Hides student flags across Toddle with one switch",
+ "Conceals student flags across Toddle with one free switch, for a shared screen; never hides Toddle's own flags",
"CSV export of results and submission status",
"A My classes filter on the Toddle home page",
- "A student sidebar anywhere a student appears in Toddle: photo, grade, contacts, where they are now, today's classes and attendance",
+ "A student sidebar anywhere a student appears in Toddle: photo, grade, email, contacts, where they are now, today's timetable and attendance",
"A class view with every teacher, and message buttons that open Toddle's own chat",
"The Attendance dashboard's Students tab with each student's year group, current class and teacher",
"Primary teacher names beside each class",
@@ -345,26 +356,26 @@ const schema = [
Toddle marks students with flags. They are useful — that is the point of
- them — and they are exactly what you do not want on a projector, in a
+ them — and what they say is exactly what you do not want on a projector, in a
screen share with a parent, in a lesson observation, or in a screenshot
- pasted into a planning document. One switch takes them off the screen.
+ pasted into a planning document. One switch conceals them while your screen
+ is shared.
-
It hides flags, not names
+
It conceals flags, not names
Students stay identifiable to you — you are still teaching, and a
- gradebook of anonymous rows is no use to anyone. It is the flag that
- comes off the screen, because that is the piece of information the
+ gradebook of anonymous rows is no use to anyone. It is what a flag says
+ that comes off the screen, because that is the piece of information the
room should not be reading over your shoulder.
- Toddle draws the gradebook on an HTML canvas, so a flag there cannot
- be hidden with styling. The extension empties the flag out of the
- cell before the browser paints it, and hides the ones Toddle
- renders as ordinary page elements everywhere else.
+ The extension no longer hides Toddle's own flags: they stay where Toddle puts
+ them, with their links and documents in Toddle's student popover. Concealing
+ them is your choice, off until you switch it on. {flags.reveal}
@@ -373,14 +384,14 @@ const schema = [
It stays on. The setting
persists as you move around Toddle and across browser sessions, so it
- is something you arm before a lesson rather than something you have
+ is something you set before a lesson rather than something you have
to remember at the worst possible moment.
The switch shows its state.
- It sits in the Toddle header and reads as on or off at a glance —
- check it before you share, the same way you check the right window is
- selected.
+ It sits in the Toddle header, the same size as Toddle's own buttons, and
+ reads as on or off at a glance — check it before you share, the same way
+ you check the right window is selected.
@@ -389,12 +400,15 @@ const schema = [
What it does not do
- It hides flags. It does not hide names, and it does not stop anyone
- opening a student's profile deliberately. It works by changing what
- Toddle draws, which means it depends on Toddle's own markup and a
- Toddle release could change that. Arm it before you share, check the
- switch is on, and treat it as a good lid — not as a compliance control
- and not as a substitute for your school's safeguarding practice.
+ It conceals flags. It does not hide names. {flags.limit} It works by
+ changing what Toddle draws, which means it depends on Toddle's own markup,
+ and a Toddle release could change that. Switch it on before you share, check
+ it is on, and treat it as a good screen — not as a compliance control and not
+ as a substitute for your school's safeguarding practice.
+
+
+ This describes version {extensionVersions.next}, the next release on the
+ Chrome Web Store. {flags.before}
@@ -425,15 +439,23 @@ const schema = [
Press Add to Chrome. Chrome asks you to
confirm, and that is the install.
-
Open any Toddle gradebook: Gradebook → Assessments.
- The new columns appear, and the flag switch is in the Toddle header, ready
- before your next lesson.
+ A welcome page opens once: what it does, a switch for every feature, and a box
+ for a licence key if you have one.
+
+
+ Open Toddle. The flag switch is in Toddle's header, and My classes is in the
+ home page's course filter. With a licence, the per-criterion columns are in any
+ gradebook (Gradebook → Assessments) and a click on a student opens the
+ sidebar.
No files to download, no Developer mode, nothing to unzip. Updates arrive on
- their own, the way every other Chrome extension does.
+ their own, the way every other Chrome extension does. Stuck?
+ How to use the extension, in our help centre.
- Because it is on the Chrome Web Store, IT can force-install it to a teaching
- organisational unit by its extension ID — the standard route, with no
- self-hosted package to maintain and no Developer mode on anyone's machine.
- Nothing for staff to click, and updates arrive on their own.
+ Because it is on the Chrome Web Store, IT can install it for every teacher by its
+ extension ID, with no self-hosted package to maintain and no Developer mode on
+ anyone's machine. Chrome keeps it up to date from the store.
+
+
+
+ In the Google Admin console, go to Devices → Chrome → Apps & extensions → Users & browsers, and pick the organisational unit your teachers are in. Students have no use
+ for it.
+
+
+ Add it by ID: {toddleExtension.extensionId}, from the Chrome Web Store.
+
+
+ Choose Force install for a rollout IT
+ owns, or Allow install to leave it to
+ each teacher. Pinning it to the toolbar puts its menu one click away.
+
+
+
+ Without the Admin console, the same force-install policy works through Group
+ Policy or Intune on Windows and a configuration profile on macOS, with the ID and
+ the store's update URL,
+ {toddleExtension.extensionId};{toddleExtension.storeUpdateUrl}.
+
+
+ The licence. An organisation licence is
+ one key for your school's email domain. A key cannot be pushed by policy, so send
+ it to staff; each teacher pastes it in once, from the welcome page or the
+ toolbar menu's Enter a licence key. If your network filters outbound
+ traffic, allow {toddleExtension.licenceHost}
+ so the daily check for cancelled keys can run. If it is blocked, licences keep
+ working on the last result.
- The extension ID is
- {toddleExtension.extensionId}. We will send
- you the console steps and the full answer to "what can this thing see?" — which
- somebody will ask, and should.
+ We will send you the full deployment guide, and the full answer to "what can this
+ thing see?" — which somebody will ask, and should.
+ {ownership.statement} That covers its code, its design and its documentation, in
+ every version, including the free features. {ownership.licence}
+ {ownership.noReuse} {ownership.reading}
+
+
What a licence lets you do
Install and use the {toddleExtension.name} for teaching and administration at the
school or organisation the licence covers, on as many machines as that licence
- allows, for as long as it is current.
+ allows, for as long as it is current. The free features may be used by anyone, on
+ the same terms. Using it gives you no other rights in it.
Resell it, sublicense it, or charge anyone else for access to it.
+
+ Copy, modify, reuse or redistribute its code, in whole or in part, in another
+ product or service, without our written permission. That includes publishing it,
+ or a changed copy of it, anywhere else.
+
Take it apart in order to build a competing product. Reading the code to satisfy
yourself about what it does is fine — we encourage it.
diff --git a/src/pages/toddle-enhancement-extension.astro b/src/pages/toddle-enhancement-extension.astro
index 9818b5c..dc04194 100644
--- a/src/pages/toddle-enhancement-extension.astro
+++ b/src/pages/toddle-enhancement-extension.astro
@@ -17,7 +17,14 @@ import { Picture } from "astro:assets";
import type { ImageOutputFormat } from "astro";
import BaseLayout from "../layouts/BaseLayout.astro";
import { toddleExtension } from "../data/site";
-import { extensionVersions, flags, pricing, refunds, toddleNotice } from "../data/legal";
+import {
+ extensionVersions,
+ flags,
+ ownership,
+ pricing,
+ refunds,
+ toddleNotice,
+} from "../data/legal";
import gradebookShot from "../assets/screenshots/01-gradebook.jpg";
import sidebarShot from "../assets/screenshots/02-student-sidebar.jpg";
import todayShot from "../assets/screenshots/03-today.jpg";
@@ -621,7 +628,10 @@ const schema = [
If your IT team would rather verify this than take our word for it, a
Chrome extension is a folder of readable JavaScript — they can unpack
- the installed package and read every line of it. We will also answer
+ the installed package and read every line of it. Reading it to check what it
+ does is welcome; the code is ours, and is not licensed for reuse elsewhere (see
+ who owns the extension). We will
+ also answer
specific questions at
diff --git a/src/pages/toddle-enhancement-extension/releases.astro b/src/pages/toddle-enhancement-extension/releases.astro
new file mode 100644
index 0000000..8b0c164
--- /dev/null
+++ b/src/pages/toddle-enhancement-extension/releases.astro
@@ -0,0 +1,124 @@
+---
+/**
+ * Releases — what changed in each version of the Toddle Enhancement Extension.
+ *
+ * The public home of the extension's release notes. The extension's source
+ * repository is private and its GitHub releases are internal test builds, so
+ * this page has no downloads and no GitHub links: teachers install and update
+ * from the Chrome Web Store. The extension's toolbar menu links here, to
+ * `#v`, so every version keeps an element with that id.
+ *
+ * The notes live in src/data/releases.ts; adding a release is one edit there.
+ */
+import BaseLayout from "../../layouts/BaseLayout.astro";
+import { releaseDate, releases, testBuilds } from "../../data/releases";
+import { toddleExtension } from "../../data/site";
+
+const title = "Releases — Toddle Enhancement Extension — Nyuchi Learning";
+const description =
+ "What changed in each version of the Toddle Enhancement Extension, in plain words for teachers: version 0.9.0, coming soon to the Chrome Web Store, and every version before it.";
+
+const storeCta = toddleExtension.webStore
+ ? { label: "Add to Chrome", href: toddleExtension.webStore }
+ : { label: "Tell me when it is live", href: toddleExtension.enquiry };
+
+const statusLabel = {
+ coming: "Coming soon to the Chrome Web Store",
+ store: "On the Chrome Web Store",
+ early: "Before the Chrome Web Store",
+} as const;
+
+const statusClass = {
+ coming: "badge badge-accent",
+ store: "badge badge-success",
+ early: "badge badge-info",
+} as const;
+
+---
+
+
+
+
+ What changed in each version, newest first. You install and update the extension
+ from the Chrome Web Store, and Chrome keeps it up to date on its own: there is
+ nothing to download here. To see which version you have, open the extension's menu
+ from Chrome's toolbar.
+
+ {testBuilds.versions.map((id) => (
+
+ ))}
+ Versions {testBuilds.from} to {testBuilds.to} were test builds for schools
+ piloting the extension. Their changes arrive together in{" "}
+ {testBuilds.arriveIn}.
+
+
+
diff --git a/tests/seo.test.ts b/tests/seo.test.ts
index cd31d57..08555d0 100644
--- a/tests/seo.test.ts
+++ b/tests/seo.test.ts
@@ -18,6 +18,7 @@ const built = (relative: string) => read(join("dist", relative));
const PAGES = [
"index.html",
"toddle-enhancement-extension/index.html",
+ "toddle-enhancement-extension/releases/index.html",
"legal/privacy/index.html",
"legal/terms/index.html",
"legal/security/index.html",
@@ -212,3 +213,56 @@ describe("the security page", () => {
);
});
});
+
+describe("the releases page", () => {
+ /* The extension's menu links to #v, and its repository is
+ private: the page must keep every anchor and offer nothing to download. */
+ const page = built("toddle-enhancement-extension/releases/index.html");
+
+ it("gives each version, and each test build, its own anchor", () => {
+ for (const version of [
+ "0.9.0",
+ "0.8.2",
+ "0.8.1",
+ "0.8.0",
+ "0.8.3",
+ "0.8.13",
+ ]) {
+ expect(page).toContain(`id="v${version}"`);
+ }
+ });
+
+ it("says 0.9.0 is coming to the Chrome Web Store", () => {
+ expect(page).toContain("Coming soon to the Chrome Web Store");
+ });
+
+ it("links to no GitHub release and offers no download", () => {
+ expect(page).not.toMatch(/github\.com/i);
+ expect(page).not.toMatch(/\.zip\b|\.crx\b/i);
+ });
+
+ it("is linked from the extension page and llms.txt", () => {
+ expect(built("toddle-enhancement-extension/index.html")).toContain(
+ 'href="/toddle-enhancement-extension/releases"',
+ );
+ expect(built("llms.txt")).toContain(
+ "https://learning.nyuchi.com/toddle-enhancement-extension/releases",
+ );
+ });
+});
+
+describe("prices", () => {
+ /* The organisation licence is US$149.99 a year, confirmed by the founder.
+ Any other figure for it anywhere on the site is a mistake. */
+ it("states the organisation price as US$149.99 everywhere", () => {
+ for (const file of [
+ "toddle-enhancement-extension/index.html",
+ "legal/terms/index.html",
+ "llms.txt",
+ ]) {
+ const text = built(file);
+ expect(text, file).toContain("US$149.99");
+ expect(text, file).not.toMatch(/US\$\s?(?!149\.99\b|5\b)\d/);
+ }
+ });
+});