diff --git a/PRIVACY.md b/PRIVACY.md index 37abd9d..9c86f32 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,51 +1,115 @@ -# 影伴隐私说明 +# 影伴隐私说明 / Shadow Mate Privacy Policy -线上展示版:[sm.shadow.wang/privacy](https://sm.shadow.wang/privacy),包含中文和 English 两个版本。仓库根目录的 `privacy-policy.html` 是发布源文件;它会随 Vercel 构建输出为 `dist/privacy.html`,由生产站点直接提供。 +当前版本:`privacy-v2` -影伴面向家庭和未成年学习者,默认遵循数据最小化原则。本文件描述当前开源版本的技术行为。 +生效日期:2026 年 8 月 20 日 / August 20, 2026 -## 收集和保存的数据 +线上展示版:[sm.shadow.wang/privacy](https://sm.shadow.wang/privacy) + +仓库根目录的 `privacy-policy.html` 是中英文线上发布源文件。本文件描述同一数据边界;两者必须同步更新。 + +## 中文 + +影伴面向家庭和未成年学习者,默认遵循数据最小化原则。本说明描述当前 Dogfooding 和小规模内测版本的技术行为。 + +### 收集和保存的数据 - 家长用于登录的邮箱,由 Supabase Auth 处理。 - 家庭空间名称。 - 学习者显示名称和年级。建议使用昵称,不填写真实姓名。 -- 打卡、积分、书架和阅读日志等学习状态。 +- 打卡、积分、书架、阅读日志、奖励和兑换等学习状态。 +- 私有后端活动事件:产品标识、随机事件 ID、内部家庭和学习者 ID、协议允许的事件类型、事件发生和服务端接收时间、家庭时区、客户端版本、操作用户 ID,以及少量有类型和长度限制的枚举、布尔值或计数诊断字段。事件类型仅用于家庭启用、学习者创建、核心激活、有效成长行为、留存达标、奖励兑现、同步失败和本地朗读失败。 + +后端活动事件只用于内测漏斗、留存和连续使用统计,以及同步和本地朗读故障诊断。它们不接受自由文本、完整错误堆栈、页面 URL、邮箱、儿童显示名称、学习内容或语音文本;业务事实仍以积分、奖励和兑换等业务表为准。 + +当前版本不要求儿童提供邮箱、手机号、生日、学校、地址、精确位置或照片,也不包含广告。 + +### Vercel Analytics 与后端活动事件 -当前版本不要求儿童提供邮箱、手机号、生日、学校、地址、精确位置或照片,也不包含广告。应用通过 `@vercel/analytics` 使用 Vercel Web Analytics 记录匿名、聚合的页面访问数据;当前没有自定义事件,也不把学习状态、邮箱或儿童显示名称作为 Analytics 自定义字段发送。Vercel 文档说明页面访问数据可能包含时间、页面 URL、来源、设备/浏览器/操作系统和粗略地理位置;其设计不使用第三方 Cookie,也不跨站识别访客。使用 Vercel Analytics 的数据会发送到 Vercel 服务器,详情见 [Vercel Web Analytics Privacy and Compliance](https://vercel.com/docs/analytics/privacy-policy)。正式对外运营前仍需重新审查 URL、Referrer-Policy、家长同意和数据处理条款。 +应用通过 `@vercel/analytics` 使用 Vercel Web Analytics 记录匿名、聚合的页面访问数据。当前不向 Vercel Analytics 发送自定义事件,也不把学习状态、邮箱或儿童显示名称作为 Analytics 自定义字段发送。Vercel 页面访问数据可能包含时间、页面 URL、来源、设备、浏览器、操作系统和粗略地理位置;详情见 [Vercel Web Analytics Privacy and Compliance](https://vercel.com/docs/analytics/privacy-policy)。 + +“不向 Vercel Analytics 发送自定义事件”不表示影伴后端完全不记录事件。上节所述后端活动事件保存在 Shadow Mate 的私有 Supabase schema 中,与 Vercel Analytics 分开处理和保留。 当前版本的本地 Piper 朗读不把文本发送到影伴服务器。影伴不采集麦克风录音。 -## 家长同意和学习者档案 +### 家长同意和学习者档案 -学习者不是独立登录账号。创建第一个学习者或添加学习者前,登录用户必须确认自己是家长或监护人,并阅读本隐私说明。系统会在 Supabase 数据库记录以下同意审计字段:家庭 ID、认证用户 ID、同意类型 `learner_data_processing`、隐私说明版本 `privacy-v1` 和数据库生成的同意时间。客户端不能修改同意时间戳,也不能在没有同意记录的情况下通过公开 API 创建新的学习者档案。 +学习者不是独立登录账号。创建第一个学习者或添加学习者前,登录用户必须确认自己是家长或监护人,并阅读本说明。系统会记录家庭 ID、认证用户 ID、同意类型 `learner_data_processing`、隐私说明版本和数据库生成的同意时间。客户端不能修改同意时间戳,也不能在没有有效同意记录时通过公开 API 创建新的学习者档案。 -当前实现是“认证账号 + 家长/监护人确认 + 服务端审计记录”。创建学习者前,登录用户需要确认自己是家长或监护人并阅读本隐私说明。 +`privacy-v2` 明确披露私有后端活动事件、180 天保留期和导出边界。新同意记录使用 `privacy-v2`。已有 `privacy-v1` 记录继续作为有效的历史同意,不改写原始同意时间,也不要求仅因本次说明更新而重新确认。 -## 数据存放位置 +### 数据存放、隔离和访问 -- 离线学习状态保存在当前设备的浏览器存储中。 +- 离线学习状态保存在当前设备的浏览器 `localStorage` 中;登录会话保存在 `sessionStorage` 中。 - 登录后,家庭和学习状态同步到项目配置的 Supabase 数据库。 -- 登录会话使用浏览器会话存储;关闭对应浏览器会话后需要重新登录。 +- 学习数据按家庭隔离。匿名访问没有学习表权限;登录用户仍必须通过 `project_id = 'shadow-mate'`、家庭成员关系和 RLS 才能读取或修改记录。 +- 后端活动事件和内测批次记录位于私有 schema。浏览器、普通登录用户和家庭成员不能直接读取这些表或执行聚合/清理函数;活动事件只能由 owner/guardian 通过受控 RPC 写入协议允许的字段,受信运维角色才能读取或清理。 + +本机数据通常会保留到用户清除网站数据、使用隐私/无痕窗口、浏览器或系统自动清理,或更换访问域名。`localStorage` 是离线缓存,不应作为唯一备份;登录并同步后,云端家庭记录才是跨设备恢复来源。 -### 本机存储细节 +### 删除、导出和保留 -- 学习记录保存在浏览器的 `localStorage` 中,键名为 `shadow_mate_workbench_v1`;当前选择的孩子使用 `shadow_mate_active_profile` 保存。 -- 这些数据通常会在关闭浏览器后继续保留,直到用户清除该网站的浏览器数据、使用隐私/无痕窗口、浏览器或系统自动清理,或更换访问域名(每个域名都是独立的存储空间)。 -- `localStorage` 是离线缓存,不应当作为唯一备份。登录并同步后,云端家庭记录才是跨设备恢复来源;浏览器清理本机数据不会删除云端记录。 -- 登录会话保存在当前浏览器会话的 `sessionStorage` 中,因此重新打开新的浏览器会话可能需要再次点击邮件链接登录。 +- “清除本机数据”只删除当前设备的离线学习记录并退出登录,不删除云端记录。 +- 家庭 JSON 导出是可移植的家庭业务数据副本,包含家庭 ID/名称、学习者档案和状态、同意记录,以及 Growth Loop 的积分项目、学习者绑定、奖励、积分流水和兑换记录。 +- 导出不包含 Supabase Auth 身份或邮箱、会话/设备数据、Vercel Analytics 数据、私有后端原始活动事件 `private.learning_activity_events`,也不包含私有内测批次记录 `private.learning_beta_batches`。这些 server-only 记录不属于可移植的家庭业务历史。 +- 原始后端活动事件从服务端 `received_at` 起保留 180 天;超过 180 天的记录由受信清理任务删除。家庭或学习者删除时,关联活动事件会通过数据库外键级联删除,不等待保留期结束。 +- 内测批次记录随家庭删除级联删除。家庭所有者使用“删除全部家庭数据”时,家庭业务数据、同意记录、关联活动事件和内测批次记录都由同一家庭删除路径覆盖。 +- 共享 Supabase 项目中的家庭数据删除不会删除 Supabase Auth 身份;用户仍可使用同一邮箱重新登录。身份删除只在专用、隔离且经过服务端授权的账号删除流程中启用。 +- 当前没有独立的“撤回同意但保留家庭”自助流程。 -## 隔离和访问 +### 安全问题 -学习数据按家庭隔离。匿名访问没有学习表权限;登录用户仍必须通过家庭成员关系和 RLS 才能读取或修改记录。Supabase publishable key 是公开客户端标识,不是数据访问授权。 +不要在公开 Issue 中提交个人数据或安全漏洞。请使用仓库的私密漏洞报告功能,流程见 [SECURITY.md](SECURITY.md)。 -## 删除和保留 +## English -- 账号面板的“清除本机数据”只删除当前设备的离线学习记录并退出登录,不删除云端记录。 -- 家庭所有者可以从账号面板导出完整家庭 JSON 数据,也可以使用“删除全部家庭数据”删除当前家庭的云端记录、清理本机数据并退出登录。 -- 共享 Supabase 项目中的家庭数据删除不会删除 Supabase Auth 身份;用户仍可使用同一邮箱重新登录。身份删除只在专用、隔离的 Supabase 项目配置中启用。 -- 删除学习者或家庭时,同意记录随家庭或学习者所属家庭级联删除;当前没有独立的“撤回同意但保留家庭”的自助流程。 -- 数据保留期限、删除/导出/更正和撤回请求会根据产品运营地区和适用法律持续更新。 +Shadow Mate is designed for families and learners who may be minors. We apply data minimization by default. This policy describes the current Dogfooding and small-scale beta implementation. -## 安全事件 +### Data We Collect and Store -不要在公开 Issue 中提交个人数据或安全漏洞。请使用仓库的私密漏洞报告功能,流程见 [SECURITY.md](SECURITY.md)。 +- A parent's sign-in email, handled by Supabase Auth. +- The household space name. +- A learner display name and grade. We recommend a nickname rather than a real name. +- Learning state such as check-ins, points, bookshelf and reading logs, rewards, and redemptions. +- Private backend activity events: product identifier, random event ID, internal household and learner identifiers, an allowlisted event type, occurrence and server receipt timestamps, household timezone, client version, actor user ID, and a small typed and length-bounded set of enum, boolean, or count diagnostic fields. Event types are limited to household activation, learner creation, core activation, effective growth activity, retention qualification, reward redemption, sync failure, and local text-to-speech failure. + +Backend activity events are used only for beta funnel, retention, and sustained-use metrics, and for sync and local text-to-speech diagnostics. They do not accept free text, full error stacks, page URLs, email addresses, learner display names, learning content, or speech text. Product facts remain in the point, reward, and redemption records. + +The current version does not require a child to provide an email address, phone number, birthday, school, address, precise location, or photo. It does not contain advertising. + +### Vercel Analytics and Backend Activity Events + +The app uses Vercel Web Analytics for anonymous, aggregated page-visit data. It currently sends no custom events to Vercel Analytics and does not send learning state, email addresses, or learner display names as custom Analytics fields. Vercel page-visit data may include time, page URL, referrer, device, browser, operating system, and approximate location. See [Vercel Web Analytics Privacy and Compliance](https://vercel.com/docs/analytics/privacy-policy). + +“No custom events sent to Vercel Analytics” does not mean the Shadow Mate backend records no events. The private backend activity events described above are stored in Shadow Mate's private Supabase schema and have separate access and retention rules. + +The local Piper text-to-speech feature does not send text to Shadow Mate servers. Shadow Mate does not record microphone audio. + +### Parental Consent and Learner Profiles + +A learner is not an independent login account. Before creating or adding a learner, the signed-in user must confirm that they are the child's parent or guardian and read this policy. The system stores the household ID, authenticated user ID, consent type `learner_data_processing`, policy version, and a database-generated consent timestamp. The client cannot change the timestamp, and the public API cannot create a learner profile without a valid consent record. + +`privacy-v2` expressly documents private backend activity events, their 180-day retention, and the export boundary. New consent records use `privacy-v2`. Existing `privacy-v1` records remain valid historical consent; their original timestamps are not rewritten, and this policy update alone does not require a new confirmation. + +### Storage, Isolation, and Access + +- Offline learning state is stored in the current browser's `localStorage`; the sign-in session uses `sessionStorage`. +- After sign-in, household and learning state sync to the configured Supabase database. +- Learning data is isolated by household. Anonymous users have no learning-table access. Signed-in users must still pass `project_id = 'shadow-mate'`, household membership, and RLS checks. +- Backend activity events and beta batch records are in a private schema. Browsers, ordinary authenticated users, and household members cannot directly read those tables or execute aggregation or cleanup functions. An owner or guardian may write only allowlisted event fields through a guarded RPC; trusted operations roles may read or purge them. + +Local data normally remains until site data is cleared, a private/incognito session is used, the browser or operating system cleans it up, or the domain changes. `localStorage` is an offline cache and should not be the only backup. After synchronization, the cloud household record is the source for cross-device recovery. + +### Deletion, Export, and Retention + +- “Clear local data” removes only offline records on the current device and signs the user out; it does not delete cloud records. +- The household JSON export is a portable copy of household business data. It includes household ID/name, learner profiles and state, consent records, and Growth Loop point items, learner bindings, rewards, point ledger, and redemption records. +- The export excludes the Supabase Auth identity or email, session/device data, Vercel Analytics data, raw private backend activity events in `private.learning_activity_events`, and private beta batch records in `private.learning_beta_batches`. Those server-only records are not portable household business history. +- Raw backend activity events are retained for 180 days from server `received_at`; trusted cleanup deletes records older than 180 days. Deleting a household or learner cascades to its activity events without waiting for the retention period. +- Beta batch records cascade when the household is deleted. “Delete all household data” covers household business data, consent records, related activity events, and beta batch records through the same household deletion path. +- Deleting household data in the shared Supabase project does not delete the Supabase Auth identity. Identity deletion is available only in a dedicated, isolated, server-authorized account deletion flow. +- There is currently no self-service flow to withdraw consent while keeping the household. + +### Security Issues + +Do not submit personal data or security vulnerabilities in public issues. Use the repository's private vulnerability reporting process described in [SECURITY.md](SECURITY.md). diff --git a/README.md b/README.md index 81cbb27..397aceb 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ tests/e2e/ 离线、云端和数据生命周期测试 ## 当前边界 -影伴当前仓库版本为 v1.3.9,生产地址为 [sm.shadow.wang](https://sm.shadow.wang/)。它是面向家庭的开源 PWA,不包含广告;当前通过 [Vercel Web Analytics](https://vercel.com/docs/analytics/privacy-policy) 记录匿名、聚合的页面访问数据,也没有儿童独立账号体系。数据范围和删除方式见 [隐私说明](https://sm.shadow.wang/privacy),安全问题请按 [安全政策](SECURITY.md) 私下报告。 +影伴当前仓库版本为 v1.3.9,生产地址为 [sm.shadow.wang](https://sm.shadow.wang/)。它是面向家庭的开源 PWA,不包含广告,也没有儿童独立账号体系。当前通过 [Vercel Web Analytics](https://vercel.com/docs/analytics/privacy-policy) 记录匿名、聚合的页面访问数据;另有与 Vercel 分离、不可由普通用户读取的私有后端活动事件,用于内测指标和有限故障诊断,原始记录保留 180 天。具体字段、导出边界和删除方式见 [隐私说明](https://sm.shadow.wang/privacy),安全问题请按 [安全政策](SECURITY.md) 私下报告。 ### 英语发音 diff --git a/docs/architecture.md b/docs/architecture.md index a277481..a8552e2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -124,7 +124,7 @@ erDiagram 限制与演进条件: -- 如果需要排行榜、日级报表或推荐模型,应新增 append-only `learning_activity_events`; +- Growth Loop 使用 private、append-only 的 `learning_activity_events` 记录有界后端事件,用于内测漏斗和故障诊断;它不保存自由文本或学习内容,普通用户不可读取,原始记录按 `received_at` 保留 180 天。排行榜、推荐模型等新用途仍需另行评审; - 如果内容编辑频繁或需要付费授权,应新增内容集合、内容项和 entitlement 表; - 如果状态接近 1 MB、冲突频率升高或查询需要跨用户聚合,应将相关字段拆表; - 不直接删除 JSONB 状态;先双写、回填、验证,再切换读取。 @@ -146,7 +146,7 @@ erDiagram - 创建家庭时 `owner_user_id` 必须等于 `auth.uid()`。 - 家庭 owner 只能为自己创建初始 owner membership。 - owner/guardian 可创建和修改 learner profile/state。 -- 创建 learner profile 还必须存在当前 owner/guardian 的 `privacy-v1` 确认记录;记录只允许服务端默认时间戳写入,客户端无更新/删除权限。 +- 创建 learner profile 还必须存在当前 owner/guardian 的有效 `privacy-v1` 或 `privacy-v2` 确认记录;新确认写入 `privacy-v2`,历史 `privacy-v1` 继续有效。记录只允许服务端默认时间戳写入,客户端无更新/删除权限。 - `learning_save_state` 是 `SECURITY INVOKER`,不会绕过 RLS。 - `learning_is_household_owner` 是唯一的 `SECURITY DEFINER` 授权辅助函数:固定空 `search_path`、仅返回当前用户是否为指定家庭 owner、仅授予 `authenticated` 执行权,用于打断 household 与 membership 策略之间的递归。 - 更新策略同时具有 `USING` 与 `WITH CHECK`。 diff --git a/docs/privacy-policy-publishing.md b/docs/privacy-policy-publishing.md index bcc2502..b911422 100644 --- a/docs/privacy-policy-publishing.md +++ b/docs/privacy-policy-publishing.md @@ -37,4 +37,13 @@ http://localhost:5173/privacy/ 响应头应包含 `content-type: text/html`,页面应显示中文标题、品牌首屏、中英文内容和移动端布局。 -如果只是修订视觉样式或文字,保持 `privacy-v1`;如果收集范围、同意机制或处理目的发生实质变化,必须升级版本并按隐私同意数据库迁移流程处理,不能只覆盖 HTML。 +如果只是修订视觉样式或不改变含义的文字,可以保持当前隐私版本;如果收集范围、同意机制、处理目的、保留期或导出边界发生实质变化,必须升级版本并按隐私同意数据库迁移流程处理,不能只覆盖 HTML。 + +## privacy-v2 迁移说明 + +`privacy-v2` 于 2026-08-20 增加私有后端活动事件、180 天保留期和 server-only 导出边界说明。数据库提案 `20260820120000_growth_loop_beta_batches.sql` 将允许版本扩展为 `privacy-v1` / `privacy-v2`,并同步更新创建同意和学习者的 RLS 条件: + +- 新客户端只写入 `privacy-v2`。 +- 已有 `privacy-v1` 记录保留原始版本和时间戳,并继续满足学习者创建前的有效同意检查。 +- 客户端读取两个受支持版本,不能把历史家庭误判为未同意,也不自动补写或改写同意记录。 +- 任何进入共享 Supabase 的 canonical migration 仍必须由 Shadow Portal 控制面审批和执行;产品仓库只提交 proposal。 diff --git a/docs/user-guide.md b/docs/user-guide.md index 32e1695..719d510 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -60,7 +60,7 @@ MacBook 与无 GMS Android 的听感可能不同:前者通常优先使用 macO - 学习记录按孩子分别同步,避免多个孩子的打卡、积分和绘本记录混在一起。 - 家庭空间显示所有孩子记录中的最近同步时间。 - 切换孩子后,页面会加载该孩子对应的学习记录。 -- 家庭所有者可以在账号面板导出完整家庭 JSON 数据。 +- 家庭所有者可以在账号面板导出家庭业务 JSON(家庭/学习者状态、同意、积分和奖励记录);私有后端活动事件和内测批次记录不在导出范围内,但会在删除家庭时一并级联删除。 - “删除全部家庭数据”会删除当前家庭的云端学习数据、清理本机数据并退出登录;共享 Supabase 项目中的 Auth 身份不会被删除,之后仍可用同一邮箱登录。 ## 共享密码 diff --git a/privacy-policy.html b/privacy-policy.html index 4c44366..cf914ed 100644 --- a/privacy-policy.html +++ b/privacy-policy.html @@ -49,7 +49,7 @@

把成长留在
值得信任的地方

影伴隐私说明

-

产品:影伴 Shadow Mate · 当前版本:privacy-v1 · 生效日期:2026 年 8 月 12 日

+

产品:影伴 Shadow Mate · 当前版本:privacy-v2 · 生效日期:2026 年 8 月 20 日

影伴面向家庭和未成年学习者,默认遵循数据最小化原则。本说明描述当前 Dogfooding 和小规模内测版本的技术行为。

@@ -58,34 +58,42 @@

我们收集和保存什么

  • 家长用于登录的邮箱,由 Supabase Auth 处理。
  • 家庭空间名称。
  • 学习者显示名称和年级。建议使用昵称,不填写真实姓名。
  • -
  • 打卡、积分、书架和阅读日志等学习状态。
  • +
  • 打卡、积分、书架、阅读日志、奖励和兑换等学习状态。
  • +
  • 私有后端活动事件:产品标识、随机事件 ID、内部家庭和学习者 ID、协议允许的事件类型、事件发生和服务端接收时间、家庭时区、客户端版本、操作用户 ID,以及少量有类型和长度限制的枚举、布尔值或计数诊断字段。
  • 当前版本不要求儿童提供邮箱、手机号、生日、学校、地址、精确位置或照片,也不包含广告。

    +

    后端活动事件只用于内测漏斗、留存和连续使用统计,以及同步和本地朗读故障诊断。事件类型仅限家庭启用、学习者创建、核心激活、有效成长行为、留存达标、奖励兑现、同步失败和本地朗读失败。它们不接受自由文本、完整错误堆栈、页面 URL、邮箱、儿童显示名称、学习内容或语音文本;业务事实仍以积分、奖励和兑换等业务表为准。

    -

    分析服务和朗读功能

    -

    应用通过 Vercel Web Analytics 记录匿名、聚合的页面访问数据;当前没有自定义事件,也不把学习状态、邮箱或儿童显示名称作为 Analytics 自定义字段发送。页面访问数据可能包含时间、页面 URL、来源、设备、浏览器、操作系统和粗略地理位置。详情见 Vercel Web Analytics Privacy and Compliance

    +

    Vercel Analytics、后端活动事件和朗读功能

    +

    应用通过 Vercel Web Analytics 记录匿名、聚合的页面访问数据;当前不向 Vercel Analytics 发送自定义事件,也不把学习状态、邮箱或儿童显示名称作为 Analytics 自定义字段发送。页面访问数据可能包含时间、页面 URL、来源、设备、浏览器、操作系统和粗略地理位置。详情见 Vercel Web Analytics Privacy and Compliance

    +

    “不向 Vercel Analytics 发送自定义事件”不表示影伴后端完全不记录事件。上述后端活动事件保存在 Shadow Mate 的私有 Supabase schema 中,与 Vercel Analytics 分开处理和保留。

    当前版本的本地 Piper 朗读不把文本发送到影伴服务器。影伴不采集麦克风录音。

    家长同意和学习者档案

    -

    学习者不是独立登录账号。创建第一个学习者或添加学习者前,登录用户必须确认自己是家长或监护人,并阅读本隐私说明。系统会在 Supabase 数据库记录家庭 ID、认证用户 ID、同意类型 learner_data_processing、隐私说明版本 privacy-v1 和数据库生成的同意时间。客户端不能修改同意时间戳,也不能在没有同意记录的情况下通过公开 API 创建新的学习者档案。

    +

    学习者不是独立登录账号。创建第一个学习者或添加学习者前,登录用户必须确认自己是家长或监护人,并阅读本隐私说明。系统会在 Supabase 数据库记录家庭 ID、认证用户 ID、同意类型 learner_data_processing、隐私说明版本和数据库生成的同意时间。客户端不能修改同意时间戳,也不能在没有有效同意记录的情况下通过公开 API 创建新的学习者档案。

    +

    privacy-v2 明确披露私有后端活动事件、180 天保留期和导出边界。新同意记录使用 privacy-v2;已有 privacy-v1 记录继续作为有效的历史同意,不改写原始同意时间,也不要求仅因本次说明更新而重新确认。

    数据存放和访问

    本机缓存通常会在关闭浏览器后继续保留,直到用户清除该网站的浏览器数据、使用隐私/无痕窗口、浏览器或系统自动清理,或更换访问域名。登录并同步后,云端家庭记录才是跨设备恢复来源。

    删除、导出和保留

    -

    当前没有独立的“撤回同意但保留家庭”的自助流程。数据保留期限、删除、导出、更正和撤回请求会根据产品运营地区和适用法律持续更新。

    +

    当前没有独立的“撤回同意但保留家庭”的自助流程。身份删除只在专用、隔离且经过服务端授权的账号删除流程中启用。

    安全问题

    请不要在公开 Issue 中提交个人数据或安全漏洞。安全问题请通过仓库的私密漏洞报告功能提交。

    @@ -93,7 +101,7 @@

    安全问题

    Shadow Mate Privacy Policy

    -

    Product: Shadow Mate · Current version: privacy-v1 · Effective date: August 12, 2026

    +

    Product: Shadow Mate · Current version: privacy-v2 · Effective date: August 20, 2026

    Shadow Mate is designed for families and learners who may be minors. We follow data minimization by default. This policy describes the technical behavior of the current Dogfooding and small-scale beta version.

    @@ -102,40 +110,48 @@

    What We Collect and Store

  • The parent's email address used to sign in, handled by Supabase Auth.
  • The family space name.
  • The learner's display name and grade. We recommend using a nickname instead of a real name.
  • -
  • Learning activity such as check-ins, points, bookshelf items, and reading logs.
  • +
  • Learning state such as check-ins, points, bookshelf and reading logs, rewards, and redemptions.
  • +
  • Private backend activity events: product identifier, random event ID, internal household and learner identifiers, an allowlisted event type, occurrence and server receipt timestamps, household timezone, client version, actor user ID, and a small typed and length-bounded set of enum, boolean, or count diagnostic fields.
  • The current version does not require a child to provide an email address, phone number, birthday, school, address, precise location, or photo. It does not contain advertising.

    +

    Backend activity events are used only for beta funnel, retention, and sustained-use metrics, and for sync and local text-to-speech diagnostics. Event types are limited to household activation, learner creation, core activation, effective growth activity, retention qualification, reward redemption, sync failure, and local text-to-speech failure. They do not accept free text, full error stacks, page URLs, email addresses, learner display names, learning content, or speech text. Product facts remain in the point, reward, and redemption records.

    -

    Analytics and Reading Aloud

    -

    The app uses Vercel Web Analytics for anonymous, aggregated page-visit data. It currently sends no custom events and does not send learning status, email addresses, or learner display names as custom Analytics fields. Page-visit data may include the time, page URL, referrer, device, browser, operating system, and approximate location. See Vercel Web Analytics Privacy and Compliance for details.

    +

    Vercel Analytics, Backend Activity Events, and Reading Aloud

    +

    The app uses Vercel Web Analytics for anonymous, aggregated page-visit data. It currently sends no custom events to Vercel Analytics and does not send learning state, email addresses, or learner display names as custom Analytics fields. Page-visit data may include time, page URL, referrer, device, browser, operating system, and approximate location. See Vercel Web Analytics Privacy and Compliance for details.

    +

    “No custom events sent to Vercel Analytics” does not mean the Shadow Mate backend records no events. The private backend activity events described above are stored in Shadow Mate's private Supabase schema and have separate access and retention rules.

    The local Piper text-to-speech feature does not send text to Shadow Mate servers in the current version. Shadow Mate does not record microphone audio.

    Parental Consent and Learner Profiles

    -

    A learner is not an independent login account. Before creating or adding a learner, the signed-in user must confirm that they are the child's parent or guardian and read this policy. Supabase stores the household ID, authenticated user ID, consent type learner_data_processing, policy version privacy-v1, and a database-generated consent timestamp. The client cannot change the timestamp, and the public API cannot create a new learner profile without a consent record.

    +

    A learner is not an independent login account. Before creating or adding a learner, the signed-in user must confirm that they are the child's parent or guardian and read this policy. Supabase stores the household ID, authenticated user ID, consent type learner_data_processing, policy version, and a database-generated consent timestamp. The client cannot change the timestamp, and the public API cannot create a new learner profile without a valid consent record.

    +

    privacy-v2 expressly documents private backend activity events, their 180-day retention, and the export boundary. New consent records use privacy-v2. Existing privacy-v1 records remain valid historical consent; their original timestamps are not rewritten, and this policy update alone does not require a new confirmation.

    Where Data Is Stored and Who Can Access It

    Local cache normally remains after the browser closes until the site's browser data is cleared, a private/incognito session is used, the browser or operating system cleans it up, or the site domain changes. After synchronization, the cloud household record is the source for cross-device recovery.

    Deletion, Export, and Retention

    -

    There is currently no self-service flow to withdraw consent while keeping the household. Data retention periods and requests for deletion, export, correction, or withdrawal will be updated as the operating region and applicable laws require.

    +

    There is currently no self-service flow to withdraw consent while keeping the household. Identity deletion is available only in a dedicated, isolated, server-authorized account deletion flow.

    Security Issues

    Do not submit personal data or security vulnerabilities in public issues. Please use the repository's private vulnerability reporting process for security issues.

    - + diff --git a/scripts/check.mjs b/scripts/check.mjs index efcf6ad..ebc3750 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -20,6 +20,9 @@ const requiredFiles = [ ".vercelignore", "supabase/tests/learning_rls_test.sql", "supabase/migrations/20260811202411_child_privacy_consent.sql", + "supabase/migrations/20260820120000_growth_loop_beta_batches.sql", + "supabase/migrations/20260820121000_growth_loop_funnel_aggregation.sql", + "supabase/migrations/20260820122000_growth_loop_activity_cleanup.sql", ]; for (const file of requiredFiles) { @@ -104,6 +107,51 @@ for (const marker of [ if (/https:\/\/esm\.sh/i.test(cloud)) { throw new Error("Runtime CDN imports are not allowed"); } +for (const marker of [ + 'export const PRIVACY_POLICY_VERSION = "privacy-v2"', + 'Object.freeze(["privacy-v1", PRIVACY_POLICY_VERSION])', + '.in("policy_version", ACCEPTED_PRIVACY_POLICY_VERSIONS)', +]) { + if (!cloud.includes(marker)) throw new Error(`cloud.js is missing privacy migration step: ${marker}`); +} + +const privacyMarkdown = await readFile("PRIVACY.md", "utf8"); +const privacyHtml = await readFile("privacy-policy.html", "utf8"); +for (const [source, content, markers] of [ + ["PRIVACY.md", privacyMarkdown, [ + "privacy-v2", + "2026 年 8 月 20 日", + "August 20, 2026", + "180 天", + "180 days", + "private.learning_activity_events", + "private.learning_beta_batches", + "不向 Vercel Analytics 发送自定义事件", + "No custom events sent to Vercel Analytics", + "已有 `privacy-v1` 记录继续作为有效的历史同意", + "Existing `privacy-v1` records remain valid historical consent", + ]], + ["privacy-policy.html", privacyHtml, [ + "当前版本:privacy-v2", + "Current version: privacy-v2", + "2026 年 8 月 20 日", + "August 20, 2026", + "180 天", + "180 days", + "private.learning_activity_events", + "private.learning_beta_batches", + "不向 Vercel Analytics 发送自定义事件", + "No custom events sent to Vercel Analytics", + "已有 privacy-v1 记录继续作为有效的历史同意", + "Existing privacy-v1 records remain valid historical consent", + ]], +]) { + for (const marker of markers) { + if (!content.includes(marker)) { + throw new Error(`${source} is missing privacy-v2 consistency marker: ${marker}`); + } + } +} const piper = await readFile("src/piper-tts.js", "utf8"); for (const marker of [ @@ -115,6 +163,16 @@ for (const marker of [ const migrationDir = "supabase/migrations"; const migrations = (await readdir(migrationDir)).filter((name) => name.endsWith(".sql")).sort(); +const migrationsByVersion = new Map(); +for (const migration of migrations) { + const version = migration.match(/^(\d{14})_/)?.[1]; + if (!version) throw new Error(`Supabase migration is missing a 14-digit version: ${migration}`); + const existing = migrationsByVersion.get(version); + if (existing) { + throw new Error(`Supabase migration version ${version} is duplicated by ${existing} and ${migration}`); + } + migrationsByVersion.set(version, migration); +} const registryMigrationName = migrations.find((name) => name.endsWith("_projects_registry_compat.sql")); const registryRestrictionName = migrations.find((name) => name.endsWith("_restrict_project_registry_access.sql") diff --git a/src/cloud.js b/src/cloud.js index 043314b..1299208 100644 --- a/src/cloud.js +++ b/src/cloud.js @@ -15,7 +15,8 @@ const AUTH_PRODUCT_NAME = "影伴 Shadow Mate"; const ACTIVE_PROFILE_KEY = `${PRODUCT_ID.replaceAll("-", "_")}_active_profile`; const PASSWORD_PROMPT_KEY = `${PRODUCT_ID.replaceAll("-", "_")}_password_prompt_skipped`; export const GUARDIAN_CONSENT_TYPE = "learner_data_processing"; -export const PRIVACY_POLICY_VERSION = "privacy-v1"; +export const PRIVACY_POLICY_VERSION = "privacy-v2"; +const ACCEPTED_PRIVACY_POLICY_VERSIONS = Object.freeze(["privacy-v1", PRIVACY_POLICY_VERSION]); const PRIVACY_POLICY_URL = "https://sm.shadow.wang/privacy"; const MAX_CONFLICT_RETRIES = 2; const CONFLICT_RETRY_DELAY_MS = 200; @@ -881,7 +882,7 @@ function renderAccount() {

    家庭空间统一管理,学习记录按孩子分别同步。切换孩子后会加载对应的学习记录。

    ${choices}
    - ${hasGuardianConsent ? '

    家长同意已记录(隐私说明版本 privacy-v1)。

    ' : `

    添加学习者前,需要由家长或监护人确认隐私说明。

    ${guardianConsentField()}`} + ${hasGuardianConsent ? '

    家长同意已记录(当前或兼容的历史隐私说明版本)。

    ' : `

    添加学习者前,需要由家长或监护人确认隐私说明。

    ${guardianConsentField()}`}