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) <noreply@anthropic.com>
This commit is contained in:
Isaac 2026-04-19 23:45:59 +02:00
parent b989b160c0
commit c99adbbe0b
6 changed files with 675 additions and 15 deletions

View file

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

View file

@ -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 24 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<UploadSecureIdFileResult, UploadSecureIdFileError> {
return engine.account.postbox.mediaBox.resourceData(resource._asResource())
|> mapError { _ -> UploadSecureIdFileError in
}
|> mapToSignal { next -> Signal<UploadSecureIdFileResult, UploadSecureIdFileError> 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<UploadSecureIdFileResult, UploadSecureIdFileError> 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 14):
```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 2021:
```swift
private let postbox: Postbox
private let network: Network
```
with:
```swift
private let engine: TelegramEngine
```
- [ ] **Step 3: Update the constructor**
Replace the `init` (lines 2631):
```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 23 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) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 5: Verify branch state**
```bash
git log --oneline master..HEAD
```
Expected (newest at top):
- `<sha> 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) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 5: Verify final branch state**
```bash
git log --oneline master..HEAD
```
Expected:
- `<sha> CLAUDE.md: record wave-5 outcome`
- `<sha> 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).

View file

@ -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 14.
## 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<UploadSecureIdFileResult, UploadSecureIdFileError> {
return postbox.mediaBox.resourceData(resource)
|> mapError { _ -> UploadSecureIdFileError in
}
|> mapToSignal { next -> Signal<UploadSecureIdFileResult, UploadSecureIdFileError> 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<UploadSecureIdFileResult, UploadSecureIdFileError> 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<UploadSecureIdFileResult, UploadSecureIdFileError> {
return engine.account.postbox.mediaBox.resourceData(resource._asResource())
|> mapError { _ -> UploadSecureIdFileError in
}
|> mapToSignal { next -> Signal<UploadSecureIdFileResult, UploadSecureIdFileError> 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<UploadSecureIdFileResult, UploadSecureIdFileError> 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.

View file

@ -2169,7 +2169,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode<SecureIdDocum
var updateImpl: ((Int64, SecureIdVerificationLocalDocumentState) -> 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)
})

View file

@ -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 {

View file

@ -87,24 +87,24 @@ func decryptedSecureIdFile(context: SecureIdAccessContext, encryptedData: Data,
return unpaddedFileData
}
public func uploadSecureIdFile(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource) -> Signal<UploadSecureIdFileResult, UploadSecureIdFileError> {
return postbox.mediaBox.resourceData(resource)
public func uploadSecureIdFile(context: SecureIdAccessContext, engine: TelegramEngine, resource: EngineMediaResource) -> Signal<UploadSecureIdFileResult, UploadSecureIdFileError> {
return engine.account.postbox.mediaBox.resourceData(resource._asResource())
|> mapError { _ -> UploadSecureIdFileError in
}
|> mapToSignal { next -> Signal<UploadSecureIdFileResult, UploadSecureIdFileError> 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
}