Date: 2026-09-04
Outcome: security remediation and the full per-user legacy Soundboard action
migration are implemented and locally verified. The approved existing per-user
Supabase tables are authoritative for categories and clips in every storage
adapter mode. Historical shared data has not been reassigned or imported.
Deployment, physical-device proof, recording provenance, and residual hardening
remain outstanding. This is not an all-clear security certification.
Base: d88346208a855294f85050b7105896b89b2f9a18, including the existing
uncommitted web transcription and iOS Local Qwen prototypes.
This work follows the August 31 web/iOS audit. Native Daybreak contributors
implement the web and iOS changes; the primary Codex agent reviews and verifies
integration. Changes require verification before any deployment claim.
Recording provenance and packaging
Four voice-sample M4A files remain tracked under
web/python-web-app/static/uploads/. Existing repository documentation calls
them user-supplied voice sample fixtures. Their identity, consent, and intended
retention have not been established by this audit. File names and durations
alone do not establish a privacy breach, and the previously tested deployment
paths returned 404.
The Docker build already excludes uploads, speech, and cache directories.
Git ignores now cover all three runtime directories, including metadata and
non-MP3 formats. The dev deployment rsync excludes those directories and the
last-input text file; these exclusions also preserve destination-only runtime
data during deployment. Neither ignoring nor excluding a file removes it from
Git history or existing clones.
Provenance review and any coordinated history removal remain outstanding.
Do not copy the recordings into documentation or replace them with newly
collected personal audio. Tests should use generated synthetic content.
Verification and rollout
Implemented controls
- Generated TTS/STS audio is allocated under
speech/<verified-user>/<uuid>
with server-owned sidecar identity. /api/generated-media and the compatibility
download route require a matching bearer identity and return private/no-store
responses. Anonymous, wrong-owner, and ownerless artifact requests fail closed.
Save-clip enforces the same ownership before reading/copying local audio.
- Regeneration preview, commit, and discard are owner-bound. A stale session ID
no longer falls back to another latest session. Generated media is fetched
into browser Blobs; tokens are never placed in media URLs or attached to
cross-origin media requests.
- Legacy clip reads, source lookup, reorder/delete/copy, conversion, and combined
media use
users/<verified-user>/soundboard/<category-uuid>/ for new objects on Supabase
and GCS. Background jobs capture owner identity before starting, and polling
rejects other users and ownerless jobs. Category-only shared caches are not
used for private reads. Existing paths remain unchanged and are resolved from
owned database rows; moves do not reconstruct paths from the new category.
- Category rename/reparent, clip moves, order, delete, and trash/restore use the
same per-user database index in every rollout mode. Combined artifacts are
indexed clip rows too; storage scans no longer resurrect trashed or unindexed
objects. Duplicate-filename moves/publishes and foreign storage-path upserts
fail closed. New category UUID namespaces avoid same-leaf-folder collisions.
- Conversion publishes a new row and retires its source in one database
transaction when requested. Regeneration stages a new version and atomically
swaps the database reference only if the expected old version is still current.
Upload/persistence failures clean up new outputs, never the previous version.
Old successful versions remain retained; automatic version garbage collection
is not included. Copy/combine failure cleanup preserves original sources.
- Storage writes use invocation-unique object paths, with the database publish
as the commit boundary. A later URL-signing failure must not roll back saved
media; clients can refresh the category to obtain another short-lived URL.
- Storage adapters reject ambiguous/traversing object keys before transport;
accepted Supabase keys are percent-encoded, preserving spaces and Unicode.
Strict database-backed clip listing and resolution reject foreign persisted
media paths before privileged signing. Supabase copy no longer requires GCS.
- The actual Flask
static endpoint is guarded, including normalized aliases
into uploads, speech, cache, and legacy output/text files. The ownerless shared
cache and legacy combined-download endpoint fail closed; clients use storage
fallback and authenticated media paths. Shipped canned Soundboard assets
remain intentionally public; they are not treated as private generated media.
- Web untrusted toast and error messages use text nodes rather than HTML
interpolation. Toast types are constrained to a fixed set.
- Flask responses gain CSP, framing protection, HSTS,
nosniff, referrer, and
permissions headers. The CSP removes unsafe-eval but still permits inline
scripts because the legacy UI depends on them. This is partial defense in
depth, not a nonce-based CSP or a replacement for fixing injection sinks.
- Hosted transcription trials require an explicit unchecked permission control
and a matching provider/version at the server boundary before provider
dispatch. Privacy wording now distinguishes OpenRouter, Hugging Face,
ElevenLabs, and on-device processing.
- Transcription temporary files use exclusive random allocation with cleanup
covering upload-save failures. Dependency pins move to
requests==2.33.0
and python-dotenv==1.2.2; the original audit did not demonstrate reachable
use of the affected helper paths.
- iOS fallback keys use device-only, when-unlocked Keychain storage. Migration
removes the legacy value only after secure persistence succeeds; failures
remain visible and preserve the old value to avoid silent credential loss.
- iOS TTS/STS input and raw ElevenLabs payload logging are removed. Local voice
profile audio and metadata use Complete file protection, backup exclusion,
protected atomic metadata writes, and deletion/orphan cleanup. Saving requires
authorization confirmation. Profiles remain installation-wide after sign-out;
account separation is not claimed.
- iOS generated soundboard filenames no longer duplicate an existing extension.
The version remains 1.3, with build incremented to 29.
Validation evidence
- Complete web suite independently rerun by the primary agent after final
integration: 1045 passed, zero failed. The pre-existing language-dropdown
assertion was updated to match the actual TTS-capable-model filter, with an
independent execution of the real JavaScript function confirming that STS-only
models are excluded. No settings production behavior was changed for that fix.
- Four relevant JavaScript runtime checks passed: Soundboard category management,
category surface, TTS-to-Soundboard workflow, and regeneration panel. Retired
shared-cache unit assertions were replaced with checks that private listing
always uses the database and never falls back to shared order/cache on failure.
- Publish/rollback tests include partial audio/text/metadata uploads, thrown
database failures, and failed signers after successful publication. The parent
identified early-return cleanup gaps during integration and added final guards
for save and copy, preserving source objects and committed replacement media.
- Namespace/provider regression tests exercise same-user list/delete/copy,
two-user same-filename isolation, strict foreign database paths, malformed
path rejection before transport, and background-job ownership. The final
reviewer found strict-list signing and Supabase-copy gaps; implementation
was corrected and focused tests rerun. Parent review additionally corrected
missing GCS resolver-prefix propagation and unsafe copy-failure cleanup.
- Focused security tests: 32 passed, including read ownership, copy ownership
with no upload on denial, regeneration isolation, disabled direct downloads,
static aliases, and six provider-consent rejection cases with no provider call.
- Primary-agent focused Python run: 49 tests passed across speech-service,
page-bootstrap, and security contract tests.
- Primary-agent execution of the actual toast function with two HTML payloads
and an ordinary control confirmed literal text nodes and constrained types.
- Daybreak iOS full
SpeakTrueTests unit target: 163 passed, zero failed.
Primary-agent independent rerun: 15 passed, covering six isolated
credential-transition tests, local-profile lifecycle, and Soundboard cache
tests. The Keychain tests inject an adapter and isolated defaults rather than
touching real credentials. Backup exclusion is checked on Simulator; Complete
file protection still requires physical-device validation.
- Synthetic rendered web check:
http://127.0.0.1:5187/, installed headless
Chrome, 1440×1000 viewport, repository template and scripts with synthetic
API responses and all external requests intercepted. Ordinary clicks verified
Speech to Text selection, initially unchecked consent, and clearing consent
on provider/model change. No JavaScript page errors. This checks UI mechanics,
not real sign-in, microphone capture, provider dispatch, or staging playback.
The frontend-testing skill’s rendered check exposed an external-logo CSP
incompatibility, addressed by using the bundled icon. Empty-model warnings in
the harness are expected from its synthetic catalog.
- Migration SQL integration: an isolated PostgreSQL 14 instance used synthetic
users, actual Soundboard table definitions, and selected existing migrations.
Checks passed for create/UUID lookup, cross-user denial, clip and category
moves with stable media paths, normal/combined trash and restore, duplicate
filename rejection, foreign-owner storage-path conflicts, atomic conversion,
and regeneration compare-and-swap (including stale-writer rejection). Storage
signing was mocked and any object listing failed the test. This is real SQL
execution, not full Supabase/RLS, deployed storage, or deployment evidence.
- Migration UI: the actual repository template/scripts in headless Chrome at
1440×1000 and 390×844 rendered synthetic account-owned categories with strict
mode disabled. The move dialog offered the other account category and omitted
the source UUID. Selection enabled confirmation; cancellation avoided a write.
No JavaScript page errors or layout overflow were observed. Empty-model and
fixture-404 console warnings were expected. Screenshots remain in
/private/tmp/speaktrue-migration-destinations-desktop.png and
/private/tmp/speaktrue-migration-categories-mobile.png.
Reproducible commands:
web/python-web-app/venv/bin/python -m pytest -q -o addopts='' web/python-web-app/tests
web/python-web-app/venv/bin/python -m pip check
xcodebuild -project ios/SpeakTrue.xcodeproj -scheme SpeakTrue \
-destination 'platform=iOS Simulator,id=3DD20D4B-A7FB-4FF7-9D16-2D89A688DAF9' \
-derivedDataPath /private/tmp/speaktrue-security-audit-derived \
-clonedSourcePackagesDirPath /private/tmp/speaktrue-security-audit-sourcepackages \
-skipPackagePluginValidation CODE_SIGNING_ALLOWED=NO test \
-only-testing:SpeakTrueTests
git diff --check
The simulator identifier is environment-specific. The primary Xcode rerun first
failed on sandbox cache access, then passed with approved Xcode/simulator access.
The local Python test environment now contains both updated dependency pins;
pip check reports no broken requirements.
Remaining boundaries and rollout prerequisites
- Database authority is approved for legacy Soundboard management. Category
and clip lifecycle use existing per-user database rows on either storage
adapter. New writes are indexed; existing media references stay stable across
category/clip moves. Database connectivity and the existing Soundboard schema
are required. Missing database state must fail closed, not fall back to global
categories or unindexed object listings. Local validation passed; this does
not remove the application’s existing Supabase-only startup guard or establish
a deployed GCS rollback configuration.
- Restore also needs the forward migration
20260904120000_fix_restore_clip_sort_order.sql: real PostgreSQL testing
reproduced an ambiguous sort_order reference in the existing restore
function. The migration qualifies the column without changing its grants,
ownership checks, retention window, or result contract. It is not deployed.
- The user chose migration, not disabling legacy actions. No shared object or
global category was moved, deleted, or assigned to an account. Historical
data migration requires a separately established ownership mapping.
- Do not deploy a reverse-proxy static alias that bypasses Flask for runtime
media. Recheck staging’s real URL map and headers after an approved deployment.
- Ownerless pre-change artifacts must be regenerated, not automatically assigned
to the next user. Shared legacy cache/combined-download URLs now fail closed.
- Successful server-generated audio has no automatic TTL cleanup yet. Access
isolation is implemented; indefinite retention is not resolved by this patch.
- Browser sessions still use localStorage. CSP retains inline-script allowance.
- Local profiles remain installation-wide, not per-account. Complete protection
and lock-state behavior require a physical-device test; simulator tests are
not physical-device evidence. Keychain migration failure deliberately retains
the old plaintext value and reports an error rather than losing the key.
- The four tracked recordings require owner/consent/retention clarification.
No original recording or Git history was removed.
- No new staging magic link was needed: these changes are not deployed, and
this turn’s UI/API tests used synthetic identities/content. Earlier audit
staging evidence does not verify the new patch.
- Private database Soundboard management rejects the unauthenticated dev-smoke
identity. Other local-development smoke behavior remains configuration-gated;
never enable it against real private accounts/storage or in staging/production.
Change accounting
Changes remain uncommitted on main at the stated base. No push or deployment.
Pre-existing user changes to prototype/provider work, package resolution,
guides, demo artifacts, and research were preserved.
- Packaging/docs:
.gitignore, dev-deploy rsync workflow, docs/index.md,
docs/privacy-policy.md, web-only examples in
docs/contracts/speech-workflow-contract.md, web README, and this report.
- Backend: the additive restore-function SQL correction and its focused source
contract test; no hosted migration was applied. Before a future commit/push,
run the repository’s full Android CI-equivalent gate for this shared backend
contract change. No Android source was modified here.
- iOS:
UserSettings, SettingsTabView, TTSViewModel, STSViewModel,
SpeechToTextView, both ElevenLabs services, SoundboardCacheManager,
SettingsDiffTests, build number in the Xcode project, and security/lifecycle
edits to the existing untracked LocalVoiceProfileStore, LocalQwenViewModel,
LocalQwenPrototypeView, and LocalQwenPrototypeTests files.
- Web:
requirements.txt; runtime/bootstrap, legacy/soundboard/STT/TTS routes;
page, speech, STT/TTS, regeneration, and Soundboard route services; five
index_* JavaScript files; index and STT/STS templates; focused API/unit
regression tests and authenticated contract-fixture updates.
- New task-created untracked files: this report,
web/python-web-app/tests/test_security_remediation_contracts.py,
web/python-web-app/src/services/soundboard_namespace_service.py,
web/python-web-app/tests/unit/test_soundboard_namespace_service.py,
backend/supabase/migrations/20260904120000_fix_restore_clip_sort_order.sql, and
web/python-web-app/tests/unit/test_restore_clip_migration.py.
- External Obsidian status, planning, work log, and generated map are updated
separately at closeout. Test logs and synthetic browser evidence remain under
/private/tmp, not in the repository or vault.