Skip to content

fix: percent-encode app-controlled path segments - #1704

Closed
nan-li wants to merge 2 commits into
mainfrom
nan/jwt-pr1-path-encoding
Closed

fix: percent-encode app-controlled path segments#1704
nan-li wants to merge 2 commits into
mainfrom
nan/jwt-pr1-path-encoding

Conversation

@nan-li

@nan-li nan-li commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Percent-encode app-controlled Live Activities path segments once, via a shared OSUrlPath helper, and (chore) normalize project.pbxproj ordering.

Details

Motivation

Values the app chooses can reach a URL path. A slash, ?, #, or % in a Live Activity type would name a different endpoint than intended. The manager was also encoding and the request encoding again, so those types went out double-encoded.

The project.pbxproj normalization is a one-time canonical UUID sort so later stacked PRs show only their own file additions. There are no logic changes in that file.

Scope

  • Live Activities start/remove/update/click/receipt request paths
  • New shared OSUrlPath in OneSignalOSCore (also usable later from ObjC)
  • project.pbxproj reorder + membership for OSUrlPath.swift

Testing

Unit testing

No new unit tests in this PR. Path-encoding coverage for user requests lands with the later JWT request-pipeline PR, which is the first consumer of external_id in paths.

Manual testing

Built UnitTestApp for iOS Simulator locally; test build succeeded.

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

nan-li and others added 2 commits August 11, 2026 16:34
Canonical sort applied by the xcodeproj tool, so later commits in this
stack show only their own file additions. No project changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Values the app chooses reach a URL path, where a slash, ?, # or % would
name a different endpoint than intended. OSUrlPath centralizes that
encoding so every path builder escapes the same way, and is @objc so the
in-app messaging path can share it.

Live Activities encoded the activity type in the manager and again in the
request, so a type holding a reserved character went out double-encoded.
Encoding now happens once, where the path is built; the manager keeps its
check so the app still hears about a type that cannot be encoded.

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

@cursor cursor 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.

Multi-model adversarial review (interrogate)

Verdict: request changes

Intent

Percent-encode app-controlled Live Activities URL path segments once via shared OSUrlPath.segment (CharacterSet.urlUserAllowed), fixing manager+request double-encoding; keep manager encodeability checks; normalize project.pbxproj for the JWT stack. JWT/identity path work is out of scope.

Reviewers

  • A: claude-fable-5-thinking-xhigh — 6 findings
  • B: gpt-5.6-sol-xhigh — 2 findings
  • C: cursor-grok-4.5-high-fast — 5 findings
  • D: claude-opus-5-thinking-high (fallback from unavailable …-xhigh) — 7 findings

Act On

  1. Start-token cache key identity changes with no migration (4/4) — Pre-PR, manager shadowed activityType with the percent-encoded value and persisted that as OSRequestSetStartToken.key in StartRequestCache (1-year TTL). This PR stores the raw string. On upgrade, for any type where encoding ≠ identity (spaces, /, %, … — exactly this PR’s population), stale encoded keys remain, remove/append miss them, and onPushSubscriptionDidChangemarkAllUnsuccessful can re-send already-encoded keys through OSUrlPath.segment again (My%20TypeMy%2520Type). Fix: decode-on-read / re-key, or bump OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY to abandon legacy entries (typed Attributes.self names are mostly unaffected).
  2. No regression coverage for single-encode (4/4) — Existing Live Activities tests use URL-safe keys and don’t assert path/URL. A tiny OSUrlPath + one prepareForExecution case with / or a space would pin the bug this PR exists to fix; deferring all tests to the JWT PR leaves this change unverified.

Consider

  • Reject "" / "." / ".." in OSUrlPath.segment so the Optional/throws path is real and path-shape can’t collapse via empty/dot segments (2–3/4). . inside typed names must still pass.
  • Prefer a segment-joining path builder (or encode at OneSignalRequest URL assembly) so “encode once” isn’t six copy-pasted guards (2/4).
  • Narrow OSUrlPath docs: they mention external_id/alias labels and ObjC sharing, but only Live Activities Swift callers are migrated; ObjC report_received still interpolates raw.

Noted

  • Update-token guards still log “activity type” while encoding activity id (pre-existing; line touched).
  • @objc NSObject wrapper is forward-looking until an ObjC consumer lands.

Dismissed

  • “Block solely because addingPercentEncoding almost never returns nil for Swift String” as a standalone merge blocker — true, but the retained throws is mostly API compatibility; pair with empty/dot rejection or clarify the comment rather than delete the surface in this PR.
  • Rewriting the whole request pipeline / encoding appId in this PR1 — out of stated scope.

Agreement map

Strong consensus on cache-key upgrade hazard and missing single-encode tests. Split on how hard to push empty/dot-segment rejection vs. structural path builder. No model disputed that the double-encode fix for new requests is directionally correct.

Inline comments call out the highest-signal spots in the diff.

Open in Web View Automation 

Sent by Cursor Automation: Untitled

guard OSUrlPath.segment(activityType) != nil else {
throw LiveActivitiesError.invalidActivityType("Cannot translate activity type to url encoded string.")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act on (4/4 reviewers): cache key identity change

Pre-change, this method shadowed activityType with the percent-encoded string and passed that as OSRequestSetStartToken(key:). That key is the dictionary identity in StartRequestCache (persisted, 1-year TTL).

Passing the raw value here fixes double-encoding for new entries, but leaves already-persisted encoded keys orphaned. On upgrade + subscription-id change, markAllUnsuccessful can re-execute a stale key like My%20Type through OSUrlPath.segmentMy%2520Type, resurrecting the bug this PR removes. removePushToStartToken with the raw type also cannot supersede the stale entry.

Please migrate/decode-on-read or bump OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY before landing.

@objc(segment:)
public static func segment(_ value: String) -> String? {
return value.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act on / consider (multi-model)

  1. Tests (4/4): This is a pure helper in a module that already has OneSignalOSCoreTests, yet nothing asserts /, ?, #, % encode once, or that already-encoded input double-encodes as documented. The JWT-PR deferral shouldn’t leave this PR’s core behavior unpinned.
  2. Optional is effectively vacuous for Swift String: addingPercentEncoding essentially never returns nil here, so manager throws / request guards never fire for real app inputs (including "").
  3. Path-shape gaps: urlUserAllowed leaves . alone and accepts "", so ".." / empty can still reshape .../start/<segment>/subscriptions/... after URL normalization. Consider guard !value.isEmpty, value != ".", value != ".." (without encoding every . inside typed type names).
  4. Doc scope: Comment mentions external_id / alias labels / ObjC builders, but only Live Activities Swift call sites are migrated in this tip.

guard let activityType = OSUrlPath.segment(self.key) else {
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot translate activity type to url encoded string.")
return false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act on: missing single-encode regression coverage

This is the site that used to second-encode a manager-pre-encoded key. A focused test that builds with key: "a b/c" (or similar), calls prepareForExecution(), and asserts the path contains a single encode (a%20b%2Fc, not %2520 / %252F) would lock the intent. Existing executor tests only use hyphenated safe keys, so they cannot distinguish old vs new behavior.


guard let activityId = self.key.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlUserAllowed) else {
guard let activityId = OSUrlPath.segment(self.key) else {
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot translate activity type to url encoded string.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Noted: self.key is an activity id, but the failure log still says “activity type”. Pre-existing copy-paste; this PR rewrote the guard line — cheap to fix while touching it (same on OSRequestRemoveUpdateToken).

@nan-li

nan-li commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Closing: Live Activities path-encoding isn't worth a standalone PR (typed Attributes.self names encode to themselves; this was getting ahead of JWT). Keeping only the project.pbxproj normalization as a separate chore.

@nan-li nan-li closed this Aug 12, 2026
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