From c99adbbe0b03ebbc56708cef57d6bd996c03f36a Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sun, 19 Apr 2026 23:45:59 +0200 Subject: [PATCH] Postbox -> TelegramEngine wave 5: uploadSecureIdFile + SecureId context Migrates uploadSecureIdFile's public signature from (context:, postbox: Postbox, network: Network, resource: MediaResource) to (context:, engine: TelegramEngine, resource: EngineMediaResource). SecureIdVerificationDocumentsContext refactored to hold engine: TelegramEngine instead of raw postbox + network, drops import Postbox (tally +1). The one instantiation site in SecureIdDocumentFormControllerNode updates to pass engine: self.context.engine. Completes the last explicitly-named future-wave candidate from CLAUDE.md. Bundled: spec + plan + C1 atomic migration + CLAUDE.md outcome. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 14 +- ...-04-18-postbox-to-telegramengine-wave-5.md | 381 ++++++++++++++++++ ...postbox-to-telegramengine-wave-5-design.md | 270 +++++++++++++ .../SecureIdDocumentFormControllerNode.swift | 2 +- ...SecureIdVerificationDocumentsContext.swift | 11 +- .../SecureId/UploadSecureIdFile.swift | 12 +- 6 files changed, 675 insertions(+), 15 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md create mode 100644 docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-5-design.md diff --git a/CLAUDE.md b/CLAUDE.md index e8cfe847ff..3986d05dc0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -142,6 +142,18 @@ No module becomes Postbox-free in this wave (both caller files import Postbox fo Plan: `docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-4.md` +### Wave 5 outcome (2026-04-18) + +Completes the last explicitly-named future-wave candidate from the wave-2 final review. + +`uploadSecureIdFile(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource)` migrated in place to `(context:, engine: TelegramEngine, resource: EngineMediaResource)`. Function body accesses raw Postbox types via `engine.account.postbox` / `engine.account.network` (internal Postbox-facing layer stays raw per the standing rule). + +1 consumer submodule fully de-Postboxed: `SecureIdVerificationDocumentsContext` (PassportUI/Sources). Signature changed from `(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: ...)` to `(engine: TelegramEngine, context: SecureIdAccessContext, update: ...)`; stored props collapsed into a single `engine: TelegramEngine` field. One instantiation site updated in the same commit. + +After this wave, the "Known future-wave candidates" list contains only the 4 permanently-blocked classes conforming to `TelegramMediaResource`. + +Plan: `docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md` + ### Modules currently free of `import Postbox` (running tally) Consumer modules that no longer import Postbox, across all waves and standalone commits: @@ -155,12 +167,12 @@ Consumer modules that no longer import Postbox, across all waves and standalone - `PresentationDataUtils` (standalone cleanup) - `MapResourceToAvatarSizes` (wave 2) - `SaveToCameraRoll` (wave 3) +- `SecureIdVerificationDocumentsContext` (wave 5) ### Known future-wave candidates Surfaced by the wave-2 final review: -- `submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift: public func uploadSecureIdFile(…, postbox: Postbox, …, resource: MediaResource)` — rule-2-sensitive (umbrella-type leak). Needs a paired wave with its caller(s). - Classes conforming to `TelegramMediaResource` (need `isEqual(to: MediaResource)` override) remain **permanently blocked** from consumer-side migration: `ICloudFileResource`, `InstantPageExternalMediaResource`, `VideoLibraryMediaResource`, `YoutubeEmbedStoryboardMediaResource`. Either move the class into `TelegramCore` or keep `import Postbox` in its module. ### Build environment quirk diff --git a/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md b/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md new file mode 100644 index 0000000000..9b77640185 --- /dev/null +++ b/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md @@ -0,0 +1,381 @@ +# Postbox → TelegramEngine Wave 5 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate `uploadSecureIdFile`'s public surface to `(context:, engine: TelegramEngine, resource: EngineMediaResource)`, refactor `SecureIdVerificationDocumentsContext` to take `engine: TelegramEngine` in place of raw `Postbox` + `Network`, and drop `import Postbox` from that caller module. Land as one atomic code commit + one CLAUDE.md tally commit on branch `refactor/postbox-to-engine-wave-5`. + +**Architecture:** Three files land together in C1 because the facade signature change, the caller class's stored-property change, and the one instantiation site are mutually breaking. The facade body inside TelegramCore continues to access raw Postbox types via `engine.account.postbox` / `engine.account.network` — CLAUDE.md's "internal Postbox-facing stays raw" rule applies to the body, while the public signature is the clean surface. C2 updates the CLAUDE.md tally and removes the wave-5-named bullet from "Known future-wave candidates". + +**Tech Stack:** Swift / Bazel. No unit tests by repo policy — verification is a full project build. + +**Spec:** [docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-5-design.md](docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-5-design.md) + +**Build command** (use for every "full build" step): + +```bash +source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 +``` + +The `source ~/.zshrc` prefix is required because `TELEGRAM_CODESIGNING_GIT_PASSWORD` lives in `~/.zshrc` and the bash tool does not source shell config by default. For a long-running build, prefer `run_in_background: true` from the controller session (subagent-spawned background builds orphan when the subagent's shell terminates). + +--- + +## Task 1: Pre-flight re-verification + +No code changes. Confirms the inventory hasn't drifted. + +- [ ] **Step 1: Re-grep `uploadSecureIdFile` call sites** + +```bash +grep -rnE "uploadSecureIdFile\(" submodules --include="*.swift" | grep -v "/SecureId/" +``` + +Expected: exactly 1 match — `submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift:43`. If the count or file has drifted, stop and revise the plan. + +- [ ] **Step 2: Re-grep `SecureIdVerificationDocumentsContext(...)` instantiation sites** + +```bash +grep -rnE "SecureIdVerificationDocumentsContext\(" submodules --include="*.swift" | grep -v "final class SecureIdVerificationDocumentsContext" +``` + +Expected: exactly 1 match — `submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift:2172`. If drift, stop. + +- [ ] **Step 3: Confirm `AccountContext.engine` protocol requirement** + +```bash +grep -nE "var engine: TelegramEngine" submodules/AccountContext/Sources/AccountContext.swift +``` + +Expected: one line matching `var engine: TelegramEngine { get }` (the protocol requirement). This confirms `self.context.engine` will be available at the instantiation site in Task 4. + +- [ ] **Step 4: Confirm `info.resource` type** + +```bash +grep -nE "let resource:" submodules/PassportUI/Sources/SecureIdVerificationDocument.swift +``` + +Expected: two matches, both showing `resource: TelegramMediaResource`. Confirms `EngineMediaResource(info.resource)` will compile (the `EngineMediaResource(_:)` init takes `MediaResource`, which `TelegramMediaResource` inherits). + +--- + +## Task 2: Migrate `uploadSecureIdFile`'s public facade and body + +No build; no commit. Tasks 2–4 share one atomic commit in Task 5. + +**File:** `submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift` + +- [ ] **Step 1: Replace the function signature and body** + +Find the `uploadSecureIdFile` function (currently starts at line 90). Replace the entire function (from `public func uploadSecureIdFile` through its closing `}`) with this version: + +```swift +public func uploadSecureIdFile(context: SecureIdAccessContext, engine: TelegramEngine, resource: EngineMediaResource) -> Signal { + return engine.account.postbox.mediaBox.resourceData(resource._asResource()) + |> mapError { _ -> UploadSecureIdFileError in + } + |> mapToSignal { next -> Signal in + if !next.complete { + return .complete() + } + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: next.path)) else { + return .fail(.generic) + } + + guard let encryptedData = encryptedSecureIdFile(context: context, data: data) else { + return .fail(.generic) + } + + return multipartUpload(network: engine.account.network, postbox: engine.account.postbox, source: .data(encryptedData.data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + |> mapError { _ -> UploadSecureIdFileError in + return .generic + } + |> mapToSignal { result -> Signal in + switch result { + case let .progress(value): + return .single(.progress(value)) + case let .inputFile(.inputFile(fileData)): + return .single(.result(UploadedSecureIdFile(id: fileData.id, parts: fileData.parts, md5Checksum: fileData.md5Checksum, fileHash: encryptedData.hash, encryptedSecret: encryptedData.encryptedSecret), encryptedData.data)) + default: + return .fail(.generic) + } + } + } +} +``` + +Changes from the original: + +- Signature: `(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource)` → `(context: SecureIdAccessContext, engine: TelegramEngine, resource: EngineMediaResource)`. +- Line 1 of body: `postbox.mediaBox.resourceData(resource)` → `engine.account.postbox.mediaBox.resourceData(resource._asResource())`. +- Inside the `mapToSignal`: `multipartUpload(network: network, postbox: postbox, ...)` → `multipartUpload(network: engine.account.network, postbox: engine.account.postbox, ...)`. + +No other file in `TelegramCore/Sources/TelegramEngine/SecureId/` is touched. No imports change inside `UploadSecureIdFile.swift` — it continues to `import Foundation`, `import Postbox`, `import MtProtoKit`, `import SwiftSignalKit`, which remain correct (the body still uses raw Postbox types via `engine.account.postbox`). + +--- + +## Task 3: Migrate `SecureIdVerificationDocumentsContext` + +No build; no commit. + +**File:** `submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift` + +- [ ] **Step 1: Drop `import Postbox`** + +Replace the import block at the top (lines 1–4): + +```swift +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +``` + +with: + +```swift +import Foundation +import TelegramCore +import SwiftSignalKit +``` + +Only `Postbox` is removed. The three remaining imports stay. + +- [ ] **Step 2: Replace stored properties** + +Find the `final class SecureIdVerificationDocumentsContext` block (starting around line 18). Replace lines 20–21: + +```swift + private let postbox: Postbox + private let network: Network +``` + +with: + +```swift + private let engine: TelegramEngine +``` + +- [ ] **Step 3: Update the constructor** + +Replace the `init` (lines 26–31): + +```swift + init(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { + self.postbox = postbox + self.network = network + self.context = context + self.update = update + } +``` + +with: + +```swift + init(engine: TelegramEngine, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { + self.engine = engine + self.context = context + self.update = update + } +``` + +- [ ] **Step 4: Update the `uploadSecureIdFile` call inside `stateUpdated`** + +Find line 43, which currently reads: + +```swift + disposable.set((uploadSecureIdFile(context: self.context, postbox: self.postbox, network: self.network, resource: info.resource) +``` + +Replace with: + +```swift + disposable.set((uploadSecureIdFile(context: self.context, engine: self.engine, resource: EngineMediaResource(info.resource)) +``` + +Two changes: +- `postbox: self.postbox, network: self.network` → `engine: self.engine`. +- `resource: info.resource` → `resource: EngineMediaResource(info.resource)`. + +No other line in this file changes. The `DocumentContext` inner class is untouched. The `stateUpdated` method's outer structure is untouched. + +--- + +## Task 4: Update the instantiation site + +No build; no commit. + +**File:** `submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift` + +- [ ] **Step 1: Update line 2172** + +Find line 2172, which currently reads: + +```swift + self.uploadContext = SecureIdVerificationDocumentsContext(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, update: { id, state in +``` + +Replace with: + +```swift + self.uploadContext = SecureIdVerificationDocumentsContext(engine: self.context.engine, context: self.secureIdContext, update: { id, state in +``` + +Two removed arguments (`postbox:`, `network:`) collapse into one new argument (`engine:`). The closure body inside `update: { id, state in ... }` is unchanged. + +No other line in this file changes. The file continues to `import Postbox` for unrelated reasons — this is expected, do not remove. + +--- + +## Task 5: Full build and commit C1 + +- [ ] **Step 1: Run the full project build** + +Run the build command from the header. Expected: clean success. + +Typical failure modes and fixes (do them inline — do not split): + +- **"cannot convert value of type 'Postbox' to expected argument type 'TelegramEngine'"** — a call site was missed. Re-grep both `uploadSecureIdFile(` and `SecureIdVerificationDocumentsContext(` across the repo. +- **"cannot convert value of type 'MediaResource' to expected argument type 'EngineMediaResource'"** — Task 3 Step 4's `EngineMediaResource(info.resource)` wrap was missed. +- **"use of unresolved identifier 'Network'"** or **"use of unresolved identifier 'Postbox'"** inside `SecureIdVerificationDocumentsContext.swift`** — Tasks 3 Steps 2–3 or 4 weren't fully applied. +- **"missing argument for parameter 'engine'"** — the Task 4 call site wasn't updated. + +Re-run the build after each fix. + +- [ ] **Step 2: Stage the three files** + +```bash +git add \ + submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift \ + submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift \ + submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift +``` + +- [ ] **Step 3: Verify diff scope** + +```bash +git diff --staged --stat +``` + +Expected: exactly 3 files staged. Approximate line changes: +- `UploadSecureIdFile.swift`: ~3 lines (signature + 2 body sites). +- `SecureIdVerificationDocumentsContext.swift`: ~8 lines (1 import removed, stored props, constructor, call site). +- `SecureIdDocumentFormControllerNode.swift`: 1 line. + +- [ ] **Step 4: Commit C1** + +```bash +git commit -m "$(cat <<'EOF' +SecureId: migrate uploadSecureIdFile + context to TelegramEngine + +uploadSecureIdFile's public signature now takes engine: TelegramEngine +and resource: EngineMediaResource instead of raw postbox: Postbox + +network: Network + MediaResource. The function body accesses raw +Postbox types via engine.account.postbox / engine.account.network +(internal Postbox-facing layer stays raw per CLAUDE.md). + +SecureIdVerificationDocumentsContext refactored in lockstep: stores +engine: TelegramEngine instead of raw postbox + network, drops +import Postbox. The one instantiation site in +SecureIdDocumentFormControllerNode updates to pass engine: +self.context.engine. + +Wave-5 of the Postbox -> TelegramEngine refactor; completes the last +explicitly-named future-wave candidate. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 5: Verify branch state** + +```bash +git log --oneline master..HEAD +``` + +Expected (newest at top): +- ` SecureId: migrate uploadSecureIdFile + context to TelegramEngine` +- `b7a1a5dfb0 docs(spec): wave-5 uploadSecureIdFile facade + SecureId context migration` + +--- + +## Task 6: Update CLAUDE.md tally and commit C2 + +- [ ] **Step 1: Add `SecureIdVerificationDocumentsContext` to the Postbox-free tally** + +Open `CLAUDE.md`. Find the "Modules currently free of `import Postbox` (running tally)" section. Add `- SecureIdVerificationDocumentsContext (wave 5)` as the last bullet in the list, immediately after `- SaveToCameraRoll (wave 3)`: + +```markdown +- `MapResourceToAvatarSizes` (wave 2) +- `SaveToCameraRoll` (wave 3) +- `SecureIdVerificationDocumentsContext` (wave 5) +``` + +- [ ] **Step 2: Add a "Wave 5 outcome" subsection** + +Still in `CLAUDE.md`, find the "Wave 4 outcome (2026-04-18)" block. Insert a new "Wave 5 outcome" subsection **after** Wave 4 and **before** "Modules currently free of `import Postbox`": + +```markdown +### Wave 5 outcome (2026-04-18) + +Completes the last explicitly-named future-wave candidate from the wave-2 final review. + +`uploadSecureIdFile(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource)` migrated in place to `(context:, engine: TelegramEngine, resource: EngineMediaResource)`. Function body accesses raw Postbox types via `engine.account.postbox` / `engine.account.network` (internal Postbox-facing layer stays raw per the standing rule). + +1 consumer submodule fully de-Postboxed: `SecureIdVerificationDocumentsContext` (PassportUI/Sources). Signature changed from `(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: ...)` to `(engine: TelegramEngine, context: SecureIdAccessContext, update: ...)`; stored props collapsed into a single `engine: TelegramEngine` field. One instantiation site updated in the same commit. + +After this wave, the "Known future-wave candidates" list contains only the 4 permanently-blocked classes conforming to `TelegramMediaResource`. + +Plan: `docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md` +``` + +- [ ] **Step 3: Remove the `uploadSecureIdFile` bullet from "Known future-wave candidates"** + +Still in `CLAUDE.md`, find the "Known future-wave candidates" list. Delete this bullet entirely: + +```markdown +- `submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift: public func uploadSecureIdFile(…, postbox: Postbox, …, resource: MediaResource)` — rule-2-sensitive (umbrella-type leak). Needs a paired wave with its caller(s). +``` + +Do not touch the remaining bullet about permanently-blocked classes. + +- [ ] **Step 4: Commit C2** + +```bash +git add CLAUDE.md +git commit -m "$(cat <<'EOF' +CLAUDE.md: record wave-5 outcome + +Adds SecureIdVerificationDocumentsContext to the Postbox-free module +tally, documents the uploadSecureIdFile facade migration, and removes +the uploadSecureIdFile bullet from "Known future-wave candidates". +After this wave, the candidate list contains only the 4 permanently- +blocked TelegramMediaResource-conforming classes. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 5: Verify final branch state** + +```bash +git log --oneline master..HEAD +``` + +Expected: +- ` CLAUDE.md: record wave-5 outcome` +- ` SecureId: migrate uploadSecureIdFile + context to TelegramEngine` +- `b7a1a5dfb0 docs(spec): wave-5 uploadSecureIdFile facade + SecureId context migration` + +--- + +## Success criteria + +- `uploadSecureIdFile`'s public signature references neither `Postbox`, `Network`, nor `MediaResource`. +- `SecureIdVerificationDocumentsContext.swift` does not contain `import Postbox`. +- Full build succeeds in `debug_sim_arm64`. +- `grep -l "import Postbox" submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift` returns no match. +- `CLAUDE.md`'s "Known future-wave candidates" list no longer mentions `uploadSecureIdFile`; the Postbox-free running tally includes `SecureIdVerificationDocumentsContext (wave 5)`. +- Branch `refactor/postbox-to-engine-wave-5` contains 3 commits above `master`: 1 doc (spec) + 1 code (C1) + 1 tally (C2). diff --git a/docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-5-design.md b/docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-5-design.md new file mode 100644 index 0000000000..3442a2ba95 --- /dev/null +++ b/docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-5-design.md @@ -0,0 +1,270 @@ +# Postbox → TelegramEngine refactor, Wave 5: `uploadSecureIdFile` facade + SecureId context migration + +**Date:** 2026-04-18 +**Status:** Design approved; awaiting implementation plan. +**Predecessors:** Waves 1–4. + +## Goal + +Complete the last explicitly-named future-wave candidate from CLAUDE.md: migrate `uploadSecureIdFile`'s public surface to stop leaking the `Postbox`/`Network`/`MediaResource` Postbox-domain types, and refactor its caller `SecureIdVerificationDocumentsContext` so the caller stops importing Postbox. + +- `uploadSecureIdFile(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource)` → `(context:, engine: TelegramEngine, resource: EngineMediaResource)`. +- `SecureIdVerificationDocumentsContext` drops its `postbox: Postbox` + `network: Network` stored properties, takes `engine: TelegramEngine` instead, and drops `import Postbox` from the file. +- The one instantiation site updates to pass `engine: self.context.engine`. + +## Non-goals + +- Migrating `SecureIdAccessContext`, `SecureIdVerificationDocument`, `SecureIdVerificationLocalDocument`, or other SecureId-domain types. They live in TelegramCore (not Postbox) already and do not leak. +- Migrating other SecureId-family functions in TelegramCore (e.g., `_internal_requestSecureIdVerification`, etc.). Future-wave work. +- Dropping `import Postbox` from `SecureIdDocumentFormControllerNode.swift`. That file imports Postbox for unrelated reasons. + +## Scope and inventory + +### Files touched (3 in the code commit) + +1. `submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift` — facade signature change, 3-line body bridge. +2. `submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift` — stored props, constructor, internal call, drop `import Postbox`. +3. `submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift` — one-line instantiation call. + +### Facade signature migration (`UploadSecureIdFile.swift`) + +Before: + +```swift +public func uploadSecureIdFile(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource) -> Signal { + return postbox.mediaBox.resourceData(resource) + |> mapError { _ -> UploadSecureIdFileError in + } + |> mapToSignal { next -> Signal in + if !next.complete { + return .complete() + } + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: next.path)) else { + return .fail(.generic) + } + + guard let encryptedData = encryptedSecureIdFile(context: context, data: data) else { + return .fail(.generic) + } + + return multipartUpload(network: network, postbox: postbox, source: .data(encryptedData.data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + |> mapError { _ -> UploadSecureIdFileError in + return .generic + } + |> mapToSignal { result -> Signal in + switch result { + case let .progress(value): + return .single(.progress(value)) + case let .inputFile(.inputFile(fileData)): + return .single(.result(UploadedSecureIdFile(id: fileData.id, parts: fileData.parts, md5Checksum: fileData.md5Checksum, fileHash: encryptedData.hash, encryptedSecret: encryptedData.encryptedSecret), encryptedData.data)) + default: + return .fail(.generic) + } + } + } +} +``` + +After: + +```swift +public func uploadSecureIdFile(context: SecureIdAccessContext, engine: TelegramEngine, resource: EngineMediaResource) -> Signal { + return engine.account.postbox.mediaBox.resourceData(resource._asResource()) + |> mapError { _ -> UploadSecureIdFileError in + } + |> mapToSignal { next -> Signal in + if !next.complete { + return .complete() + } + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: next.path)) else { + return .fail(.generic) + } + + guard let encryptedData = encryptedSecureIdFile(context: context, data: data) else { + return .fail(.generic) + } + + return multipartUpload(network: engine.account.network, postbox: engine.account.postbox, source: .data(encryptedData.data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + |> mapError { _ -> UploadSecureIdFileError in + return .generic + } + |> mapToSignal { result -> Signal in + switch result { + case let .progress(value): + return .single(.progress(value)) + case let .inputFile(.inputFile(fileData)): + return .single(.result(UploadedSecureIdFile(id: fileData.id, parts: fileData.parts, md5Checksum: fileData.md5Checksum, fileHash: encryptedData.hash, encryptedSecret: encryptedData.encryptedSecret), encryptedData.data)) + default: + return .fail(.generic) + } + } + } +} +``` + +Three substantive body changes, all in line with the CLAUDE.md rule that "internal Postbox-facing stays raw" — the body is inside TelegramCore itself so it accesses raw Postbox types through `engine.account.postbox` without going through the wave-3 facades: + +- `postbox.mediaBox.resourceData(resource)` → `engine.account.postbox.mediaBox.resourceData(resource._asResource())` (unwrap the engine resource before handing to raw MediaBox). +- `network: network` → `network: engine.account.network`. +- `postbox: postbox` → `postbox: engine.account.postbox`. + +The `_internal_*` convention does not apply here because `uploadSecureIdFile` is itself the facade — there is no separate raw-typed `_internal_uploadSecureIdFile` helper, and this wave does not introduce one. The function continues to have a single definition serving both internal TelegramCore wiring and consumer use. + +### Caller-class migration (`SecureIdVerificationDocumentsContext.swift`) + +Before: + +```swift +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +private final class DocumentContext { + private let disposable: Disposable + + init(disposable: Disposable) { + self.disposable = disposable + } + + deinit { + self.disposable.dispose() + } +} + +final class SecureIdVerificationDocumentsContext { + private let context: SecureIdAccessContext + private let postbox: Postbox + private let network: Network + private let update: (Int64, SecureIdVerificationLocalDocumentState) -> Void + private var contexts: [Int64: DocumentContext] = [:] + private(set) var uploadedFiles: [Data: Data] = [:] + + init(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { + self.postbox = postbox + self.network = network + self.context = context + self.update = update + } + + func stateUpdated(_ documents: [SecureIdVerificationDocument]) { + // ... + disposable.set((uploadSecureIdFile(context: self.context, postbox: self.postbox, network: self.network, resource: info.resource) + // ... + } +} +``` + +After: + +```swift +import Foundation +import TelegramCore +import SwiftSignalKit + +private final class DocumentContext { + private let disposable: Disposable + + init(disposable: Disposable) { + self.disposable = disposable + } + + deinit { + self.disposable.dispose() + } +} + +final class SecureIdVerificationDocumentsContext { + private let context: SecureIdAccessContext + private let engine: TelegramEngine + private let update: (Int64, SecureIdVerificationLocalDocumentState) -> Void + private var contexts: [Int64: DocumentContext] = [:] + private(set) var uploadedFiles: [Data: Data] = [:] + + init(engine: TelegramEngine, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { + self.engine = engine + self.context = context + self.update = update + } + + func stateUpdated(_ documents: [SecureIdVerificationDocument]) { + // ... + disposable.set((uploadSecureIdFile(context: self.context, engine: self.engine, resource: EngineMediaResource(info.resource)) + // ... + } +} +``` + +Changes: + +1. Drop `import Postbox` (line 2). +2. Replace `private let postbox: Postbox` and `private let network: Network` with `private let engine: TelegramEngine`. +3. Constructor: `postbox:, network:, context:, update:` → `engine:, context:, update:`. +4. Constructor body: `self.postbox = postbox; self.network = network` → `self.engine = engine`. +5. Inside `stateUpdated`: `postbox: self.postbox, network: self.network` → `engine: self.engine`; `resource: info.resource` → `resource: EngineMediaResource(info.resource)` (wrap; `info.resource` is `TelegramMediaResource` per `SecureIdVerificationLocalDocument` definition). + +`DocumentContext` inner class is untouched. Other methods in the file are untouched. + +### Instantiation-site edit (`SecureIdDocumentFormControllerNode.swift`) + +Single line, [line 2172](submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift#L2172): + +```swift +// Before +self.uploadContext = SecureIdVerificationDocumentsContext(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, update: { id, state in + +// After +self.uploadContext = SecureIdVerificationDocumentsContext(engine: self.context.engine, context: self.secureIdContext, update: { id, state in +``` + +`self.context` is the outer `AccountContext`. `self.context.engine` is the `TelegramEngine` (universally available via the `AccountContext` protocol). `self.secureIdContext` is the unrelated inner `SecureIdAccessContext` — kept as the `context:` argument. + +## Execution-time re-inventory + +Before editing, re-grep to catch any new call sites: + +```bash +grep -rnE "uploadSecureIdFile\(" submodules --include="*.swift" | grep -v "/SecureId/" +grep -rnE "SecureIdVerificationDocumentsContext\(" submodules --include="*.swift" | grep -v "final class SecureIdVerificationDocumentsContext" +``` + +Expected: exactly 1 match for each — `SecureIdVerificationDocumentsContext.swift:43` and `SecureIdDocumentFormControllerNode.swift:2172` respectively. If either count has drifted, stop and revise the plan. + +## Commit plan + +**C1 — `SecureId: migrate uploadSecureIdFile + context to TelegramEngine`** (atomic) + +- All three files listed above, landing together. + +**C2 — `CLAUDE.md: record wave-5 outcome`** + +- Add `SecureIdVerificationDocumentsContext` to the "Modules currently free of `import Postbox`" running tally. +- Add a "Wave 5 outcome (2026-04-18)" subsection describing the migration. +- Remove the `uploadSecureIdFile` bullet from "Known future-wave candidates". After this, only the 4 permanently-blocked classes remain. + +## Build verification + +One full project build after C1's edits, before committing. Bazel command from CLAUDE.md with `source ~/.zshrc 2>/dev/null;` prefix. + +## Risks and mitigations + +- **Risk:** an additional call site of `uploadSecureIdFile` appears between planning and execution. **Mitigation:** the execution-time re-grep catches this. Expected 1 match. +- **Risk:** `SecureIdDocumentFormControllerNode.swift`'s `self.context` isn't an `AccountContext` at the instantiation site. **Mitigation:** confirm at execution time. The `AccountContext` protocol mandates `var engine: TelegramEngine { get }`, so any concrete `AccountContext` has it. +- **Risk:** behavior regression from `multipartUpload(network: engine.account.network, postbox: engine.account.postbox, …)`. **Mitigation:** these are the same underlying instances as the pre-migration `self.network` / `self.postbox` values (both originate from `self.context.account.network` / `.postbox`). Zero behavior change. +- **Risk:** after `import Postbox` is dropped from `SecureIdVerificationDocumentsContext.swift`, an implicit `Network` type (used elsewhere in the file?) fails to resolve. **Mitigation:** the file's only `Network` usage is in the stored `private let network` and the constructor parameter — both removed. No other `Network` reference survives. +- **Rule-2 compliance:** no `Postbox`/`Account`/`MediaBox` typealias introduced. No new wrapper struct. The facade body's `engine.account.postbox.mediaBox` and `engine.account.network` are internal expressions inside TelegramCore (not public surface). ✅ + +## Abandonment criteria + +If any of the 3 files cannot be migrated mechanically (e.g. `SecureIdDocumentFormControllerNode.swift`'s enclosing class doesn't have an `AccountContext`), abandon the wave and record the reason. The one-commit atomic shape means either the whole thing lands or none of it does. + +## Expected outcome + +- `uploadSecureIdFile`'s public signature references neither `Postbox` nor `Network` nor `MediaResource`. +- `SecureIdVerificationDocumentsContext` no longer imports Postbox and joins the Postbox-free running tally. +- `SecureIdDocumentFormControllerNode.swift` continues to import Postbox for unrelated reasons (no tally impact). +- `uploadSecureIdFile` bullet is removed from CLAUDE.md's "Known future-wave candidates"; after this wave, only the 4 permanently-blocked `TelegramMediaResource`-conforming classes remain in the candidate list. +- Full build succeeds in `debug_sim_arm64`. +- Zero behavior change. diff --git a/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift b/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift index e3f85c953c..3157d2e8ff 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift @@ -2169,7 +2169,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode Void)? - self.uploadContext = SecureIdVerificationDocumentsContext(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, update: { id, state in + self.uploadContext = SecureIdVerificationDocumentsContext(engine: self.context.engine, context: self.secureIdContext, update: { id, state in updateImpl?(id, state) }) diff --git a/submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift b/submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift index f72286c482..6ffec45c81 100644 --- a/submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift +++ b/submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift @@ -1,5 +1,4 @@ import Foundation -import Postbox import TelegramCore import SwiftSignalKit @@ -17,15 +16,13 @@ private final class DocumentContext { final class SecureIdVerificationDocumentsContext { private let context: SecureIdAccessContext - private let postbox: Postbox - private let network: Network + private let engine: TelegramEngine private let update: (Int64, SecureIdVerificationLocalDocumentState) -> Void private var contexts: [Int64: DocumentContext] = [:] private(set) var uploadedFiles: [Data: Data] = [:] - init(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { - self.postbox = postbox - self.network = network + init(engine: TelegramEngine, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { + self.engine = engine self.context = context self.update = update } @@ -40,7 +37,7 @@ final class SecureIdVerificationDocumentsContext { if self.contexts[info.id] == nil { let disposable = MetaDisposable() self.contexts[info.id] = DocumentContext(disposable: disposable) - disposable.set((uploadSecureIdFile(context: self.context, postbox: self.postbox, network: self.network, resource: info.resource) + disposable.set((uploadSecureIdFile(context: self.context, engine: self.engine, resource: EngineMediaResource(info.resource)) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { switch result { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift index fbc091f82e..0ee5c27c2d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift @@ -87,24 +87,24 @@ func decryptedSecureIdFile(context: SecureIdAccessContext, encryptedData: Data, return unpaddedFileData } -public func uploadSecureIdFile(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource) -> Signal { - return postbox.mediaBox.resourceData(resource) +public func uploadSecureIdFile(context: SecureIdAccessContext, engine: TelegramEngine, resource: EngineMediaResource) -> Signal { + return engine.account.postbox.mediaBox.resourceData(resource._asResource()) |> mapError { _ -> UploadSecureIdFileError in } |> mapToSignal { next -> Signal in if !next.complete { return .complete() } - + guard let data = try? Data(contentsOf: URL(fileURLWithPath: next.path)) else { return .fail(.generic) } - + guard let encryptedData = encryptedSecureIdFile(context: context, data: data) else { return .fail(.generic) } - - return multipartUpload(network: network, postbox: postbox, source: .data(encryptedData.data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + + return multipartUpload(network: engine.account.network, postbox: engine.account.postbox, source: .data(encryptedData.data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> mapError { _ -> UploadSecureIdFileError in return .generic }