Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.beaux.ultraedge"
"bundleIdentifier": "com.beaux.ultraedge",
"privacyManifests": {
"NSPrivacyTracking": false,
"NSPrivacyCollectedDataTypes": []
}
},
"android": {
"adaptiveIcon": {
Expand Down
49 changes: 49 additions & 0 deletions docs/release/COLLABORATION-SAFETY-OPERATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Collaboration safeguards — implementation and release gates

Status: source implementation only, September 6, 2026. No migration deployed, no live identities changed, no operational moderation response verified. **An implemented queue is not a monitored human response process. Do not submit on that assumption.**

Confirmed by explicit user instruction: the legal operator is **Beaux Walton**. The approved private privacy/support/abuse email is **hello@heybeaux.dev**. The app provides a direct `mailto:` link with the same address shown if opening email fails. Mailbox deliverability, monitoring and operational response have not been tested; no email was sent. Operator identity and contact approval are settled, not outstanding confirmation gates.

## App and backend contract

`ue_live(action,args)` retains existing actions. Migration `supabase/collaboration/002_safety.sql` copies the original implementation to a client-inaccessible `ue_live_core` and adds a checked wrapper. No client table grants or permissive RLS policies are added. Existing optimistic timing revisions, request deduplication and owner permissions remain in the core.

- `abuse_report`: `{roomId,userId,reason,details,requestId}` returns `{id}`. Both guests must currently belong to the room; self-reporting is rejected. Reasons are `harassment`, `hate`, `sexual_content`, `threats`, `other`; details are optional, max 1,000 characters. Request IDs deduplicate retries; ten new reports per reporting guest per rolling 24 hours. Failed submissions are not queued offline and UI never announces success without a receipt.
- The private `ue_live_abuse_reports` table stores reporter/subject guest IDs, room reference, reason/details, timestamps/status, and a bounded evidence snapshot: event name, subject display name, subject timing observations and, only for reports about the owner, the shared plan. No invitation code or session token is copied. Clients cannot inspect reports, review them or change their status. The app includes neither moderator tooling nor notifications. Only an authorized backend operator can access the queue.
- Evidence format version 1 preserves room/subject IDs and plan revision even if the live room is later removed. For owner reports the full plan is retained only if its serialized JSON is at most 262,144 bytes; otherwise `plan` is null and `planExcerpt` contains the first 16,000 characters of serialized plan text (an excerpt, **not valid complete JSON**). Observations are a deterministic latest-first sample (received time descending, checkpoint ID/kind tie-break), capped at 100 rows and 131,072 serialized bytes. `truncation` records original plan bytes, limits, total/included observation counts and separate plan/observation truncation flags. JSON escaping/UTF-8 overhead is bounded too; a large valid room cannot make reporting fail the 600,000-byte evidence constraint. Reason/details are never shortened by this sampling. Ordinary validation, membership and report quotas still apply.
- Reviewers must inspect truncation metadata: a sample can omit the offending content. Use the preserved room/subject IDs to inspect current authorized server content where it still exists, and seek clarification through the verified private support process if needed. An excerpt is not proof that omitted content was safe, nor a complete historical record. No additional data collection or retention guarantee is implied.
- `block_user`: `{roomId,userId}` returns `{removedRoomIds}`. Blocker leaves every common room they do not own. Where blocker owns the room, target is removed and invite rotates. Client purges departed-room cache and pending observations after server acknowledgement; retained rooms hide target observations. Server reads hide historical observations between blocked guests; server joins reject either guest entering a room containing the other. Blocks are across rooms, not merely a UI mute.
- Block actions, joins, reporting and deletion use a shared transaction advisory lock to prevent interleaved join/block membership checks and reporting quota races. This intentionally serializes a bounded private-team workload; load-test before larger use. Existing room locks and owner checks remain. Do not bypass the public wrapper with new privileged endpoints.
- Blocks last until either identity is deleted. There is no unblock UI in this iteration. Guest identities are not verified people: another identity can evade a block. Existing cached/exported copies on other devices cannot be recalled; offline devices cannot immediately receive access changes. Owner removals without blocking retain the original historical-report semantics.

## Filtering is limited

Server triggers reject new/updated display names and all string values in shared snapshots matching configured whole phrases after Unicode NFKC/case/punctuation normalization. `ue_live_content_terms` has just four starter entries: two English profanities and two explicit threat/self-harm harassment phrases. These are **limited baseline filtering, not full hate, sexual-content, threat, multilingual or semantic moderation**. Spelling obfuscation and other abusive language can evade it; innocent context can be rejected. In-app reporting and blocking are additional controls, not proof of comprehensive moderation.

No retroactive scan occurs when a term changes. Before release, operators must review existing names/plans against the current rules, handle matches, curate broader proportionate terms (normalized lowercase plain words/phrases), and validate false-positive cases. Provide a private way to contest an incorrect filter. Do not edit the original migration to adjust a deployed term list; use an independently reviewed follow-up change.

## Deletion and retention

“Delete my collaboration data” is visible at the bottom of the live team panel, including inside an open room and when no room/actor is cached. Events → Join / open a live team race opens the panel; Settings → Privacy & Support explains the route. No guest is created solely to delete it.

- A rejected/unacknowledged server deletion preserves rooms and pending observations so the user can retry. Deletion is idempotent for a still-valid guest token even after its user row is gone, allowing retry after a lost response. Expired/lost credentials remain a recovery limitation requiring the operator's verified private support process.
- Once the server acknowledges deletion, local collaboration cache is cleared even if local sign-out throws or returns an error. UI distinguishes completed server deletion from local session/cache cleanup problems. Local race plans are untouched.
- Deleting either guest cascades associated blocks and abuse reports/evidence. This intentionally prioritizes the current guest deletion contract; it is **not** a policy of retaining all abuse evidence for investigations. Closing a room instead nulls the queue's room reference and retains evidence for review.
- There is no automatic queue aging/purge job. Before launch, the operator must choose and publish an evidence-retention limit, implement a reviewed purge schedule, determine appropriate deletion/legal handling, and validate it. Do not promise a duration yet. Provider auth logs, external logs, backups and previously downloaded copies are separate; this SQL does not erase those.

## Migration / release procedure — requires approval

1. Confirm the actual deployed `ue_live` body and schema match `001_live_rooms.sql`. Compare any production changes before replacing the RPC; the core is a copy, not an automatic merge. Take the operator's approved backup and review grants/definer ownership. Use a privileged database owner; no service key belongs in app configuration.
2. On an isolated staging database, apply `001_live_rooms.sql` only if not already installed, then apply `002_safety.sql` once. The latter is a one-time transactional migration, not rerunnable. **Never rerun 001 over 002**: it would replace the safety wrapper. Do not run either against production without explicit approval.
3. Run `bash scripts/test-live-safety-local.sh` for a disposable local PostgreSQL cluster, plus dedicated Jest tests. It binds only a temporary Unix socket, not a TCP listener; stops the cluster on exit and prints retained logs. It never uses project Supabase credentials. This validates real PostgreSQL behavior with minimal auth functions, not real Supabase JWT/network/device behavior.
4. Verify staging permissions explicitly: `anon` cannot execute `ue_live`; `authenticated` can execute only the wrapper, cannot execute core/filter functions and cannot access safety tables directly; RLS remains enabled. Test deployed roles/default grants as well as those mocked locally.
5. Two test devices: create/join, offensive name/plan rejection, submit report then block, target-owner / blocker-owner / third-party-owned common rooms, rotated invitations, blocked rejoin both directions, historical observations hidden, stale/offline state, observation conflicts, revoke/leave/close, and guest deletion including failure/retry. Use disposable identities with approval, never real team identities. Test the exact final binary.
6. Deploy only in an approved maintenance/release window. Old apps can continue timing operations but lack reporting/block UI; server filtering/block enforcement still applies. Do not ship the new UI against a backend without migration: it will reject the new actions. Preserve the safety wrapper during rollback; prefer a reviewed forward fix instead of reopening unsafe direct access.

## Operational gates — still open

- Beaux Walton is the confirmed legal operator. Assign trained queue reviewers; verify restricted access, duty coverage, monitoring/alerts and escalation for threats or child-safety issues. The approved contact is hello@heybeaux.dev; mailbox deliverability and human response still need operational validation. Do not invent an emergency service or response time.
- Assign someone to check `pending` reports, examine evidence with least-privilege access, record a decision using `reviewing` then `resolved`/`dismissed` and `reviewed_at`, and take proportionate action through an approved procedure (content correction/removal, invitation revocation, room closure, account measures where justified). Status alone does not remove content. Add secure tooling/audit records as needed; none is supplied by this migration.
- Establish response targets, appeals handling through hello@heybeaux.dev, identity-evasion handling, retention/purge and escalation procedures. Validate receipt-to-human-triage and action end to end. The UI's receipt explicitly does not promise human review. No retention duration or SLA is approved by the contact confirmation.
- Publish the confirmed Beaux Walton / hello@heybeaux.dev operator/contact information at the approved destinations and finalize provider/retention facts. The app targets `https://ultraedge.heybeaux.dev/privacy` and `/support`; these are intended destinations, not proof of deployment. Independently validate mailbox operation and monitoring before claiming a staffed support response.
- Have the owner assess adequacy under [Apple App Review Guideline 1.2](https://developer.apple.com/app-store/review/guidelines/#user-generated-content). Invite-only sharing is not treated as exempt. **Safeguard implementation alone does not close App Review risk.**
9 changes: 9 additions & 0 deletions docs/release/LOCAL-APP-STORE-RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Free local-only App Store candidate

September 6, 2026: Beaux authorized App Review submission and previously specified that this first release is free and local-only. Cloud backup, online sharing and in-app messaging are reserved for a future monetised release.

This candidate includes the reviewed attachment lifecycle fixes and hosted privacy/support links. Both live-room UI entry points are removed. Online collaboration implementation remains dormant for future development; no collaboration backend migration is deployed for this release. App collection manifests are empty and tracking is disabled. App Privacy, screenshots and review metadata must match this candidate before submission.

Validation: 36 Jest suites / 215 tests pass (3 TODO), TypeScript passes. Use Jest with --watchman=false on this host. For signed archives use EXPO_NO_DOTENV=1 and ULTRAEDGE_DISABLE_WATCHMAN=1. The installed release bundle is in the source repository vendor/bundle for Ruby 3.4.0; select Homebrew Ruby and the absolute BUNDLE_PATH when building from this worktree. Do not print or commit fastlane/.env.

Website updated through heybeaux/heybeaux-web PR 19, merged through the configured GitHub deployment integration. Store privacy/support URLs, local-only description/review instructions and age-rating changes are saved. Submission and processed-build verification are tracked separately under workspace reports/UltraEdge-distribution.
33 changes: 33 additions & 0 deletions docs/release/PRIVACY-DATA-MAPPING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Privacy declaration evidence — September 6, 2026

Source changes, not approved/published App Store Connect answers. Native `ios/UltraEdge/PrivacyInfo.xcprivacy` and Expo `app.json` declare matching categories; required-reason API entries in the native manifest are unchanged. Rebuild and review the aggregate privacy report before release. Do not reuse build 8's empty collection declaration.

The user explicitly confirms **Beaux Walton** as legal operator and **hello@heybeaux.dev** as the approved private privacy/support/abuse email. These facts supersede earlier drafts awaiting operator/contact confirmation. Mailbox deliverability and operational response are untested; no email was sent. No phone number, address, copyright approval, retention duration or SLA is inferred from this confirmation.

## Declared collection

All entries are linked to the guest identity, not used for tracking, and have `NSPrivacyCollectedDataTypePurposeAppFunctionality` as their purpose.

| Apple category / manifest suffix | Evidence and rationale |
| --- | --- |
| Name / `Name` | Team display names; optional real names can be supplied, including names in logistics labels. |
| User ID / `UserID` | Supabase guest UUID in memberships, observations, blocks and reports. Pseudonymous is not automatically unlinked. |
| Fitness / `Fitness` | Shared race distance/start/target timing and checkpoint exercise observations. This includes exercise-related data without HealthKit; not a declaration of local-only weight collection. |
| Other User Content / `OtherUserContent` | Event/checkpoint names, stops/logistics, and retained relevant plan evidence. Client `makeLiveSnapshot` uses an explicit upload allowlist. |
| Customer Support / `CustomerSupport` | In-app abuse report reason/details and associated evidence stored in the private queue. Declared rather than relying on the optional-feedback exemption. |
| Other Data Types / `OtherDataTypes` | Connection/security metadata associated with authentication. Supabase documents retained auth event logs including user ID, IP and user-agent. This is a conservative mapping for documented provider behavior; operator must verify actual project/log settings and purposes, and revise this category if the confirmed use requires a more specific Apple category. No geographic inference from IP is implemented in the app. |

`src/lib/liveRaceSync.ts`, `src/lib/liveRaceModel.ts`, and `supabase/collaboration/001_live_rooms.sql` / `002_safety.sql` are the implementation evidence. Data transmitted only to fulfill a request in real time is distinct from retained collection, but rooms, guest identity and reports persist. Ongoing optional collaboration is not treated as exempt optional feedback. These interpretations follow [Apple's App privacy details](https://developer.apple.com/app-store/app-privacy-details/) and [collection manifest guidance](https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests).

## Not inferred as collected by this feature

Crew email/phone fields, personal notes, GPX files, gear photos and PDF guides are excluded from the collaboration allowlist; local profile/weight entries are not uploaded through it. User-selected exports and third-party map/file services need separate assessment, not an automatic blanket “no collection” claim. Free-form text can contain private data; users should avoid entering it in shared names, labels or reports. The app does not promise an automated detector will strip sensitive text.

## Owner verification required

- [Supabase Auth Audit Logs](https://supabase.com/docs/guides/auth/audit-logs) documents default authentication event logging and example IP/user-agent fields. Confirm the actual project configuration, region, externally retained logs, diagnostic/security uses, retention and backups before final policy and App Privacy answers. No exact duration or guaranteed Canada-only processing is asserted here; the in-app unverified region claim was removed.
- Confirm network behavior and integrated SDK manifests on the release candidate. Review any additional diagnostics/usage data or provider IP-derived location behavior; do not hide it under Other Data Types if Apple's more specific categories apply.
- Queue evidence survives room closure but cascades if either related guest identity is deleted. No automatic retention purge is implemented; see `COLLABORATION-SAFETY-OPERATIONS.md`. Publish only confirmed retention practices.
- Evidence is bounded and may be partial: owner plans above 262,144 serialized bytes become a 16,000-character serialized-text excerpt; subject observations are capped at 100 rows / 131,072 bytes. Stable room/subject IDs, plan revision, reason/details and truncation counts/flags remain. Do not describe reports as storing a complete copy of every shared plan or observation history.
- Publish and externally verify the intended privacy/support links with the confirmed Beaux Walton / hello@heybeaux.dev operator/contact details, validate mailbox delivery/monitoring separately, and update App Store Connect separately. Source URLs are not proof that policy/support pages are live. Identity/contact approval does not establish operational response.
- In Xcode Organizer, generate the new archive's Privacy Report; inspect the app and SDK aggregate against actual traffic and these disclosures. Validate required-reason codes independently. Review submitted labels with the responsible operator, not as a substitute for their approval.
4 changes: 2 additions & 2 deletions ios/UltraEdge.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@
CODE_SIGN_ENTITLEMENTS = UltraEdge/UltraEdge.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = T348Z33H7K;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
Expand Down Expand Up @@ -437,7 +437,7 @@
CODE_SIGN_ENTITLEMENTS = UltraEdge/UltraEdge.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = T348Z33H7K;
INFOPLIST_FILE = UltraEdge/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
Expand Down
2 changes: 1 addition & 1 deletion ios/UltraEdge/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>8</string>
<string>9</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
Expand Down
Loading
Loading