Skip to content

feat: add verifiable usage proof for dictation - #18

Open
fireharp wants to merge 3 commits into
mainfrom
feat/proof-of-use
Open

feat: add verifiable usage proof for dictation#18
fireharp wants to merge 3 commits into
mainfrom
feat/proof-of-use

Conversation

@fireharp

@fireharp fireharp commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds an isolated MumbliApp/ProofOfUse/ module that signs anonymous usage receipts on each successful dictation (no transcript text, feature-flagged off by default)
  • Settings toggle + menu bar badge for local receipt count and verification guide link
  • Enrollment and publish scripts (scripts/proof/enroll-install.sh, scripts/proof/publish-proof.sh)
  • Public trust anchors at docs/proof/trusted-keys.json and verification docs at docs/proof/README.md

Test plan

  • Build Mumbli from branch (xcodebuild -scheme MumbliApp build)
  • Enable Settings → Usage Proof → Sign dictation usage receipts
  • Run ./scripts/proof/enroll-install.sh with install public key from Settings
  • Complete one dictation and confirm ~/Library/Application Support/Mumbli/proof/receipts.jsonl grows
  • Run ./scripts/proof/publish-proof.sh and verify with pou verify-public + pou verify-audit
  • Confirm module is removable by deleting ProofOfUse/ and the single AppDelegate hook

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added an optional “Proof of Use” feature for signing and collecting usage receipts.
    • Added a new settings section to enable the feature, view status, copy the install key, and open related folders/guides.
    • Added menu bar indicators and a verification link when the feature is available.
  • Documentation

    • Published setup, enrollment, verification, and removal guidance for the proof workflow.
  • Chores

    • Updated ignored files and added support scripts for proof enrollment and publishing.

Adds an isolated ProofOfUse module that signs anonymous usage receipts on
successful dictation, with settings UI, menu bar badge, enrollment/publish
scripts, and public verification docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fireharp, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b03b6e5d-ba91-45cc-b437-ad153cb062e6

📥 Commits

Reviewing files that changed from the base of the PR and between aaf8381 and 67068f7.

📒 Files selected for processing (22)
  • MumbliApp.xcodeproj/project.pbxproj
  • MumbliApp/AppDelegate.swift
  • MumbliApp/ProofOfUse/AppAttestation.swift
  • MumbliApp/ProofOfUse/AutoEnrollment.swift
  • MumbliApp/ProofOfUse/CanonicalJSON.swift
  • MumbliApp/ProofOfUse/PouModels.swift
  • MumbliApp/ProofOfUse/ProjectGrant.swift
  • MumbliApp/ProofOfUse/ProofOfUseBadge.swift
  • MumbliApp/ProofOfUse/ProofOfUseConfig.swift
  • MumbliApp/ProofOfUse/ProofOfUseFacade.swift
  • MumbliApp/ProofOfUse/ProofOfUseSettingsSection.swift
  • MumbliApp/ProofOfUse/UsageReceiptSigner.swift
  • MumbliApp/ProofOfUse/project-grant.json
  • MumbliAppUITests/ProofOfUseUITests.swift
  • docs/proof/README.md
  • scripts/proof/embed-grant.sh
  • scripts/proof/enroll-install.sh
  • scripts/proof/publish-proof.sh
  • scripts/proof/show-install-key.swift
  • scripts/proof/test-canonical.swift
  • scripts/proof/test-proof-e2e.sh
  • scripts/proof/watch-receipts.sh
📝 Walkthrough

Walkthrough

This PR adds an optional Proof-of-Use module to MumbliApp, providing cryptographic install identity, canonical JSON encoding, dictation receipt signing/storage, config-driven enablement, menu bar/settings UI integration, publishing/enrollment scripts, trusted-keys configuration, and documentation.

Changes

Proof-of-Use feature

Layer / File(s) Summary
Data models, encoding, and identity primitives
MumbliApp/ProofOfUse/PouModels.swift, Base64URL.swift, CanonicalJSON.swift, InstallIdentity.swift
Adds Codable wire models, URL-safe Base64 helper, a deterministic canonical JSON encoder, and a Keychain-backed Ed25519 install identity with signing support.
Configuration and enablement
MumbliApp/ProofOfUse/ProofOfUseConfig.swift
Adds static identifiers, a launch-flag/UserDefaults-backed enablement toggle, and computed URLs for proof storage files.
Receipt signing and append-only storage
MumbliApp/ProofOfUse/UsageReceiptSigner.swift, UsageReceiptStore.swift
Builds/signs dictation receipts (nullifier, event body, commitment) and appends them to a JSONL store with a serial queue.
Facade entry point and app wiring
MumbliApp/ProofOfUse/ProofOfUseFacade.swift, MumbliApp/AppDelegate.swift
Exposes a singleton facade for recording dictation events and status queries; wires --enable-proof-of-use launch flag and dictation completion recording into AppDelegate.
Menu bar and settings UI integration
MumbliApp/ProofOfUse/ProofOfUseBadge.swift, ProofOfUseSettingsSection.swift, MumbliApp/UI/MenuBarController.swift, SettingsView.swift
Adds badge/link SwiftUI views and a settings section, wired into the menu bar dropdown and settings screen.
Xcode project wiring
MumbliApp.xcodeproj/project.pbxproj
Registers new ProofOfUse Swift files as build files, file references, a group, and sources build phase entries.
Publishing scripts, trusted keys, and documentation
scripts/proof/enroll-install.sh, publish-proof.sh, docs/proof/README.md, trusted-keys.json, .gitignore
Adds credential enrollment and proof publishing scripts, trusted key material, usage documentation, and staging-artifact ignore rules.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
    participant AppDelegate
    participant ProofOfUseFacade
    participant UsageReceiptSigner
    participant InstallIdentity
    participant UsageReceiptStore

    AppDelegate->>ProofOfUseFacade: recordDictation(event)
    ProofOfUseFacade->>ProofOfUseFacade: check isEnabled
    ProofOfUseFacade->>UsageReceiptSigner: signDictation(event, publicKey, credential)
    UsageReceiptSigner->>InstallIdentity: sign(canonicalBytes)
    InstallIdentity-->>UsageReceiptSigner: signature
    UsageReceiptSigner-->>ProofOfUseFacade: PouReceiptWithCommitment
    ProofOfUseFacade->>UsageReceiptStore: append(receipt)
    UsageReceiptStore-->>ProofOfUseFacade: success
Loading
sequenceDiagram
    participant Developer
    participant PublishScript as publish-proof.sh
    participant PouCLI as pou CLI
    participant DocsOutput as docs/proof output

    Developer->>PublishScript: run publish-proof.sh
    PublishScript->>PouCLI: aggregate(receipts, trusted keys)
    PouCLI-->>PublishScript: public-proof.json, audit-pack.json
    PublishScript->>PouCLI: verify-public(public-proof.json)
    PublishScript->>PouCLI: verify-audit(audit-pack.json)
    PublishScript->>DocsOutput: copy verified artifacts
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding verifiable usage proof for dictation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/proof-of-use

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
scripts/proof/enroll-install.sh (1)

8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unused MAC_APP_DIR.

Confirmed by shellcheck (SC2034): this var is computed but never referenced later in the script.

🧹 Proposed fix
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-MAC_APP_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
 POU_REPO="${POU_REPO:-$HOME/Prog/Stuff/proof-of-use}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/proof/enroll-install.sh` at line 8, Remove the unused MAC_APP_DIR
assignment from the enroll-install.sh script, since it is computed but never
referenced anywhere else. Keep the rest of the script unchanged, and verify that
no logic depends on MAC_APP_DIR before deleting it.

Source: Linters/SAST tools

docs/proof/README.md (1)

34-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language hint to the fenced code block.

Flagged by markdownlint (MD040); the block showing the receipts path has no language specified, unlike other blocks in this file.

📝 Proposed fix
-```
+```text
 ~/Library/Application Support/Mumbli/proof/receipts.jsonl
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/proof/README.md around lines 34 - 36, The fenced block in the proof
README is missing a language hint, triggering MD040. Update the markdown fence
around the receipts path example to use a text language tag, matching the style
used by other fenced blocks in the document.


</details>

<!-- cr-comment:v1:47c10c8749c7fa9099edf489 -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>scripts/proof/publish-proof.sh (1)</summary><blockquote>

`34-34`: _🩺 Stability & Availability_ | _🔵 Trivial_ | _⚡ Quick win_

**No existence check for `TRUSTED_KEYS` before parsing.**

If `docs/proof/trusted-keys.json` is missing, this fails with an opaque `python3` traceback instead of a clear error, unlike the explicit checks used for `RECEIPTS` and `ATTESTOR_KEY` above.




<details>
<summary>🛡️ Proposed fix</summary>

```diff
+if [[ ! -f "$TRUSTED_KEYS" ]]; then
+  echo "Trusted keys file not found at $TRUSTED_KEYS"
+  exit 1
+fi
+
 ISSUER_PUB="$(python3 -c "import json; print(json.load(open('$TRUSTED_KEYS'))['issuer_public_key'])")"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/proof/publish-proof.sh` at line 34, The ISSUER_PUB assignment in
publish-proof.sh parses TRUSTED_KEYS without verifying the file exists first,
unlike the earlier RECEIPTS and ATTESTOR_KEY checks. Add an explicit existence
check for TRUSTED_KEYS before the python3/json.load call, and fail with a clear,
consistent error message if the trusted-keys file is missing. Keep the fix
localized near the ISSUER_PUB logic so the script reports a friendly validation
error instead of a Python traceback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MumbliApp/ProofOfUse/CanonicalJSON.swift`:
- Around line 66-68: The writeString helper in CanonicalJSON is using
JSONSerialization.data(withJSONObject:) without allowing fragments, so top-level
String values can fail during canonical JSON generation. Update the
writeString(_:, into:) implementation to pass the fragmentsAllowed option when
serializing the string, and keep the change scoped to this helper so both object
keys and string values are handled correctly.

In `@MumbliApp/ProofOfUse/InstallIdentity.swift`:
- Around line 24-31: Serialize key access in InstallIdentity so
loadOrCreatePrivateKey, publicKeyBase64, and sign cannot race across concurrent
ProofOfUseFacade.recordDictation Task.detached calls. Add a single synchronized
access path (for example, a private serial queue or lock) around the Keychain
load/create/save flow, and cache the generated or loaded
Curve25519.Signing.PrivateKey in memory so signDictation uses one consistent key
for both installPub and signatures without repeated Keychain round-trips.

In `@MumbliApp/ProofOfUse/ProofOfUseSettingsSection.swift`:
- Around line 41-46: The “Copy install key” button in ProofOfUseSettingsSection
never resets its copied state, so update the Button action that toggles
copiedInstallKey to true and then schedule it to revert back after a short
delay, matching the confirmation-flag pattern used elsewhere in
SettingsView.swift. Use the existing copiedInstallKey state and
DispatchQueue.main.asyncAfter so the label changes to “Copied” briefly and then
returns to “Copy install key.”

In `@MumbliApp/ProofOfUse/UsageReceiptSigner.swift`:
- Around line 88-96: `UsageReceiptSignerError` currently only conforms to
`CustomStringConvertible`, so `ProofOfUseFacade` will not surface its enrollment
hint when logging `error.localizedDescription`. Update `UsageReceiptSignerError`
in `UsageReceiptSigner` to conform to `LocalizedError` and move the existing
user-facing message from `description` into `errorDescription` so the
missing-credential case is reported correctly.

In `@MumbliApp/ProofOfUse/UsageReceiptStore.swift`:
- Around line 8-12: The receipt encoding in UsageReceiptStore is not serialized
because append performs encoder.encode before entering the receipts queue, so
detached callers can race on the shared JSONEncoder. Update append to run the
encode work inside the queue’s synchronized section, or instantiate a new
JSONEncoder per call, and keep the existing encoder property out of concurrent
use.

---

Nitpick comments:
In `@docs/proof/README.md`:
- Around line 34-36: The fenced block in the proof README is missing a language
hint, triggering MD040. Update the markdown fence around the receipts path
example to use a text language tag, matching the style used by other fenced
blocks in the document.

In `@scripts/proof/enroll-install.sh`:
- Line 8: Remove the unused MAC_APP_DIR assignment from the enroll-install.sh
script, since it is computed but never referenced anywhere else. Keep the rest
of the script unchanged, and verify that no logic depends on MAC_APP_DIR before
deleting it.

In `@scripts/proof/publish-proof.sh`:
- Line 34: The ISSUER_PUB assignment in publish-proof.sh parses TRUSTED_KEYS
without verifying the file exists first, unlike the earlier RECEIPTS and
ATTESTOR_KEY checks. Add an explicit existence check for TRUSTED_KEYS before the
python3/json.load call, and fail with a clear, consistent error message if the
trusted-keys file is missing. Keep the fix localized near the ISSUER_PUB logic
so the script reports a friendly validation error instead of a Python traceback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 07da783e-89a1-4bad-bfc1-63b88da99ac8

📥 Commits

Reviewing files that changed from the base of the PR and between 49f16e1 and aaf8381.

📒 Files selected for processing (19)
  • .gitignore
  • MumbliApp.xcodeproj/project.pbxproj
  • MumbliApp/AppDelegate.swift
  • MumbliApp/ProofOfUse/Base64URL.swift
  • MumbliApp/ProofOfUse/CanonicalJSON.swift
  • MumbliApp/ProofOfUse/InstallIdentity.swift
  • MumbliApp/ProofOfUse/PouModels.swift
  • MumbliApp/ProofOfUse/ProofOfUseBadge.swift
  • MumbliApp/ProofOfUse/ProofOfUseConfig.swift
  • MumbliApp/ProofOfUse/ProofOfUseFacade.swift
  • MumbliApp/ProofOfUse/ProofOfUseSettingsSection.swift
  • MumbliApp/ProofOfUse/UsageReceiptSigner.swift
  • MumbliApp/ProofOfUse/UsageReceiptStore.swift
  • MumbliApp/UI/MenuBarController.swift
  • MumbliApp/UI/SettingsView.swift
  • docs/proof/README.md
  • docs/proof/trusted-keys.json
  • scripts/proof/enroll-install.sh
  • scripts/proof/publish-proof.sh

Comment thread MumbliApp/ProofOfUse/CanonicalJSON.swift Outdated
Comment on lines +24 to +31
private func loadOrCreatePrivateKey() throws -> Curve25519.Signing.PrivateKey {
if let existing = try loadPrivateKeyFromKeychain() {
return existing
}
let key = Curve25519.Signing.PrivateKey()
try savePrivateKeyToKeychain(key)
return key
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

TOCTOU race on install key creation.

loadOrCreatePrivateKey checks the Keychain, and only if absent generates and saves a new key, with no synchronization. ProofOfUseFacade.recordDictation runs on Task.detached, so if two dictation events complete concurrently before any key exists (e.g. overlapping hold/hands-free sessions), both threads can pass the "not found" check, generate different keys, and race on SecItemAdd/SecItemUpdate. The losing thread's in-memory key (already used to compute installPub/signature for its receipt) can diverge from the key that ends up persisted in the Keychain, producing a receipt whose install_public_key/install_signature won't verify against the install's actual current key.

Additionally, publicKeyBase64() and sign() (called separately in signDictation, per the linked context) each independently call loadOrCreatePrivateKey(), so even a single recordDictation call performs two separate Keychain round-trips that could, in theory, observe different keys if a race with another thread occurs in between.

Serialize key load/create/sign behind a single access point (e.g. a private serial queue or lock), and consider caching the loaded key in memory to avoid repeated Keychain reads.

🔒 Proposed fix using a serial queue
 final class InstallIdentity {
     static let shared = InstallIdentity()
 
     private let keychainAccount = "com.mumbli.proof.install"
     private let keychainService = "com.mumbli.proof"
+    private let queue = DispatchQueue(label: "com.mumbli.proof.install-identity")
+    private var cachedKey: Curve25519.Signing.PrivateKey?
 
     private init() {}
 
     func publicKeyBase64() throws -> String {
-        let key = try loadOrCreatePrivateKey()
+        let key = try synchronizedLoadOrCreatePrivateKey()
         return Base64URL.encode(key.publicKey.rawRepresentation)
     }
 
     func sign(_ message: Data) throws -> Data {
-        let key = try loadOrCreatePrivateKey()
+        let key = try synchronizedLoadOrCreatePrivateKey()
         return try key.signature(for: message)
     }
+
+    private func synchronizedLoadOrCreatePrivateKey() throws -> Curve25519.Signing.PrivateKey {
+        try queue.sync {
+            if let cachedKey { return cachedKey }
+            let key = try loadOrCreatePrivateKey()
+            cachedKey = key
+            return key
+        }
+    }

Also applies to: 33-50, 52-78

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MumbliApp/ProofOfUse/InstallIdentity.swift` around lines 24 - 31, Serialize
key access in InstallIdentity so loadOrCreatePrivateKey, publicKeyBase64, and
sign cannot race across concurrent ProofOfUseFacade.recordDictation
Task.detached calls. Add a single synchronized access path (for example, a
private serial queue or lock) around the Keychain load/create/save flow, and
cache the generated or loaded Curve25519.Signing.PrivateKey in memory so
signDictation uses one consistent key for both installPub and signatures without
repeated Keychain round-trips.

Comment on lines +41 to +46
Button(copiedInstallKey ? "Copied" : "Copy install key") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(installPublicKey, forType: .string)
copiedInstallKey = true
}
.controlSize(.small)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"Copy install key" button never reverts to its original label.

copiedInstallKey is set to true on tap but never reset, so the button permanently shows "Copied" after the first click. The rest of SettingsView.swift resets similar confirmation flags after a delay (e.g. elevenLabsSavedConfirm via DispatchQueue.main.asyncAfter(deadline: .now() + 2.0)); apply the same pattern here for consistency.

💚 Proposed fix
                                 Button(copiedInstallKey ? "Copied" : "Copy install key") {
                                     NSPasteboard.general.clearContents()
                                     NSPasteboard.general.setString(installPublicKey, forType: .string)
                                     copiedInstallKey = true
+                                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
+                                        copiedInstallKey = false
+                                    }
                                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Button(copiedInstallKey ? "Copied" : "Copy install key") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(installPublicKey, forType: .string)
copiedInstallKey = true
}
.controlSize(.small)
Button(copiedInstallKey ? "Copied" : "Copy install key") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(installPublicKey, forType: .string)
copiedInstallKey = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
copiedInstallKey = false
}
}
.controlSize(.small)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MumbliApp/ProofOfUse/ProofOfUseSettingsSection.swift` around lines 41 - 46,
The “Copy install key” button in ProofOfUseSettingsSection never resets its
copied state, so update the Button action that toggles copiedInstallKey to true
and then schedule it to revert back after a short delay, matching the
confirmation-flag pattern used elsewhere in SettingsView.swift. Use the existing
copiedInstallKey state and DispatchQueue.main.asyncAfter so the label changes to
“Copied” briefly and then returns to “Copy install key.”

Comment on lines +88 to +96
enum UsageReceiptSignerError: Error, CustomStringConvertible {
case missingCredential

var description: String {
switch self {
case .missingCredential:
return "Proof-of-use credential not found. Run enrollment (see Settings > Usage proof)."
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify downstream error rendering paths.
rg -n -C3 'UsageReceiptSignerError|localizedDescription|LocalizedError|CustomStringConvertible' MumbliApp/ProofOfUse MumbliApp/AppDelegate.swift

Repository: fireharp/mumbli

Length of output: 4699


Conform UsageReceiptSignerError to LocalizedError.

ProofOfUseFacade logs error.localizedDescription, so the current CustomStringConvertible message won’t show up there. Expose the enrollment hint via errorDescription instead.

-enum UsageReceiptSignerError: Error, CustomStringConvertible {
+enum UsageReceiptSignerError: LocalizedError, CustomStringConvertible {
     case missingCredential
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MumbliApp/ProofOfUse/UsageReceiptSigner.swift` around lines 88 - 96,
`UsageReceiptSignerError` currently only conforms to `CustomStringConvertible`,
so `ProofOfUseFacade` will not surface its enrollment hint when logging
`error.localizedDescription`. Update `UsageReceiptSignerError` in
`UsageReceiptSigner` to conform to `LocalizedError` and move the existing
user-facing message from `description` into `errorDescription` so the
missing-credential case is reported correctly.

Comment on lines +8 to +12
private let encoder: JSONEncoder = {
let enc = JSONEncoder()
enc.outputFormatting = [.sortedKeys]
return enc
}()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify append call sites and shared encoder usage.
rg -n -C3 'UsageReceiptStore\.shared\.append|Task\.detached|JSONEncoder|private let encoder' MumbliApp

Repository: fireharp/mumbli

Length of output: 3278


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== UsageReceiptStore.swift ==\n'
cat -n MumbliApp/ProofOfUse/UsageReceiptStore.swift

printf '\n== ProofOfUseFacade.swift ==\n'
cat -n MumbliApp/ProofOfUse/ProofOfUseFacade.swift

printf '\n== All append call sites ==\n'
rg -n 'UsageReceiptStore\.shared\.append|append\(_ item: PouReceiptWithCommitment\)|private let encoder|JSONEncoder\(' MumbliApp/ProofOfUse MumbliApp/Models

Repository: fireharp/mumbli

Length of output: 4546


🌐 Web query:

Swift Foundation JSONEncoder thread safety shared instance concurrent use documentation

💡 Result:

The Swift Foundation JSONEncoder class is not thread-safe and should not be shared across concurrent threads or tasks [1]. While JSONEncoder is intended to be a reusable object for configuring encoding strategies (such as date or key encoding strategies), its internal implementation involves mutable state that is not protected for concurrent access [1][2][3]. Attempting to use a single instance of JSONEncoder simultaneously from multiple threads can lead to unpredictable behavior, data corruption, or runtime crashes [4]. Key points regarding its usage: 1. Thread Safety: JSONEncoder is not thread-safe. You must not access a single instance from multiple threads or asynchronous tasks concurrently [1]. 2. Best Practice: If you need to perform encoding in a concurrent environment, create a new instance of JSONEncoder for each thread or task, or ensure that access to a shared instance is properly synchronized (e.g., using a serial dispatch queue or an actor) [1]. 3. Reusability: While you should not share instances concurrently, JSONEncoder is designed to be reusable sequentially. It is efficient to create and configure an instance once and reuse it within the same thread or serial context. 4. Implementation Details: Internally, the encoder uses reference-based storage and caching mechanisms (such as shared sub-encoders) that maintain mutable state [2][3][4]. Even recent internal improvements to these caching mechanisms emphasize the risks of stale or leaked state when encoders are improperly managed [4]. For optimal performance and safety, avoid sharing JSONEncoder instances across concurrent boundaries [1].

Citations:


Serialize encoding on the receipts queue MumbliApp/ProofOfUse/UsageReceiptStore.swift:21-25append can be called from detached tasks, but encoder.encode runs before queue.sync, so concurrent writes can hit the same JSONEncoder instance. Move encoding inside the queue or create a fresh encoder per append.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MumbliApp/ProofOfUse/UsageReceiptStore.swift` around lines 8 - 12, The
receipt encoding in UsageReceiptStore is not serialized because append performs
encoder.encode before entering the receipts queue, so detached callers can race
on the shared JSONEncoder. Update append to run the encode work inside the
queue’s synchronized section, or instantiate a new JSONEncoder per call, and
keep the existing encoder property out of concurrent use.

Fix CanonicalJSON string encoding to match Go (no escaped slashes),
open verification guide locally instead of GitHub main, and add
ProofOfUseUITests plus canonical JSON test script.

Co-authored-by: Cursor <cursoragent@cursor.com>
@mintlify

mintlify Bot commented Jul 6, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
mumbli 🟢 Ready View Preview Jul 6, 2026, 2:27 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

Embed project grant in the app bundle, verify code signature locally, auto-issue credentials without manual enrollment, and wire publish/e2e scripts to the grant registry.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant