diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index c07034f..d7848e6 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -451,6 +451,28 @@ bodies total 37,095 bytes. **This retires the Option C frozen-tail invariant by existed only to police a boundary inside an unreviewable single line, and the append-only rule above supersedes it. No bespoke hash is needed for future updates: the diff is the proof. +### 2026-10-07 - `docs/store/STORE_PATH.md` amended from Scan & Action's record: the build job and native sign-in are copied, not written; EU trader status is required even outside the EU; Android follows the iOS submission; three sessions to TestFlight and about nine to the first submission + +**No row is edited. Outside this file: `docs/store/STORE_PATH.md` (amended in place, each correction marked "Corrected by §8", and a new §8). The owner's other app, Scan & Action (`tornidomaroc-web/scan-and-action`, same Apple team), was read only, through the GitHub API; nothing in it was changed. Nothing is built, nothing in production changed. Rows 42, 69 and 81 are not touched, and Section 7 takes no deletions.** + +**WHAT SCAN & ACTION PROVES, AND WHAT IT DOES NOT.** It reached TestFlight on the owner's iPhone on 2026-09-28 (build 5) through a free `macos-26` job signed by an App Store Connect API key, after a wrong signing fix and its revert (#261, #262), a processing rejection ITMS-90683 for a missing photo-library purpose string (#263), and later Apple's cap of ten Development certificates per team (#277). It signs in to Google and Apple through one native plugin and revokes Apple tokens at deletion, proven on the phone (#258, #272). **It has never been submitted to App Review**, so it gives no evidence of what a reviewer decides on 4.2 or 3.1.3(f); its export options still carry `testFlightInternalTestingOnly`. + +**CORRECTIONS TO THE PATH.** (1) Google sign-in goes through `@capgo/capacitor-social-login`'s native sheet and `signInWithIdToken`, not the system browser and a deep link. (2) The build job is Scan & Action's two-job workflow, copied and adapted, so the App Store Connect key never sits on a runner that ran npm; the owner's iPhone is already registered on the team, which removes the device dead end. (3) **EU trader status is required even outside the EU:** Apple's page, read 2026-10-07, says *"Even if you don't distribute apps in the EU, you'll still need to declare a trader status"*, which corrects `PIVOT_PLAN.md` §9's "deferred, not blocking" and the owner's brief. It is declared once per account, so one declaration covers both apps; Scan & Action's board still lists it open. (4) **Android moves after the iOS submission:** the owner has no Android device, so Android Google sign-in cannot be witnessed, and Scan & Action records that the account cannot register as a Google Play merchant. (5) The estimates fall from four to **three sessions to TestFlight** and from ten to **about nine to the first submission**. + +**THE ARCHITECTURE, RE-JUDGED AND KEPT.** Capacitor's own configuration reference says of `server.url`: *"This is not intended for use in production."* That is weighed in §8.3. Scan & Action bundled its build at no extra cost because it was already a client-rendered app calling its backend with tokens; KnowFlow is server rendered with cookie sessions, so bundling costs it six to eight sessions. The deciding question is whether the native bridge reaches a page loaded from `tryknowflow.com`, and the first build answers it with a simulator launch check copied from Scan & Action; **if the bridge is absent, the plan switches to the bundled build before any later item is built.** + +**REPORTED TO THE OWNER, NOT FIXED: A LIVE RISK IN SCAN & ACTION.** Its signing job runs `xcodebuild archive` on an Xcode project that arrives as an artifact from the job that ran `npm ci`, and does not check that `project.pbxproj` matches the commit. A compromised npm dependency could add a build-phase script that Xcode runs on the runner holding the team's **Admin** App Store Connect key, which also reaches KnowFlow. The suggested fix and two smaller findings are in `STORE_PATH.md` §8.5; KnowFlow's copy of the workflow carries the check from its first version, and KnowFlow gets its own key. + +### 2026-10-07 - #137 PR B and PR C paused on the owner's ruling, registered and resumable; the path to the stores measured in `docs/store/STORE_PATH.md`: four sessions to TestFlight on the owner's iPhone, about ten to the first App Store submission + +**No row is edited (the brief allowed Section 7 additions only). Outside this file: `docs/store/STORE_PATH.md` (new, analysis and decisions only). Nothing is built, nothing in production changed, no grant moved. Rows 42, 69 and 81 are not touched, and Section 7 takes no deletions.** + +**THE PAUSE, AS THE OWNER RULED IT ON 2026-10-07.** Register #137's PR B (revoke the unused `authenticated` verbs: `knowledge_bases`, `conversations`, `messages`, `quiz_items` UPDATE and DELETE; `chunks` and `quizzes` UPDATE) and PR C (`alter default privileges for role postgres in schema public revoke insert, update, delete on tables from anon, authenticated`; `profiles` losing all three verbs with the dead signup upsert deleted; EXECUTE revoked from PUBLIC on the five functions of `docs/db-grants-audit-137.md` §1.7) are **paused, not dropped**. The owner's reasons: the core protection is live (PR A, `7265eef`, applied and proven 2026-10-07), the product has no real users yet, and the pace toward the stores is too slow. **Both stay registered under row #137 and resumable without a new audit:** PR B is six lines of the `INTENDED` matrix in `scripts/verify-public-grants.mjs` plus one migration, and the harness already exercises every verb `authenticated` keeps, so an over-reach goes red on its first run; PR C's plan is `docs/db-grants-audit-137.md` §4 and §6. **The agent agrees with the pause, with evidence (`STORE_PATH.md` §6):** every verb PR B would revoke is confined to the student's own rows by row security, so no cross-user write exists; and PR C's main purpose, catching a future table born with full API grants, is now served by `public-grants`, a required check since 2026-10-07 that fails any PR whose database holds a table outside the declared matrix. **Row #137's status cell still reads "NOT BUILT"; PR A is built and live (the two blocks below), and PR B and PR C are paused as recorded here.** The row is rewritten when #137 closes. + +**THE STORE PATH, MEASURED (`docs/store/STORE_PATH.md`).** Apple's App Review Guidelines, account-deletion page, App Privacy page, TestFlight overview and upcoming-requirements page, GitHub's Actions billing and runner-image pages, Codemagic's pricing, and Google Play's testing, target-API and payments pages were read on 2026-10-07 and are cited by section. The findings that change the plan: **(1) the architecture.** The file objects to `PIVOT_PLAN.md` §5's static client bundle ("Option B") and recommends a native shell that loads `tryknowflow.com`: guideline 4.2 judges features, content and UI, not where the HTML loads from, while the signed-in app is server rendered (the dashboard layout, home, settings, subjects and ask pages, the middleware, and all 13 API routes reading the session from cookies), so a static bundle would cost about 6 to 8 sessions and a second front end. The residual risk of the hosted shell is guideline 2.5.2 (code that changes features after review), rated medium. **This is the one decision the file asks the owner to sign.** **(2) Two facts that break today's code in any shell:** Google blocks OAuth inside embedded web views (since 2021-09-30), so the Google button must go through the system browser and a deep link; and `src/lib/platform.ts`'s store flag is build-time only, so server-rendered pages would show the Upgrade links 3.1.1 forbids unless the shell marks its requests. **(3) A blocker no row tracked: guideline 5.1.2(i) now requires explicit permission before personal data is shared with third-party AI.** The privacy page names Anthropic and Voyage AI; nothing in the app asks. **(4) In-app purchase:** 3.1.3(b) lets an app honour a web purchase only if the same item is also sold through in-app purchase, so the first submission goes under 3.1.3(f) (a free companion to a paid web tool, no purchase surface), with a one-session fallback (native requests served the free tier) if App Review rejects it. **(5) Building without a Mac costs nothing:** GitHub's standard runners are free on public repositories, this repository is public, and the GA `macos-26` image meets Apple's Xcode 26 requirement of 2026-04-28; larger runners bill even here, so the label is exactly `macos-26`. **(6) Google Play:** a personal account created after 2023-11-13 must run a closed test with 12 testers for 14 days before production; whether the owner's account is one is unknown and is the owner's to read; new apps must target API 36 since 2026-08-31. **(7) Verdicts:** Phase 6, Phase 7 (B5b and B6), Phase 9 and #137 PR B and PR C all move after the first submission, with evidence per item; the file objects to `PIVOT_PLAN.md` §7's "Must merge before the mobile shell" for Phase 7, because the hosted shell adds no surface the web app does not already expose. + +**THE ORDER RECOMMENDED.** To TestFlight on the owner's iPhone: T1 (the platform marker read at request time, native routing, the Google button hidden in native) → T2 (the Capacitor project, `ios/` and `android/`) → T3 (the owner: the App Store Connect app record and an API key in three GitHub secrets, about 30 minutes) → T4 (the iOS build and upload job on `macos-26`) → T5 (the owner installs through TestFlight): **about four sessions.** Then S1 (Google through the system browser, Sign in with Apple through the native sheet) → S4 (the AI consent) → S3 (a local study reminder and "Open in KnowFlow" for 4.2) → S2 (Apple token revocation on deletion) → S6/S7 (screenshots from the `/preview/*` routes, the App Store Connect forms) → S8 (the owner's reviewer account) → S9 (submit): **about ten sessions in total.** Android in parallel: its internal test starts as T2 lands and its closed test as S1 lands. **The first item to build is T1.** + ### 2026-10-07 - #137 PR A applied to production after the merge and proven on the live database: the 25 grant lines flipped exactly, the six policies gone, everything else byte-identical, the definer functions and every refusal witnessed inside a transaction that rolled itself back; `public-grants` now a required check **Row #137 is not edited: the owner's brief for this step allowed Section 7 additions only. Its text still reads "NOT BUILT", which is now true of PR B and PR C and no longer of PR A; the block below supersedes that sentence until the row is rewritten when #137 closes. Outside this file: nothing in the repository; on production, the one file `20261007_public_grants_dead_writes_and_anon.sql`, run in the SQL editor by the agent on the owner's written instruction of 2026-10-07 (the editor's "Potential issue detected" dialog for the DROP POLICY statements confirmed by the agent under that instruction); and on GitHub, `public-grants` added to the required status checks of `main`. Rows 42, 69 and 81 are not touched, and Section 7 takes no deletions.** diff --git a/docs/store/STORE_PATH.md b/docs/store/STORE_PATH.md new file mode 100644 index 0000000..b0d0894 --- /dev/null +++ b/docs/store/STORE_PATH.md @@ -0,0 +1,587 @@ +# The path to the stores: distance, order and decisions (2026-10-07) + +**Status: analysis and decisions only. Nothing was built, nothing in production +changed.** Written on `main` at `ab39878` on the owner's brief of 2026-10-07, +which restates the ruling of 2026-08-24: publishing to the Apple App Store and +Google Play is the primary goal of the project (`PIVOT_PLAN.md` §5). + +Every external rule below was read from its official source on 2026-10-07 and +is cited by section. Facts about this repository were read from the code at +`ab39878`. Facts about the owner's accounts are owner-attested and carry the +date they were last read. + +**AMENDED 2026-10-07, same day, after reading Scan & Action** (the owner's +other app, same Apple team, `tornidomaroc-web/scan-and-action` at its `main` +of 2026-10-07, read only through the GitHub API). §8 holds the lessons item by +item. The corrections it forced are made in place below and each is marked +*"Corrected by §8"*: Google sign-in through a native plugin instead of the +system browser (§1.2, S1), the build job copied rather than written (T4), EU +trader status, which Apple requires even outside the EU (§3.6), Android +sequenced after iOS (§5), and the session estimates (§0, §7). + +## 0. The answer first + +| Question | Answer | +|---|---| +| Distance to a TestFlight build on the owner's iPhone | **3 working sessions** of agent work, plus about 30 minutes of the owner's own console steps. Nothing in Phases 6, 7 or 9 is on this path. *Corrected by §8: was 4; the build job and its signing fixes are copied from Scan & Action, and the owner's iPhone is already registered on the team.* | +| Distance to the first App Store review submission | **About 9 working sessions** in total, the three above included, plus the owner's icon, a reviewer account, the EU trader declaration and the App Store Connect forms. *Corrected by §8: was 10.* | +| Distance to Google Play production | **After the iOS submission, not in parallel.** *Corrected by §8:* the owner has no Android device, so Android Google sign-in cannot be witnessed, and Scan & Action records that the account cannot register as a Google Play merchant. About 2 extra sessions, then a **fixed 14-day closed test with 12 testers** if the owner's Play account is subject to it (§5). | +| Mac needed? | **No.** GitHub's standard `macos-26` runner is free on this public repository and ships the Xcode 26 that Apple now requires (§4). | +| Money needed before the first submission | **None.** Every cost below is optional or post-launch, and each is named in USD where it appears. | +| The decision this file asks the owner to sign | **Ship the iOS app as a native shell that loads `tryknowflow.com`, not as the static client bundle `PIVOT_PLAN.md` §5 chose.** The reasons are in §1.1. | + +## 1. What the iOS app is: the one architecture decision + +### 1.1 Objection to `PIVOT_PLAN.md` §5 "Option B" + +§5 chose a client-rendered static bundle inside Capacitor, over the existing +`/api/*` routes, and called a shell that loads the hosted site (`server.url`) +"the textbook 4.2 trigger". This file objects to both halves, with evidence. + +**What Apple actually judges.** Guideline 4.2, verbatim: *"Your app should +include features, content, and UI that elevate it beyond a repackaged +website."* It names features, content and UI. It says nothing about where the +HTML is loaded from, and no guideline mentions web views at all (the full text +was searched on 2026-10-07; the only web-engine rule, 2.5.6, requires WebKit, +which Capacitor uses). A static bundle of the same screens shows the reviewer +exactly what the hosted site shows. **It changes the cost, not the verdict.** +`PIVOT_PLAN.md` §5 itself conceded this on 2026-08-24: *"Option B is NOT itself +an answer to 4.2"*. + +**What the static bundle would cost here.** The signed-in app is server +rendered. At `ab39878`: + +| Server-rendered surface that a static bundle cannot run | Server calls in the file | +|---|---| +| `src/app/[locale]/dashboard/layout.tsx` | 7 | +| `src/app/[locale]/dashboard/settings/page.tsx` | 6 | +| `src/app/[locale]/dashboard/page.tsx` (student home) | 5 | +| `src/app/[locale]/dashboard/knowledge/page.tsx` (subjects) | 1 | +| `src/app/[locale]/dashboard/agent/page.tsx` (ask) | 1 | +| `src/middleware.ts` (session refresh, auth guard, locale cookie) | n/a | + +All 13 API routes read the session from cookies through +`src/lib/supabase/server.ts`. A bundle served from `capacitor://localhost` +calls `tryknowflow.com` cross-origin, so cookies do not travel: every route +would need bearer-token auth and CORS, and the five pages and the middleware +would need rewriting as client code. That is roughly 6 to 8 sessions before +the first build, and it forks the app into two front ends that must be kept +equal forever. + +**What the hosted-site shell costs.** One Capacitor project whose start URL is +`https://tryknowflow.com//dashboard`, with the native layer this file +lists in §2.2. Cookies, server components, the middleware and every API route +work unchanged, because the web view is on the site's own origin. One front +end, one deploy. + +**The real risk of the hosted-site shell, stated.** It is guideline **2.5.2**, +not 4.2: apps *"may not download, install, or execute code which introduces or +changes features or functionality of the app"*. A shell over a live site can +change after review. The mitigations: the shell restricts navigation to +`tryknowflow.com` and opens every other link in Safari, the native features are +in the binary, and no feature is ever added to the app by a web deploy alone +without a matching app update. Apple has not cited 2.5.2 against account-based +SaaS shells in the cases this file could find, but that is not proof, so it is +recorded as the residual risk of this decision, rated medium. + +**Decision recommended: the hosted-site shell.** Reversible: if App Review +rejects it under 2.5.2 or 4.2, the static bundle remains possible later, and +every native piece built for the shell carries over. + +### 1.2 Two facts that break today's code inside any shell + +1. **Google sign-in does not work inside an embedded web view.** Google has + blocked OAuth in embedded web views since 2021-09-30 (`disallowed_useragent`; + [Google Developers Blog](https://developers.googleblog.com/upcoming-security-changes-to-googles-oauth-20-authorization-endpoint-in-embedded-webviews/)). + ~~Inside the shell, the Google button must open the system browser + (`SFSafariViewController` / `ASWebAuthenticationSession`) and return through + a deep link that hands the code to `/api/auth/callback` in the web view.~~ + *Corrected by §8:* inside the shell, Google and Apple both sign in through + the native sheet of `@capgo/capacitor-social-login`, which returns an ID + token that the page hands to Supabase's `signInWithIdToken`. Scan & Action + shipped exactly this (its PR #258) and witnessed both on the owner's iPhone + on 2026-09-28. No deep link and no redirect allow-list entry are needed. + Until it exists, the shell hides the Google button; email and password + work as they are. +2. **The store flag is build-time only.** `src/lib/platform.ts` reads + `NEXT_PUBLIC_KF_PLATFORM` at build time. Vercel builds the web app, so every + server-rendered page answers `web` and shows the Upgrade links that 3.1.1 + forbids in the app. The shell must identify itself on every request, + navigations included, which only a user-agent marker does + (Capacitor `appendUserAgent`), and `resolvePlatform` must read it. + `scripts/verify-store-purchase-links.mjs` extends to the request path. + +## 2. Distance, item by item, in order + +Agent sessions are estimates for one focused working session each, including +the proof script every PR here carries. "Owner" steps are console steps the +agent does not take because they need the owner's credentials. + +### 2.1 To a TestFlight build on the owner's iPhone + +| # | Item | What it actually requires | Who | Size | +|---|---|---|---|---| +| T1 | **Platform at request time** | A user-agent marker read by `resolvePlatform`; the middleware sends a native request for a marketing page (`/`, `/pricing`, `/refund`) to the app's entry; the Google button hidden in native until S1; proof extended to server-rendered pages. Web behaviour unchanged. | Agent | 1 session | +| T2 | **Capacitor project** | `@capacitor/core`, `@capacitor/ios`, `@capacitor/android`; `capacitor.config.ts` with `server.url`, `appendUserAgent`, `allowNavigation` limited to the site, an offline page bundled through `server.errorPath`; the `ios/` project with `TARGETED_DEVICE_FAMILY = 1` (`STORE_ASSETS.md` §5), `ITSAppUsesNonExemptEncryption = NO` (HTTPS only), bundle id `com.knowflow.app` (row #119 (iii), the owner's convention); a temporary icon from the in-app mark. `android/` generated in the same PR. | Agent | 1 session | +| T3 | **Owner console steps** | In App Store Connect: **Apps → +** (name, primary language Arabic, bundle id from T2, SKU); **Users and Access → Integrations → App Store Connect API → generate a new key for KnowFlow** (not Scan & Action's key, §8.4); in GitHub, create the environment `testflight` limited to deployments from `main` and put the Issuer ID, Key ID and the `.p8` contents in it as three environment secrets. The agent never sees the key. If "KnowFlow" is taken as an App Store name, choose another display name here; the bundle id is unaffected. *Corrected by §8:* the owner's iPhone is already a registered device on this team (Scan & Action, 2026-09-28), which development signing needs; nothing to do for it. | Owner | ~30 min | +| T4 | **The iOS build job** | *Corrected by §8:* **copy Scan & Action's `.github/workflows/ios-testflight.yml`** and adapt it (§8.4): two jobs so the key never sits on a runner that ran npm; `macos-26`; API-key automatic signing that archives for development and re-signs for the App Store at export (Scan & Action's PRs #261 and #262 are the dead end of forcing a Distribution identity); the stale-certificate sweep, because Apple caps a team at ten Development certificates and every hosted run makes one; `testFlightInternalTestingOnly` until the submission build; `CFBundleVersion` from the run number; `Package.resolved` committed. Triggered by `workflow_dispatch` and by a push to `main` that touches the native project only; never by a pull request. | Agent | 1 session | +| T5 | **Install** | Add the owner to an internal testing group in TestFlight; install the TestFlight app on the iPhone; install the build. Apple: internal testers are App Store Connect users, up to 100, and a build stays testable for 90 days ([TestFlight overview](https://developer.apple.com/help/app-store-connect/test-a-beta-version/testflight-overview/)). Internal testing needs no App Review. | Owner | ~10 min | + +**Total: 3 agent sessions and about 40 minutes of the owner** (*corrected by +§8*: was 4). At T5 the owner signs in with email and password and uses the +real app on the real phone. Sign in with Google arrives with S1. **The first +build is also the test of the one thing Scan & Action cannot vouch for:** that +the Capacitor bridge reaches a page loaded from `tryknowflow.com` (§8.3). T2 +carries a simulator launch check copied from Scan & Action's `ios-audit.yml` +that reads `window.Capacitor` inside the loaded page. + +### 2.2 From TestFlight to the first App Store submission + +| # | Item | What it requires, and why | Who | Size | +|---|---|---|---|---| +| S1 | **Google and Apple through one native plugin** | *Corrected by §8:* Scan & Action's route, PR #258: `@capgo/capacitor-social-login` with `providers = { google: true, apple: true, facebook: false, twitter: false }`, its patch that guards the AppTrackingTransparency import, and the binary audit that proves neither the Facebook SDK nor AppTrackingTransparency is linked (§8.2). The page calls `signInWithIdToken` with the token; the session cookie lands on `tryknowflow.com` inside the web view, so every server-rendered page sees it. Owner: an iOS OAuth client in KnowFlow's Google Cloud project, its id added to Supabase's Google provider as an authorized client; **Sign in with Apple** ticked on the App ID; **Client IDs = the App ID** in Supabase's Apple provider; the entitlement `com.apple.developer.applesignin` in the project. No Services ID, no six-monthly secret. Required by **4.8** (§3.3). | Agent + owner (20 min) | 1 session | +| S2 | **Apple token revocation on account deletion** | Apple: *"Apps that support Sign in with Apple should use the Sign in with Apple REST API to revoke user tokens"* ([Offering account deletion](https://developer.apple.com/support/offering-account-deletion-in-your-app/)). *Corrected by §8:* copy Scan & Action's design, PR #272, proven by one real deletion on the owner's iPhone on 2026-09-30: the delete dialog opens one more Apple sheet, its authorization code goes with `DELETE /api/account`, the server exchanges it at `/auth/token` and revokes at `/auth/revoke` with a client secret minted per call, nothing stored, and a failed revocation never blocks the deletion but is named in the response and logged (Scan & Action logged only failures at first, and success then read as an absence; its PR #274). Owner: one Sign in with Apple key for KnowFlow, its Team ID, Key ID and `.p8` set as server variables in Vercel. | Agent + owner (10 min) | 1 session | +| S3 | **Native value for 4.2** | Two affordances a website cannot have: (a) a **local daily study reminder** tied to the streak (`@capacitor/local-notifications`, no server, no push certificate, off until the student turns it on, per 5.1.2(i)'s rule that system features may not be required); (b) **"Open in KnowFlow"** for PDF and Word files from Files, Mail and the share sheet (document types in `Info.plist`; the shell passes the file into the existing upload with the 4 MB limit). Together with S1's native sign-in sheet these are the answer to 4.2 (§3.1). | Agent | 2 sessions | +| S4 | **Explicit consent before content goes to third-party AI** | New since this plan was written. **5.1.2(i)**: *"You must clearly disclose where personal data will be shared with third parties, including with third-party AI, and obtain explicit permission before doing so."* The privacy page already names Anthropic and Voyage AI (`privacy/page.tsx:27-35`), but nothing in the app asks. A one-time sheet before the first upload or question, in Arabic and English, naming both providers and what they receive, with Accept and Not now; not now leaves the student unable to upload or ask, with the reason shown. **No register row tracks this; it is a submission blocker.** | Agent | 1 session | +| S5 | **The owner's icon** | One 1024 × 1024 PNG, opaque, square corners (`STORE_ASSETS.md` §1.3, row #120). The temporary icon of T2 is fine for TestFlight, not for the store. | Owner | — | +| S6 | **Screenshots** | Apple accepts a 6.9" set at 1320 × 2868 (`STORE_ASSETS.md` §3.1). The `/preview/*` routes render every screen of §3.3 with fixtures and no sign-in, so the set can be made in Chromium at that size in both languages without touching a real account. | Agent | 0.5 session | +| S7 | **App Store Connect forms** | App Privacy (§3.4, answers drafted by the agent, entered by the owner), age rating questionnaire, category Education, privacy policy URL, support URL (the support e-mail of #19), Arabic and English listing text, review notes stating there is no purchase in the app and naming the reviewer account. | Agent drafts, owner enters | 0.5 session | +| S8 | **A reviewer account** | **2.1(a)**: *"include demo account info (and turn on your back-end service!) if your app includes a login."* A dedicated e-mail-and-password account with one subject, one material, a summary and a quiz. The owner creates it, because the agent does not create production accounts; the three protected test addresses stay untouched. | Owner | 15 min | +| S9 | **Submit** | Upload the final build through T4, attach it to the version, submit. | Owner | — | + +**Total from today to submission: about 9 agent sessions** (T1–T4: 3, S1–S4: +5, S6–S7 with S10 below: 1) and about 2 hours of the owner across the steps. +*Corrected by §8: was 10.* + +| # | Added by §8 | What it requires | Who | Size | +|---|---|---|---|---| +| S10 | **The operator named in the legal pages** | Scan & Action named its individual operator in its privacy policy and terms (its PR #269), because the account is an individual's and the App Store lists the seller by that name. KnowFlow's privacy and terms pages name no operator (`grep`, 2026-10-07). | Agent | inside S6/S7 | +| S11 | **EU trader declaration** | §3.6. Account-level, once, so one declaration covers Scan & Action and KnowFlow. | Owner | 10 min | + +## 3. Apple review risks, each with a verdict and the cheapest fix + +### 3.1 Guideline 4.2, minimum functionality + +**Text:** 4.2 as quoted in §1.1, and **4.2.2**: *"Other than catalogs, apps +shouldn't primarily be marketing materials, advertisements, web clippings, +content aggregators, or a collection of links."* + +**Verdict: passable, medium risk.** KnowFlow is an account-based tool with +upload, ask, summaries, quizzes and a streak, not a clipping. What a reviewer +hits without native value is a site in a frame: no native sign-in sheet, no +reaction to files, nothing that works with the network off. + +**Cheapest fix:** S1's native sign-in sheet, S3's reminder and Open-in, and +T2's bundled offline page, so airplane mode shows a designed screen instead of +a blank view. No offline copy of materials is proposed: it would need a local +store and sync, several sessions, and 4.2 does not ask for it by name. + +### 3.2 Account deletion in the app + +**Text:** **5.1.1(v)**: *"If your app supports account creation, you must also +offer account deletion within the app."* Apple's support page adds: *"Offer to +delete the entire account record, along with associated personal data ... +only offering to temporarily deactivate or disable an account is +insufficient."* + +**Verdict: met today, one gap.** Settings carries the deletion card with a +server-checked typed-email confirmation (`DELETE /api/account`, register +#61(b), PR #91), and it renders inside the shell unchanged. **The gap is +Sign in with Apple revocation (S2)**, which exists only once S1 adds Apple. + +### 3.3 Sign in with Apple, because Google sign-in is offered + +**Text:** **4.8**: apps that use a third-party login such as Google Sign-In +*"must also offer as an equivalent option another login service"* that limits +data to name and e-mail, lets the user keep the e-mail private, and does not +collect interactions for advertising without consent. The exceptions (own +accounts only, education or enterprise accounts, government ID, third-party +client) do not fit: KnowFlow offers Google beside its own accounts. + +**Verdict: blocker until S1.** The rule names the properties, not Apple, but +Sign in with Apple is the one service a reviewer will not question, and its +web half is already built and hidden (row #119). + +**Cheapest fix:** S1's native path, which row #119 (iv) already found removes +the Services ID, the six-monthly secret and its monitor. One residual from +#119 (ii) stays: an Apple "Hide My Email" relay address bounces our mail +unless `tryknowflow.com` is registered for Private Email Relay. Without it, a +student who signs in with Apple and hides the address receives no +confirmation or reset mail; Sign in with Apple accounts need no confirmation +mail, so this does not block review, but the owner's registration of the +domain (10 minutes, `#89` has the DNS) should go with S1. + +### 3.4 App Privacy labels + +**Text:** App Privacy details are *"required to submit new apps and app +updates"*, and must cover *"all of the data you or your third-party partners +collect"*; "collect" means transmitting data off the device so that it can be +accessed *"for a period longer than what is necessary to service the +transmitted request in real time"* +([App privacy details](https://developer.apple.com/app-store/app-privacy-details/)). + +**Verdict: no risk if answered truthfully; drafted here.** No analytics or +advertising SDK is in `package.json` (checked 2026-10-07), so nothing is used +for tracking and no App Tracking Transparency prompt is needed while AdMob +(Phase 9) is out. + +| Data type (Apple's names) | Collected | Linked to the user | Purpose | +|---|---|---|---| +| Contact info: Name, Email Address | Yes | Yes | App Functionality | +| User Content: Other User Content (uploaded files and their text), Customer Support (support mail) | Yes | Yes | App Functionality | +| User Content: questions asked (Other User Content) | Yes | Yes | App Functionality | +| Identifiers: User ID | Yes | Yes | App Functionality | +| Usage Data: Product Interaction (study events, usage counters) | Yes | Yes | App Functionality | +| Purchases | **No** in the app (no purchase surface in native) | — | — | +| Tracking | **No** | — | — | + +Anthropic and Voyage AI process the text of materials and questions; Apple's +"third-party partners" definition covers code added to the app, but the +data is collected by KnowFlow's own servers either way, so the rows above +already cover it. The consent of S4 is a separate requirement from the label. + +### 3.5 In-app purchase and the Paddle plan + +**Text:** **3.1.1**: to unlock features or subscriptions *"you must use +in-app purchase"*; outside the United States storefront *"apps and their +metadata may not include buttons, external links, or other calls to action +that direct customers to purchasing mechanisms other than in-app purchase."* +**3.1.3(b)**, multiplatform services, lets an app honour what was bought on +the web only *"provided those items are also available as in-app purchases +within the app"*. **3.1.3(f)**, free stand-alone apps: *"Free apps acting as +a stand-alone companion to a paid web based tool (i.e. VoIP, Cloud Storage, +Email Services, Web Hosting) do not need to use in-app purchase, provided +there is no purchasing inside the app, or calls to action for purchase +outside of the app."* + +**Verdict: the first submission goes under 3.1.3(f), with no purchase surface +in the app; medium risk.** 3.1.3(b) would require building StoreKit, App Store +Server Notifications and a second writer of `subscriptions` (register #73 +names exactly that hazard), several sessions, for a product with no paying +customer. The free app with Pro hidden is the cheapest shape, and the purchase +surface is already hidden by the platform flag once T1 makes it request-time. + +**What a reviewer hits:** the demo account is free, sees limits, and sees no +Upgrade, no price and no link to `/pricing` anywhere in the app (T1's proof +holds this). **If App Review rejects under 3.1.1 or 3.1.3(b)**, the next +cheapest fix is one session, not StoreKit: requests from the native shell are +served the free tier whatever the web subscription says, so nothing bought +elsewhere unlocks anything in the app and 3.1.1 has nothing to bite on. In-app +purchase is built only when the owner wants revenue from the app itself. + +**Reader apps (3.1.3(a)) do not apply:** they are limited to *"magazines, +newspapers, books, audio, music, and video."* + +### 3.6 Two more that the brief did not list + +- **2.1(a), a reviewer account and a live backend.** S8 covers the account. + The backend: the Supabase free project paused twice (rows #21, #63). Since + 2026-09-04 `deletion-orphan-watch` calls the database as `anon` every half + hour (its last three runs, 2026-10-06/07, all succeeded), which is the kind + of activity a pause waits for the absence of. **Verdict: mitigated, not + proven**; nothing in this repository reads Supabase's pause clock. The + certain fix is Supabase Pro (**USD 25 a month**, register #22), which this + file does not recommend before submission, only before public launch. +- **EU trader status: the plan's premise is wrong.** *Corrected by §8.* + `PIVOT_PLAN.md` §9 (2026-09-08) and the owner's brief of 2026-10-07 treat + trader status as EU-only and not blocking. Apple's page, read 2026-10-07: + *"Even if you don't distribute apps in the EU, you'll still need to declare + a trader status."* The declaration is made once per account (Business, + Agreements, Compliance), with a per-app override + ([DSA trader requirements](https://developer.apple.com/help/app-store-connect/manage-compliance-information/manage-european-union-digital-services-act-trader-requirements)). + The page does not say what happens at submission without it, so this file + treats it as a submission blocker, the cautious reading. **Cost: ten minutes + of the owner, once, and it covers both apps.** Scan & Action's board still + lists it open for that app. Declaring as a trader publishes the address and + phone only on EU product pages, and KnowFlow is not distributed in the EU. +- **Age rating.** Apple requires the updated questionnaire (deadline was + 2026-01-31 for existing apps; new apps answer it at creation, + [Upcoming requirements](https://developer.apple.com/news/upcoming-requirements/)). + Students may be minors (`PIVOT_PLAN.md` §8); the app has AI-generated text + and no user-to-user content. The owner answers in S7; no code follows from + it while ads are out. + +## 4. Building and signing iOS without a Mac + +| Route | Cost (USD) | Free tier | Effort here | Verdict | +|---|---|---|---|---| +| **GitHub Actions, standard `macos-26` runner** | **0** on a public repository: *"The use of standard GitHub-hosted runners is free: In public repositories"*; *"Larger runners are always charged for, even when used by public repositories"* ([GitHub Actions billing](https://docs.github.com/en/billing/concepts/product-billing/github-actions)). For contrast, a private repository pays $0.062 a minute for macOS. | Unlimited minutes; this repository is **PUBLIC** (read 2026-10-07). `macos-26` is GA arm64 and standard ([runner images](https://github.com/actions/runner-images)). | T4: one workflow, signing through the App Store Connect API key. Everything else in this repository already runs on Actions. | **Recommended.** | +| Codemagic | 0 within **500 macOS M2 minutes a month**, one build at a time; then **$0.095 a minute** ([pricing](https://codemagic.io/pricing/)). | 500 minutes is roughly 25–40 iOS builds a month. | A second CI system, its own secrets store and its own config file. | Fallback only, if Actions signing proves unworkable. | +| A rented cloud Mac (MacinCloud and similar) | Paid monthly; not priced here because it is not recommended. | None found. | Interactive Xcode, but a second machine to keep. | Rejected: costs money for what Actions does free. | +| Buying a Mac | Hundreds of USD. | — | — | Rejected. | + +**Two rules the build must keep.** (1) The label is exactly `macos-26`. The +`-large`, `-xlarge` and `-intel` labels are larger runners and bill even here. +(2) Apple requires uploads built with Xcode 26 and an iOS 26 SDK since +2026-04-28, and a minimum target of iOS 13 since 2026-09-09 +([Upcoming requirements](https://developer.apple.com/news/upcoming-requirements/)). +The `macos-26` image meets the first; Capacitor's current template meets the +second. + +**The public-repository caveat.** Workflow logs are public. The API key lives +only in Actions secrets, which GitHub masks and does not expose to pull +requests from forks; the iOS job runs on `workflow_dispatch` only. Nothing +signed is committed. + +## 5. Google Play: distance, and order + +**Owner-attested, 2026-08-24, no re-read since:** an active Play Console +developer account with published apps, $25 paid, Moroccan verification +cleared (`PIVOT_PLAN.md` §5). + +**Distance.** T2 generates `android/` in the same PR as `ios/`; T1, S1 (the +Google part), S3 (Open-in has an Android equivalent) and S4 serve both shells. +The Android-only work is a build job on the free `ubuntu-24.04` runner that +signs an app bundle with the owner's upload keystore (the owner's existing +process, stored as secrets) and about **2 sessions**. The owner creates the +app in Play Console and uploads the first bundle by hand. Play then requires +the **target API level 36 (Android 16) for new apps from 2026-08-31** +([target API](https://developer.android.com/google/play/requirements/target-sdk)), +which the current Capacitor Android template must be checked against in T2. + +**The fixed wait.** *"Google Play requires personal developer accounts +created after November 13, 2023, to test their apps before those apps are +eligible for distribution"*: **at least 12 testers opted in continuously for +14 days** in a closed test, then an application for production access that +Google answers within seven days +([testing requirements](https://support.google.com/googleplay/android-developer/answer/14151465)). +The page reads as per app. **Unknown here:** the owner's account type and +creation date, which decide whether this applies. If it does, it is up to +three weeks of calendar time that no engineering shortens, and the twelve +testers must be real people the owner recruits, since the product has no +users yet. + +**Billing.** Play's payments policy: apps *"may not lead users to a payment +method other than Google Play's billing system"*, including through +*"in-app webviews, buttons, links"* +([Payments policy](https://support.google.com/googleplay/android-developer/answer/9858738)). +The same native flag of T1 hides every purchase surface. Whether a free Play +app may honour a web subscription was **not answered by the page read**; +under the 3.5 fallback (native served the free tier) the question does not +arise. + +~~**Order: in parallel, Android's clock started as early as possible.**~~ +*Corrected by §8.* **Order: Android after the iOS submission.** Three facts +from Scan & Action, same owner, same accounts, overturn the parallel plan: + +1. **There is no Android device.** Scan & Action's board: Android Google + sign-in *"is UNVERIFIED and stays so until a device exists"*; a mismatch of + the Play App Signing SHA-1 fails with `[28444] Developer console is not set + up correctly` *"on the device only"*; and its open PR #285 records that the + emulator route was refused because it needs a typed password. KnowFlow's + Android sign-in would sit in the same unverifiable state. +2. **The account cannot sell through Play.** Scan & Action's invariant states + *"the Morocco-based developer account cannot register as a Google Play + merchant"* (owner-stated there, not read from Play here). So the Android app + is free with no purchase surface, exactly like iOS, and nothing on Android + earns money sooner. +3. **The twelve testers are people, and the product has none yet.** If the + 14-day rule applies, the owner must recruit them; starting the clock early + does not help without them. + +`T2` still generates `android/` so the two shells never diverge, and the +Android build job is about two sessions once a device and twelve testers +exist. + +## 6. Phases and items placed before the mobile build: before or after the first submission? + +| Item | Verdict | Evidence, and what a user or reviewer would hit if it were missing | +|---|---|---| +| **Phase 6, flashcards and spaced repetition** | **After.** | No guideline asks for it. The app already has upload, ask with citations, summaries, quizzes and a streak, which is the utility 4.2 judges. Register #82 already recommends holding it (free NotebookLM ships the same feature). A reviewer missing flashcards hits nothing. | +| **Phase 7, B5b deep upload hardening** | **After.** | The shell adds no attack surface: it calls the same public `/api/ingest` the web app has exposed since Phase 0, which already has the extension and MIME allowlist (B5a), sanitized file names (B4), a 4 MB limit (#50) and per-user daily caps. A reviewer uploads a normal PDF. The risk B5b closes (a crafted archive costing compute) exists today on the web and does not grow with the app. **This file objects to `PIVOT_PLAN.md` §7 "Must merge before the mobile shell"**: the gate assumed the shell was a new surface, and with the hosted-site shell it is not. | +| **Phase 7, B6 asynchronous ingestion** | **After, with one check in TestFlight.** | A 4 MB text file ingests in 80.5 s inside one request (register #50 §5 measurement), with the progress UI of #113. A reviewer's small PDF finishes in seconds. **The check:** on iOS, sending the app to the background during an upload may suspend the web view's request; #113's copy tells the student that leaving is safe. Test on the owner's iPhone in TestFlight (T5); if the upload dies, the copy changes before submission, not the architecture. | +| **#137 PR B** (unused `authenticated` verbs) | **After. The owner's pause is right.** | Every verb it would revoke is already confined by row security to the student's own rows (`docs/db-grants-audit-137.md` §2); no cross-user write exists. A reviewer cannot reach it. | +| **#137 PR C** (default privileges, `profiles`, EXECUTE from PUBLIC) | **After. The owner's pause is right, for a reason that did not exist a week ago.** | Its main purpose was to catch a future table born with full API grants. `public-grants` (required on `main` since 2026-10-07) now fails any PR whose database has a table outside its declared matrix, so S2 or S4, if either adds a table, must declare its grants there. The hygiene items (`profiles.plan`, EXECUTE from PUBLIC) are not reachable harm (audit §1.6, §1.7). | +| **Phase 9, AdMob** | **After, and better after.** | Not required by anything. Adding it before the first submission would add App Tracking Transparency, an advertising row in App Privacy, and the minors question (`PIVOT_PLAN.md` §8) to the first review. | +| **#22 Supabase free tier** | **After the first submission, before public launch.** | See §3.6. | +| **#70 cancel path, #81 the $49 price** | **Not on the app path.** | Both are web billing surfaces; the app has no purchase surface (§3.5). #81 is not edited by this file. | +| **#119 Sign in with Apple** | **Before (S1).** | Guideline 4.8. | +| **#120 store assets** | **Before (S5, S6).** | An upload or listing without them fails validation. | +| **5.1.2(i) AI consent** (no row) | **Before (S4).** | Guideline 5.1.2(i), quoted in S4. | + +## 7. Recommendation + +*Corrected by §8; the paragraphs below supersede the first version of this +section.* The shortest ordered path to TestFlight on the owner's iPhone is +**T1 → T2 → T3 (owner) → T4 → T5 (owner)**: about three working sessions of +agent work and forty minutes of the owner, with Phases 6, 7 and 9 and #137 +PR B and PR C all after the first submission. + +**The first item to build next is T1**, the request-time platform marker and +the native routing, because every later step depends on the app hiding its +purchase surface and its Google button by itself, and because it changes no +web behaviour, so it can ship to production safely before any shell exists. + +Then S1 → S4 → S3 → S2 → S6/S7 with S10 → S11 and S8 (owner) → S9 for the +first App Store submission, about six more sessions, nine in total. Android +follows the iOS submission (§5). + +## 8. Lessons from Scan & Action (read 2026-10-07) + +**Source and limits.** `tornidomaroc-web/scan-and-action`, public, read only +through the GitHub API on 2026-10-07: `WORK-QUEUE.md` (its only board), +`CLAUDE.md`, `.github/workflows/ios-testflight.yml` and `ios-audit.yml`, +`apps/frontend/capacitor.config.ts`, the `ios/` project (`Info.plist`, +`App.entitlements`, `PrivacyInfo.xcprivacy`, `ExportOptions.plist`, +`MainViewController.swift`), the social-login patch, the environment policy of +`testflight`, and its merged PRs since 2026-09-26. Nothing in that repository +was changed. **The most important limit: Scan & Action has never been +submitted to App Review.** Its board's last stage line is *"Submission, once +design steps 1 to 5 are done and every APPLE TRACK blocker is closed"*, and +its export options still carry `testFlightInternalTestingOnly`. So it proves +the pipeline, signing, processing, TestFlight and native sign-in on this team; +it proves nothing yet about what a reviewer decides on 4.2, 3.1.3(f) or +anything else. + +### 8.1 The path Scan & Action took, in order + +| When | PR | Step | Error, dead end or finding | Fix | +|---|---|---|---|---| +| 2026-09-26/27 | #258 | Google and Apple sign-in | Apple 4.8 makes Apple mandatory once Google is offered | `@capgo/capacitor-social-login`, native sheets, `signInWithIdToken`; Apple native-only, so no Services ID and no six-monthly secret | +| 2026-09-27 | #260 | iOS platform and TestFlight workflow | Capacitor 8 SPM template generated on Windows and committed | `macos-26`, API-key automatic signing, run on `main` and dispatch only | +| 2026-09-27 | #261 | Signing | Run 36355312731: *"Your team has no devices from which to generate a provisioning profile"*; #261 forced an Apple Distribution identity | **Wrong fix**: run 36355938894, *"conflicting provisioning settings"* | +| 2026-09-27/28 | #262 | Signing | (the revert) | Revert #261; register the owner's iPhone (2026-09-28); archive signs for development, export re-signs for the App Store; a test forbids the override | +| 2026-09-28 | #263 | Processing | Build 4 **rejected in processing, ITMS-90683**, missing `NSPhotoLibraryUsageDescription` | Purpose strings for every API a linked plugin can reach, localized; `PrivacyInfo.xcprivacy` (UserDefaults `CA92.1`); a test derives the keys from plugin sources. **Build 5 passed and installed on the owner's iPhone** | +| 2026-09-28 | #264 | Hardening | The Admin key sat on a runner that had run npm | Two jobs (`web` builds, `testflight` has no checkout and no npm), actions pinned to SHAs, `pipefail`; Swift packages resolved and scanned for plugins and macros before the key exists | +| 2026-09-29 | #268, #269 | Store metadata | No Support URL; legal pages named no operator | A public `/support` page; the individual operator named | +| 2026-09-29 | #270 | Binary | The social-login plugin links the Facebook SDK and AppTrackingTransparency even unused, which forced a tracking purpose string in an app that does not track | Facebook provider off in `capacitor.config.ts`, the import guarded by a patch, `ios-audit.yml` reads `otool -L` on every PR and every archive | +| 2026-09-29 | #271 | 5.1.1(i), 2.1(a) | No privacy link inside the native app (the landing, which held it, never renders natively); "Data coming soon" on a new account's first screen | A Legal panel in Settings with no price or plan; the placeholders rewritten | +| 2026-09-30 | #272, #274 | Account deletion | Apple's revocation page | Fresh Apple sheet at deletion, server exchange and revoke, nothing stored, proven on the owner's iPhone; then `main` went red on a time-bomb test nobody read, and a success that logged nothing | +| 2026-09-30 | #275 | Reproducibility | An upstream Swift package release could move the build | `Package.resolved` committed; strict resolution | +| 2026-09-30 | #276 | 4.2 | Its own board judged the app *"a B2B web admin panel on a phone"*, which is what 4.2 excludes | The system document scanner (VisionKit) as the native value | +| 2026-10-01/04 | #277 | Signing | Run 13: *"Your account has reached the maximum number of certificates"*: every hosted run creates a Development certificate, Apple caps the team at ten | A sweep that revokes API-made Development certificates older than six hours, team-wide | + +**Cost, inferred from PR dates (the repository records no sessions):** about +fourteen PRs over nine days, roughly ten working sessions, of which about four +were spent on dead ends KnowFlow can now skip (#261 and its revert, ITMS-90683, +the certificate cap, the Facebook SDK). + +**Still open there (its board, 2026-10-07):** the EU trader declaration, brand +verification on the Google consent screen, Private Email Relay registration, +Android Google sign-in (no device), the App Privacy labels, the measured first +scan, the submission build, and the design steps before submission. Its +"No support page" item is still unticked although #268 shipped `/support`. + +### 8.2 Each item, and what it means for KnowFlow + +| Item | Verdict for KnowFlow | How, or why not | +|---|---|---| +| Free `macos-26` build, API-key automatic signing | **Applies as is** | Same team, same public-repository terms. | +| Secrets kept from fork runs | **Applies as is** | An environment `testflight` limited to `main`; no `pull_request` trigger; two jobs so the key never meets npm. | +| Device registration for development signing | **Already done** | The owner's iPhone is registered on the team; the dead end of #261 is not repeated. | +| Ten-certificate cap and the sweep | **Applies with adaptation** | The cap is per team, so the two apps' runs **share it**. KnowFlow must carry the same six-hour rule, so neither pipeline revokes a certificate the other is using, and the two pipelines must never be given different rules. | +| Purpose strings, ITMS-90683 | **Applies with adaptation** | KnowFlow's plugins differ: social login, local notifications, document opening. Its file input accepts only documents (`DropZone.tsx:169`, `.pdf,.docx,.pptx,.xlsx,.txt,.md`), so no camera or photo key is expected; copy the test that derives keys from plugin sources rather than guessing. | +| Privacy manifest | **Applies as is** | The same plugin ships no manifest; the UserDefaults `CA92.1` declaration carries over. | +| Facebook SDK and AppTrackingTransparency | **Applies as is** | Same plugin, same switch, same patch, same `otool` audit. | +| 4.2 native value | **Applies with adaptation** | Scan & Action's answer is a document scanner, native to its purpose. KnowFlow's answer stays §3.1's: native sign-in, a local study reminder, "Open in KnowFlow". A scanner that photographs textbook pages would be stronger, but needs text recognition before ingestion (a photo has no text, register #128), so it is not proposed for the first submission. | +| 3.1.3(f) and the payment surface | **Applies with adaptation: the mechanism does not carry over** | Scan & Action gates on `Capacitor.isNativePlatform()` in the bundled client. **That cannot work in a shell over the live site:** KnowFlow's Upgrade links are rendered on the server, before any client code runs, so the server must know it is talking to the app. Replaced by T1's user-agent marker; Scan & Action itself already appends one on Android (`appendUserAgent: 'ScanActionAndroid'`). The proof becomes a request-path test, not a render of a client component. | +| In-app privacy link | **Already met, check in native** | KnowFlow's Settings links privacy and terms (`dashboard/settings/page.tsx`); T1 must not send `/privacy` or `/terms` to the app entry. | +| Placeholder text, 2.1(a) | **Checked, none found** | `grep` for "coming soon" and its Arabic forms in `src/`: nothing. | +| Sign in with Apple and Google | **Applies as is** | S1, corrected. One adaptation: the session cookie is set on `tryknowflow.com` inside the web view by the page itself, which a live-site shell supports directly. | +| Apple revocation at deletion | **Applies as is** | S2, corrected; the server half becomes a Next.js route on Vercel. | +| Private Email Relay | **Applies with adaptation** | Same rule, KnowFlow's own domain and sender (#119 (ii), #89). | +| Reviewer account | **Applies with adaptation** | Scan & Action keeps its reviewer account on Pro through a plan override with no subscription. KnowFlow has no override, and a hand-made `subscriptions` row without a Paddle id trips register #73's guard by design. KnowFlow's reviewer account is free, seeded with one subject and one material, and the review notes state the daily limits, so a reviewer is not surprised by a limit. | +| Screenshots | **Not evidenced** | Scan & Action has not produced its set yet; §2.2 S6 stands. | +| EU trader status | **Applies, and corrects this file** | Account-level; one declaration covers both apps (§3.6). | +| App Privacy labels | **Applies with adaptation** | Scan & Action's rule, "derive the processor list from the code at submission time", is right and adopted; the processors differ (Anthropic, Voyage AI, Supabase, Resend, Vercel, Railway). | +| TestFlight internal distribution | **Applies as is** | Builds join the internal group automatically on processing. | + +### 8.3 The architecture, re-judged + +**Kept: the shell over the live site, with one new fact weighed against it and +one new test.** + +**The new fact against it.** Capacitor's own configuration reference says of +`server.url`: *"This is intended for use with live-reload servers. This is not +intended for use in production"*, and says the same of `allowNavigation` +([Capacitor config](https://capacitorjs.com/docs/config)). That is the +framework disclaiming support, not Apple refusing anything. The concrete +failure it warns of is that the native bridge and plugins are designed for the +bundled origin. + +**What Scan & Action's experience says.** Nothing from App Review, because it +has not been reviewed. What it does show: (1) its bundled build cost nothing +extra **because its web app was already a client-rendered Vite app calling a +backend over CORS with tokens**; KnowFlow's is the opposite, server rendered +with cookie sessions (§1.1), so the same choice costs KnowFlow about six to +eight sessions; (2) its own judgment of 4.2 turned on native value, a +scanner, not on bundling, which is §1.1's reading; (3) its sign-in plugin +hands a token to the page, which works the same whether the page is bundled +or loaded from `tryknowflow.com`, provided the bridge reaches that page. + +**So the decisive question is the bridge on the remote origin, and the first +build answers it.** T2 copies Scan & Action's simulator launch check +(`MainViewController.swift` writes what the loaded page reports; +`ios-audit.yml` reads it) and asserts that `window.Capacitor` exists inside +`https://tryknowflow.com`. **If it does not, the architecture changes to the +bundled build before any S-item is built**, at the six-to-eight-session cost, +and this file is corrected. That makes the risk cost one session to discover, +not a rejection. + +**A risk the live-site shell adds, named.** Every page on `tryknowflow.com` +gets the native bridge. A cross-site-scripting flaw on the site would reach +the app's plugins. The plugin set is kept small (sign-in, notifications, +document opening; no file-system write, no contacts), and `allowNavigation` +is limited to `tryknowflow.com`. + +### 8.4 Reuse, and what must not be shared + +**Copy, with attribution in each file's header:** + +- `.github/workflows/ios-testflight.yml`: the two-job shape, the pre-key + package resolution and plugin-and-macro scan, the certificate sweep, the + archive-for-development and export-for-store pattern, the `otool` readings, + the key-removal step. **Adapt:** paths (`apps/frontend` becomes the + repository root), the plugin list in the artifact, the positive control + (`DocumentScannerPlugin` does not exist in KnowFlow), and the `push` trigger + limited to native paths, because a web change does not change a live-site + app. +- `.github/workflows/ios-audit.yml` and the launch check in + `MainViewController.swift`, adapted to read the remote page. +- `ios/ExportOptions.plist`, with KnowFlow's own values. +- `patches/@capgo+capacitor-social-login+8.5.11.patch` and the + `SocialLogin.providers` map, re-checked against the plugin version KnowFlow + installs. +- The purpose-string test and `PrivacyInfo.xcprivacy`. +- The design of the revocation service (#272) and its logging of every outcome + (#274). + +**Never shared:** + +- **The App Store Connect API key.** KnowFlow gets its own key in its own + repository's environment. A shared key means one leak exposes both apps and + one revocation breaks both pipelines. +- **The Sign in with Apple key**, the Google OAuth clients and their Google + Cloud project, the Supabase project and its keys. +- **The bundle id** (`com.scanaction.app` is Scan & Action's; KnowFlow's is + `com.knowflow.app`) and the App Store Connect app record. +- **The reviewer account.** +- The **Team ID** is the same team and is not a secret; it is the one value + both projects legitimately carry. + +### 8.5 Live risks found in Scan & Action: reported, not fixed + +Recorded for the owner. Nothing was changed in that repository. + +1. **The signing job can execute Xcode project content produced by the job + that ran npm.** `web` runs `npm ci` (and so every package's install script, + including `patch-package`) and uploads `apps/frontend/ios` as an artifact; + `testflight` downloads it, places the Admin `.p8` in `$RUNNER_TEMP/asc`, + and runs `xcodebuild archive`. It checks that certain files exist and that + no package declares a SwiftPM plugin or macro, but **it does not check that + `App.xcodeproj/project.pbxproj` matches the commit**. A compromised npm + dependency could add a Run Script build phase to that file in the artifact, + and Xcode would run it on the runner that holds the key. The key has the + **Admin** role on the team that also holds KnowFlow. **Suggested fix there:** + in `testflight`, compare every committed file under `ios/` (all but the + synced web bundle, `capacitor.config.json` and the CLI-written + `CapApp-SPM/Package.swift`) with the commit, read through the API without + npm, and fail on any difference; and give the key the narrowest role that + still creates certificates. KnowFlow's copy of the workflow carries this + check from its first version. +2. **An Admin key on a team with two apps.** Any leak of Scan & Action's key + is a leak for KnowFlow too. Separate keys (§8.4) limit the blast radius of + the next one, not of this one. +3. **The board says "No support page" is open while `/support` shipped in + #268.** Not a security risk; a record that will mislead the submission + checklist.