typeferry 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/application/build.d.ts +2 -2
  2. package/dist/application/build.js +8 -2
  3. package/dist/application/build.js.map +1 -1
  4. package/dist/application/cli-arguments.d.ts +39 -2
  5. package/dist/application/cli-arguments.js +59 -1
  6. package/dist/application/cli-arguments.js.map +1 -1
  7. package/dist/application/config.d.ts +57 -1
  8. package/dist/application/config.js +36 -0
  9. package/dist/application/config.js.map +1 -1
  10. package/dist/application/native-command.d.ts +35 -0
  11. package/dist/application/native-command.js +148 -0
  12. package/dist/application/native-command.js.map +1 -0
  13. package/dist/application/native-devices.d.ts +31 -0
  14. package/dist/application/native-devices.js +94 -0
  15. package/dist/application/native-devices.js.map +1 -0
  16. package/dist/application/native-doctor.d.ts +17 -0
  17. package/dist/application/native-doctor.js +79 -0
  18. package/dist/application/native-doctor.js.map +1 -0
  19. package/dist/application/native-project.d.ts +16 -0
  20. package/dist/application/native-project.js +82 -0
  21. package/dist/application/native-project.js.map +1 -0
  22. package/dist/application/native-simulator-settings.d.ts +24 -0
  23. package/dist/application/native-simulator-settings.js +55 -0
  24. package/dist/application/native-simulator-settings.js.map +1 -0
  25. package/dist/application/native-simulator.d.ts +19 -0
  26. package/dist/application/native-simulator.js +101 -0
  27. package/dist/application/native-simulator.js.map +1 -0
  28. package/dist/application/native.d.ts +2 -0
  29. package/dist/application/native.js +86 -0
  30. package/dist/application/native.js.map +1 -0
  31. package/dist/application/runtime.d.ts +13 -0
  32. package/dist/application/runtime.js +15 -0
  33. package/dist/application/runtime.js.map +1 -0
  34. package/dist/application/vite-config.d.ts +2 -2
  35. package/dist/application/vite-config.js +16 -2
  36. package/dist/application/vite-config.js.map +1 -1
  37. package/dist/auth/client/index.d.ts +4 -0
  38. package/dist/auth/client/index.js +2 -0
  39. package/dist/auth/client/index.js.map +1 -1
  40. package/dist/auth/client/native-http.d.ts +24 -0
  41. package/dist/auth/client/native-http.js +19 -0
  42. package/dist/auth/client/native-http.js.map +1 -0
  43. package/dist/auth/client/native-session.d.ts +34 -0
  44. package/dist/auth/client/native-session.js +65 -0
  45. package/dist/auth/client/native-session.js.map +1 -0
  46. package/dist/auth/index.d.ts +6 -0
  47. package/dist/auth/index.js +3 -0
  48. package/dist/auth/index.js.map +1 -1
  49. package/dist/auth/server/native-handoff.d.ts +69 -0
  50. package/dist/auth/server/native-handoff.js +65 -0
  51. package/dist/auth/server/native-handoff.js.map +1 -0
  52. package/dist/cli/index.js +5 -0
  53. package/dist/cli/index.js.map +1 -1
  54. package/dist/cli/run.js +18 -1
  55. package/dist/cli/run.js.map +1 -1
  56. package/dist/client/backend-origin.d.ts +17 -0
  57. package/dist/client/backend-origin.js +25 -0
  58. package/dist/client/backend-origin.js.map +1 -0
  59. package/dist/client/client-http.js +8 -3
  60. package/dist/client/client-http.js.map +1 -1
  61. package/dist/client/client-socket.js +6 -1
  62. package/dist/client/client-socket.js.map +1 -1
  63. package/dist/client/client.d.ts +9 -0
  64. package/dist/client/client.js.map +1 -1
  65. package/dist/client/index.d.ts +1 -0
  66. package/dist/client/index.js +1 -0
  67. package/dist/client/index.js.map +1 -1
  68. package/dist/ejson/clone.js +7 -1
  69. package/dist/ejson/clone.js.map +1 -1
  70. package/dist/native/index.d.ts +76 -0
  71. package/dist/native/index.js +5 -0
  72. package/dist/native/index.js.map +1 -0
  73. package/dist/native/share-file.d.ts +18 -0
  74. package/dist/native/share-file.js +41 -0
  75. package/dist/native/share-file.js.map +1 -0
  76. package/dist/native/templates/bridge-view-controller.d.ts +4 -0
  77. package/dist/native/templates/bridge-view-controller.js +86 -0
  78. package/dist/native/templates/bridge-view-controller.js.map +1 -0
  79. package/dist/native/templates/file-staging.d.ts +4 -0
  80. package/dist/native/templates/file-staging.js +77 -0
  81. package/dist/native/templates/file-staging.js.map +1 -0
  82. package/dist/native/templates/http-session.d.ts +4 -0
  83. package/dist/native/templates/http-session.js +117 -0
  84. package/dist/native/templates/http-session.js.map +1 -0
  85. package/dist/native/templates/index.d.ts +4 -0
  86. package/dist/native/templates/index.js +20 -0
  87. package/dist/native/templates/index.js.map +1 -0
  88. package/dist/native/templates/keychain.d.ts +4 -0
  89. package/dist/native/templates/keychain.js +48 -0
  90. package/dist/native/templates/keychain.js.map +1 -0
  91. package/dist/native/templates/media-permission-policy.d.ts +4 -0
  92. package/dist/native/templates/media-permission-policy.js +24 -0
  93. package/dist/native/templates/media-permission-policy.js.map +1 -0
  94. package/dist/native/templates/plugin.d.ts +4 -0
  95. package/dist/native/templates/plugin.js +314 -0
  96. package/dist/native/templates/plugin.js.map +1 -0
  97. package/dist/native/templates/wake-locks.d.ts +4 -0
  98. package/dist/native/templates/wake-locks.js +47 -0
  99. package/dist/native/templates/wake-locks.js.map +1 -0
  100. package/dist/native/wake-lock.d.ts +9 -0
  101. package/dist/native/wake-lock.js +17 -0
  102. package/dist/native/wake-lock.js.map +1 -0
  103. package/package.json +18 -2
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Same-origin native transport, with a private cookie jar persisted in Keychain.
3
+ */
4
+ export const HTTP_SESSION_SWIFT = String.raw `import Foundation
5
+
6
+ final class TypeFerryHTTPSession: NSObject, URLSessionTaskDelegate {
7
+ let backendURL: URL
8
+ private let cookieAccount: String
9
+ private var session: URLSession!
10
+ private let cookieLock = NSLock()
11
+ private var generation = 0
12
+
13
+ init(backendURL: URL) throws {
14
+ self.backendURL = backendURL
15
+ cookieAccount = "cookies:" + backendURL.absoluteString
16
+ super.init()
17
+
18
+ let configuration = Self.makeConfiguration()
19
+ if let data = try TypeFerryKeychain.read(cookieAccount),
20
+ let rows = try PropertyListSerialization.propertyList(from: data, format: nil) as? [[String: Any]] {
21
+ for row in rows {
22
+ let properties = Dictionary(uniqueKeysWithValues: row.map { (HTTPCookiePropertyKey($0.key), $0.value) })
23
+ if let cookie = HTTPCookie(properties: properties),
24
+ cookie.expiresDate.map({ $0 > Date() }) ?? true {
25
+ configuration.httpCookieStorage?.setCookie(cookie)
26
+ }
27
+ }
28
+ }
29
+ session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
30
+ }
31
+
32
+ func accepts(_ url: URL) -> Bool {
33
+ TypeFerryMediaPolicy.isTrusted(localURL: backendURL, requestURL: url, isMainFrame: true)
34
+ }
35
+
36
+ func request(_ request: URLRequest, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
37
+ guard let url = request.url, accepts(url) else { completion(.failure(Self.failure("Untrusted backend URL."))); return }
38
+ let (requestSession, requestGeneration) = snapshot()
39
+ requestSession.dataTask(with: request) { [weak self] data, response, error in
40
+ if let error = error { completion(.failure(error)); return }
41
+ guard let self = self, let response = response as? HTTPURLResponse, let data = data else {
42
+ completion(.failure(Self.failure("The backend returned no response."))); return
43
+ }
44
+ do {
45
+ try self.persistCookies(session: requestSession, generation: requestGeneration)
46
+ completion(.success((data, response)))
47
+ } catch { completion(.failure(error)) }
48
+ }.resume()
49
+ }
50
+
51
+ func download(_ request: URLRequest, completion: @escaping (Result<URL, Error>) -> Void) {
52
+ guard let url = request.url, accepts(url) else { completion(.failure(Self.failure("Untrusted download URL."))); return }
53
+ let (requestSession, requestGeneration) = snapshot()
54
+ requestSession.downloadTask(with: request) { [weak self] location, response, error in
55
+ if let error = error { completion(.failure(error)); return }
56
+ guard let self = self, let location = location, let response = response as? HTTPURLResponse,
57
+ (200..<300).contains(response.statusCode) else {
58
+ completion(.failure(Self.failure("The download failed."))); return
59
+ }
60
+ do {
61
+ try self.persistCookies(session: requestSession, generation: requestGeneration)
62
+ let retained = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
63
+ try FileManager.default.moveItem(at: location, to: retained)
64
+ completion(.success(retained))
65
+ } catch { completion(.failure(error)) }
66
+ }.resume()
67
+ }
68
+
69
+ func clearCookies() throws {
70
+ cookieLock.lock()
71
+ defer { cookieLock.unlock() }
72
+ let previous = session
73
+ generation += 1
74
+ // A fresh cookie jar prevents in-flight Set-Cookie from resurrecting logout.
75
+ session = URLSession(configuration: Self.makeConfiguration(), delegate: self, delegateQueue: nil)
76
+ previous?.invalidateAndCancel()
77
+ try TypeFerryKeychain.delete(cookieAccount)
78
+ }
79
+
80
+ func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse,
81
+ newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
82
+ guard let url = request.url, accepts(url) else { completionHandler(nil); return }
83
+ completionHandler(request)
84
+ }
85
+
86
+ private func snapshot() -> (URLSession, Int) {
87
+ cookieLock.lock()
88
+ defer { cookieLock.unlock() }
89
+ return (session, generation)
90
+ }
91
+
92
+ private static func makeConfiguration() -> URLSessionConfiguration {
93
+ let configuration = URLSessionConfiguration.ephemeral
94
+ configuration.httpShouldSetCookies = true
95
+ configuration.timeoutIntervalForRequest = 60
96
+ configuration.timeoutIntervalForResource = 300
97
+ return configuration
98
+ }
99
+
100
+ private func persistCookies(session: URLSession, generation: Int) throws {
101
+ cookieLock.lock()
102
+ defer { cookieLock.unlock() }
103
+ guard generation == self.generation else { throw Self.failure("The native session was cleared.") }
104
+ let rows = (session.configuration.httpCookieStorage?.cookies ?? []).compactMap { cookie -> [String: Any]? in
105
+ guard let properties = cookie.properties else { return nil }
106
+ return Dictionary(uniqueKeysWithValues: properties.map { ($0.key.rawValue, $0.value) })
107
+ }
108
+ let data = try PropertyListSerialization.data(fromPropertyList: rows, format: .binary, options: 0)
109
+ try TypeFerryKeychain.write(cookieAccount, data: data)
110
+ }
111
+
112
+ static func failure(_ message: String) -> NSError {
113
+ NSError(domain: "TypeFerryNative", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
114
+ }
115
+ }
116
+ `;
117
+ //# sourceMappingURL=http-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-session.js","sourceRoot":"","sources":["../../../src/native/templates/http-session.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgH3C,CAAA"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Framework-owned Swift source files installed explicitly by the iOS scaffold.
3
+ */
4
+ export declare const IOS_NATIVE_TEMPLATES: Readonly<Record<string, string>>;
@@ -0,0 +1,20 @@
1
+ import { BRIDGE_VIEW_CONTROLLER_SWIFT } from './bridge-view-controller.js';
2
+ import { FILE_STAGING_SWIFT } from './file-staging.js';
3
+ import { HTTP_SESSION_SWIFT } from './http-session.js';
4
+ import { KEYCHAIN_SWIFT } from './keychain.js';
5
+ import { MEDIA_PERMISSION_POLICY_SWIFT } from './media-permission-policy.js';
6
+ import { NATIVE_PLUGIN_SWIFT } from './plugin.js';
7
+ import { WAKE_LOCKS_SWIFT } from './wake-locks.js';
8
+ /**
9
+ * Framework-owned Swift source files installed explicitly by the iOS scaffold.
10
+ */
11
+ export const IOS_NATIVE_TEMPLATES = {
12
+ 'TypeFerryBridgeViewController.swift': BRIDGE_VIEW_CONTROLLER_SWIFT,
13
+ 'TypeFerryMediaPolicy.swift': MEDIA_PERMISSION_POLICY_SWIFT,
14
+ 'TypeFerryWakeLocks.swift': WAKE_LOCKS_SWIFT,
15
+ 'TypeFerryFileStaging.swift': FILE_STAGING_SWIFT,
16
+ 'TypeFerryKeychain.swift': KEYCHAIN_SWIFT,
17
+ 'TypeFerryHTTPSession.swift': HTTP_SESSION_SWIFT,
18
+ 'TypeFerryNativePlugin.swift': NATIVE_PLUGIN_SWIFT,
19
+ };
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/native/templates/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAA;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,6BAA6B,EAAE,MAAM,2BAA2B,CAAA;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAE/C;;GAEG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAqC;IACpE,qCAAqC,EAAE,4BAA4B;IACnE,4BAA4B,EAAE,6BAA6B;IAC3D,0BAA0B,EAAE,gBAAgB;IAC5C,4BAA4B,EAAE,kBAAkB;IAChD,yBAAyB,EAAE,cAAc;IACzC,4BAA4B,EAAE,kBAAkB;IAChD,6BAA6B,EAAE,mBAAmB;CACnD,CAAA"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Native-only secrets survive application upgrades without entering web storage.
3
+ */
4
+ export declare const KEYCHAIN_SWIFT: string;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Native-only secrets survive application upgrades without entering web storage.
3
+ */
4
+ export const KEYCHAIN_SWIFT = String.raw `import Foundation
5
+ import Security
6
+
7
+ struct TypeFerryKeychain {
8
+ static func read(_ account: String) throws -> Data? {
9
+ var query = baseQuery(account)
10
+ query[kSecReturnData] = true
11
+ query[kSecMatchLimit] = kSecMatchLimitOne
12
+ var result: CFTypeRef?
13
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
14
+ if status == errSecItemNotFound { return nil }
15
+ guard status == errSecSuccess else { throw failure(status) }
16
+ return result as? Data
17
+ }
18
+
19
+ static func write(_ account: String, data: Data) throws {
20
+ let query = baseQuery(account)
21
+ let updates: [CFString: Any] = [kSecValueData: data]
22
+ let status = SecItemUpdate(query as CFDictionary, updates as CFDictionary)
23
+ if status == errSecItemNotFound {
24
+ var insertion = query
25
+ insertion[kSecValueData] = data
26
+ insertion[kSecAttrAccessible] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
27
+ let insertStatus = SecItemAdd(insertion as CFDictionary, nil)
28
+ guard insertStatus == errSecSuccess else { throw failure(insertStatus) }
29
+ } else if status != errSecSuccess { throw failure(status) }
30
+ }
31
+
32
+ static func delete(_ account: String) throws {
33
+ let status = SecItemDelete(baseQuery(account) as CFDictionary)
34
+ guard status == errSecSuccess || status == errSecItemNotFound else { throw failure(status) }
35
+ }
36
+
37
+ private static func baseQuery(_ account: String) -> [CFString: Any] {
38
+ [kSecClass: kSecClassGenericPassword,
39
+ kSecAttrService: (Bundle.main.bundleIdentifier ?? "typeferry") + ".typeferry",
40
+ kSecAttrAccount: account]
41
+ }
42
+
43
+ private static func failure(_ status: OSStatus) -> NSError {
44
+ NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: [NSLocalizedDescriptionKey: "Secure session storage is unavailable."])
45
+ }
46
+ }
47
+ `;
48
+ //# sourceMappingURL=keychain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.js","sourceRoot":"","sources":["../../../src/native/templates/keychain.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CvC,CAAA"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Foundation-only origin policy, also executed by the native security tests.
3
+ */
4
+ export declare const MEDIA_PERMISSION_POLICY_SWIFT: string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Foundation-only origin policy, also executed by the native security tests.
3
+ */
4
+ export const MEDIA_PERMISSION_POLICY_SWIFT = String.raw `import Foundation
5
+
6
+ struct TypeFerryMediaPolicy {
7
+ static func isTrusted(localURL: URL, requestURL: URL, isMainFrame: Bool) -> Bool {
8
+ guard isMainFrame, requestURL.user == nil, requestURL.password == nil else { return false }
9
+ return localURL.scheme?.lowercased() == requestURL.scheme?.lowercased()
10
+ && localURL.host?.lowercased() == requestURL.host?.lowercased()
11
+ && effectivePort(localURL) == effectivePort(requestURL)
12
+ }
13
+
14
+ private static func effectivePort(_ url: URL) -> Int? {
15
+ if let port = url.port { return port }
16
+ switch url.scheme?.lowercased() {
17
+ case "https": return 443
18
+ case "http": return 80
19
+ default: return nil
20
+ }
21
+ }
22
+ }
23
+ `;
24
+ //# sourceMappingURL=media-permission-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-permission-policy.js","sourceRoot":"","sources":["../../../src/native/templates/media-permission-policy.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;CAmBtD,CAAA"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Narrow Capacitor bridge: pinned backend requests, Keychain, system auth and sharing.
3
+ */
4
+ export declare const NATIVE_PLUGIN_SWIFT: string;
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Narrow Capacitor bridge: pinned backend requests, Keychain, system auth and sharing.
3
+ */
4
+ export const NATIVE_PLUGIN_SWIFT = String.raw `import AuthenticationServices
5
+ import Capacitor
6
+ import UIKit
7
+ import WebKit
8
+
9
+ @objc(TypeFerryNativePlugin)
10
+ public class TypeFerryNativePlugin: CAPPlugin, CAPBridgedPlugin, ASWebAuthenticationPresentationContextProviding {
11
+ public let identifier = "TypeFerryNativePlugin"
12
+ public let jsName = "TypeFerryNative"
13
+ public let pluginMethods: [CAPPluginMethod] = [
14
+ CAPPluginMethod(name: "request", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "getSession", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "setSession", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "clearSession", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "authenticate", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "acquireWakeLock", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "releaseWakeLock", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "getState", returnType: CAPPluginReturnPromise),
22
+ CAPPluginMethod(name: "beginFile", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "appendFile", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "finishFile", returnType: CAPPluginReturnPromise),
25
+ CAPPluginMethod(name: "cancelFile", returnType: CAPPluginReturnPromise),
26
+ CAPPluginMethod(name: "shareFile", returnType: CAPPluginReturnPromise)
27
+ ]
28
+ private var http: TypeFerryHTTPSession?
29
+ private var initializationError: Error?
30
+ private var authentication: ASWebAuthenticationSession?
31
+ private var sharing = false
32
+ private let fileStaging = TypeFerryFileStaging()
33
+ private var stagingExpiry: DispatchWorkItem?
34
+ private static let stagingTimeout: TimeInterval = 600
35
+ private static let wakeLocks = TypeFerryWakeLocks { enabled in UIApplication.shared.isIdleTimerDisabled = enabled }
36
+ private let wakeLockOwner = UUID().uuidString
37
+ private var observers: [NSObjectProtocol] = []
38
+
39
+ public override func load() {
40
+ do {
41
+ guard let origin = getConfig().getString("backendOrigin"), let url = URL(string: origin),
42
+ url.host != nil, url.user == nil, url.password == nil, url.query == nil, url.fragment == nil,
43
+ url.path.isEmpty || url.path == "/" else {
44
+ throw TypeFerryHTTPSession.failure("A valid backend origin is required.")
45
+ }
46
+ var permitted = url.scheme == "https"
47
+ #if DEBUG
48
+ permitted = permitted || (url.scheme == "http" && ["localhost", "127.0.0.1", "::1"].contains(url.host ?? ""))
49
+ #endif
50
+ guard permitted else { throw TypeFerryHTTPSession.failure("The backend requires HTTPS.") }
51
+ http = try TypeFerryHTTPSession(backendURL: url)
52
+ } catch { initializationError = error }
53
+
54
+ observers.append(NotificationCenter.default.addObserver(forName: .capacitorDecidePolicyForNavigationAction, object: nil, queue: .main) { [weak self] notification in
55
+ guard let self = self, let webView = self.bridge?.webView,
56
+ let navigation = notification.object as? WKNavigationAction,
57
+ navigation.sourceFrame.webView === webView,
58
+ navigation.targetFrame?.isMainFrame == true else { return }
59
+ Self.wakeLocks.removeOwner(self.wakeLockOwner)
60
+ })
61
+
62
+ for name in [UIApplication.didBecomeActiveNotification, UIApplication.didEnterBackgroundNotification] {
63
+ observers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
64
+ guard let self = self else { return }
65
+ let active = UIApplication.shared.applicationState == .active
66
+ Self.wakeLocks.setActive(owner: self.wakeLockOwner, active: active)
67
+ self.notifyListeners("appStateChange", data: ["isActive": active])
68
+ })
69
+ }
70
+ }
71
+
72
+ deinit {
73
+ let owner = wakeLockOwner
74
+ DispatchQueue.main.async { TypeFerryNativePlugin.wakeLocks.removeOwner(owner) }
75
+ for observer in observers { NotificationCenter.default.removeObserver(observer) }
76
+ authentication?.cancel()
77
+ stagingExpiry?.cancel()
78
+ }
79
+
80
+ @objc func request(_ call: CAPPluginCall) {
81
+ do {
82
+ let request = try makeRequest(call)
83
+ guard let http = http else { throw initializationError ?? TypeFerryHTTPSession.failure("Native transport is unavailable.") }
84
+ http.request(request) { result in
85
+ switch result {
86
+ case .success(let (data, response)):
87
+ guard let body = String(data: data, encoding: .utf8) else { call.reject("Native RPC responses must be UTF-8."); return }
88
+ var headers: [String: String] = [:]
89
+ for (key, value) in response.allHeaderFields {
90
+ guard let name = key as? String, name.lowercased() != "set-cookie" else { continue }
91
+ headers[name] = String(describing: value)
92
+ }
93
+ call.resolve(["status": response.statusCode, "statusText": HTTPURLResponse.localizedString(forStatusCode: response.statusCode), "headers": headers, "body": body])
94
+ case .failure(let error): call.reject("The backend request failed.", nil, error)
95
+ }
96
+ }
97
+ } catch { call.reject("The backend request was rejected.", nil, error) }
98
+ }
99
+
100
+ @objc func getSession(_ call: CAPPluginCall) {
101
+ do {
102
+ let data = try TypeFerryKeychain.read("session")
103
+ call.resolve(["value": data.flatMap { String(data: $0, encoding: .utf8) } as Any? ?? NSNull()])
104
+ } catch { call.reject("Unable to read the secure session.", nil, error) }
105
+ }
106
+
107
+ @objc func setSession(_ call: CAPPluginCall) {
108
+ guard let value = call.getString("value"), let data = value.data(using: .utf8), data.count <= 65_536 else {
109
+ call.reject("A session value of at most 64 KiB is required."); return
110
+ }
111
+ do { try TypeFerryKeychain.write("session", data: data); call.resolve() }
112
+ catch { call.reject("Unable to store the secure session.", nil, error) }
113
+ }
114
+
115
+ @objc func clearSession(_ call: CAPPluginCall) {
116
+ do {
117
+ try http?.clearCookies()
118
+ try TypeFerryKeychain.delete("session")
119
+ call.resolve()
120
+ } catch { call.reject("Unable to clear the secure session.", nil, error) }
121
+ }
122
+
123
+ @objc func acquireWakeLock(_ call: CAPPluginCall) {
124
+ DispatchQueue.main.async {
125
+ guard UIApplication.shared.applicationState == .active else {
126
+ call.reject("The application must be active to keep the screen awake."); return
127
+ }
128
+ Self.wakeLocks.setActive(owner: self.wakeLockOwner, active: true)
129
+ call.resolve(["id": Self.wakeLocks.acquire(owner: self.wakeLockOwner)])
130
+ }
131
+ }
132
+
133
+ @objc func releaseWakeLock(_ call: CAPPluginCall) {
134
+ guard let id = call.getString("id") else { call.reject("A wake lock identifier is required."); return }
135
+ DispatchQueue.main.async {
136
+ Self.wakeLocks.release(id: id, owner: self.wakeLockOwner)
137
+ call.resolve()
138
+ }
139
+ }
140
+
141
+ @objc func getState(_ call: CAPPluginCall) {
142
+ DispatchQueue.main.async { call.resolve(["isActive": UIApplication.shared.applicationState == .active]) }
143
+ }
144
+
145
+ @objc func authenticate(_ call: CAPPluginCall) {
146
+ guard let urlValue = call.getString("url"), let url = URL(string: urlValue), http?.accepts(url) == true,
147
+ let scheme = call.getString("callbackScheme"), scheme == getConfig().getString("callbackScheme"),
148
+ !scheme.isEmpty, !["http", "https", "capacitor", "file", "javascript"].contains(scheme.lowercased()) else {
149
+ call.reject("Untrusted authentication URL or callback scheme."); return
150
+ }
151
+ DispatchQueue.main.async {
152
+ guard self.authentication == nil else { call.reject("Authentication is already running."); return }
153
+ let session = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) { [weak self] callback, error in
154
+ DispatchQueue.main.async {
155
+ self?.authentication = nil
156
+ guard let callback = callback, callback.scheme == scheme else {
157
+ call.reject("Authentication did not complete.", nil, error); return
158
+ }
159
+ call.resolve(["url": callback.absoluteString])
160
+ }
161
+ }
162
+ session.presentationContextProvider = self
163
+ self.authentication = session
164
+ if !session.start() { self.authentication = nil; call.reject("Authentication could not start.") }
165
+ }
166
+ }
167
+
168
+ public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
169
+ bridge?.viewController?.view.window ?? ASPresentationAnchor()
170
+ }
171
+
172
+ @objc func beginFile(_ call: CAPPluginCall) {
173
+ guard let name = call.getString("fileName"), let size = call.getInt("size") else {
174
+ call.reject("A file name and size are required."); return
175
+ }
176
+ DispatchQueue.main.async {
177
+ guard !self.sharing else { call.reject("File sharing is already running."); return }
178
+ do {
179
+ let id = try self.fileStaging.begin(fileName: name, size: size)
180
+ self.sharing = true
181
+ self.extendStagingDeadline(id: id)
182
+ call.resolve(["id": id])
183
+ } catch { call.reject("The file could not be prepared.", nil, error) }
184
+ }
185
+ }
186
+
187
+ @objc func appendFile(_ call: CAPPluginCall) {
188
+ guard let id = call.getString("id"), let offset = call.getInt("offset"), let base64 = call.getString("base64") else {
189
+ call.reject("A file identifier, offset and chunk are required."); return
190
+ }
191
+ DispatchQueue.main.async {
192
+ do {
193
+ try self.fileStaging.append(id: id, offset: offset, base64: base64)
194
+ self.extendStagingDeadline(id: id)
195
+ call.resolve()
196
+ } catch {
197
+ self.cancelStaging(id: id)
198
+ call.reject("The file chunk could not be written.", nil, error)
199
+ }
200
+ }
201
+ }
202
+
203
+ @objc func finishFile(_ call: CAPPluginCall) {
204
+ guard let id = call.getString("id") else { call.reject("A file identifier is required."); return }
205
+ DispatchQueue.main.async {
206
+ do {
207
+ let file = try self.fileStaging.finish(id: id)
208
+ self.stagingExpiry?.cancel()
209
+ self.stagingExpiry = nil
210
+ self.presentShare(file.url, fileName: file.fileName, call: call)
211
+ } catch {
212
+ self.cancelStaging(id: id)
213
+ call.reject("The file could not be completed.", nil, error)
214
+ }
215
+ }
216
+ }
217
+
218
+ @objc func cancelFile(_ call: CAPPluginCall) {
219
+ guard let id = call.getString("id") else { call.reject("A file identifier is required."); return }
220
+ DispatchQueue.main.async {
221
+ self.cancelStaging(id: id)
222
+ call.resolve()
223
+ }
224
+ }
225
+
226
+ private func cancelStaging(id: String) {
227
+ guard fileStaging.contains(id: id) else { return }
228
+ fileStaging.cancel(id: id)
229
+ stagingExpiry?.cancel()
230
+ stagingExpiry = nil
231
+ sharing = false
232
+ }
233
+
234
+ private func extendStagingDeadline(id: String) {
235
+ stagingExpiry?.cancel()
236
+ let expiry = DispatchWorkItem { [weak self] in self?.cancelStaging(id: id) }
237
+ stagingExpiry = expiry
238
+ DispatchQueue.main.asyncAfter(deadline: .now() + Self.stagingTimeout, execute: expiry)
239
+ }
240
+
241
+ @objc func shareFile(_ call: CAPPluginCall) {
242
+ do {
243
+ var request = try makeRequest(call)
244
+ request.httpMethod = "GET"
245
+ request.httpBody = nil
246
+ guard let fileName = call.getString("fileName"), !fileName.isEmpty,
247
+ fileName == URL(fileURLWithPath: fileName).lastPathComponent,
248
+ ![".", ".."].contains(fileName), !fileName.contains("\\"),
249
+ let http = http else { throw TypeFerryHTTPSession.failure("A safe file name is required.") }
250
+ DispatchQueue.main.async {
251
+ guard !self.sharing else { call.reject("File sharing is already running."); return }
252
+ self.sharing = true
253
+ http.download(request) { result in
254
+ DispatchQueue.main.async {
255
+ switch result {
256
+ case .failure(let error): self.sharing = false; call.reject("The file could not be downloaded.", nil, error)
257
+ case .success(let temporary): self.presentShare(temporary, fileName: fileName, call: call)
258
+ }
259
+ }
260
+ }
261
+ }
262
+ } catch { call.reject("The download was rejected.", nil, error) }
263
+ }
264
+
265
+ private func presentShare(_ temporary: URL, fileName: String, call: CAPPluginCall) {
266
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
267
+ do {
268
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
269
+ let destination = directory.appendingPathComponent(fileName)
270
+ try FileManager.default.moveItem(at: temporary, to: destination)
271
+ guard let controller = bridge?.viewController, controller.presentedViewController == nil else {
272
+ throw TypeFerryHTTPSession.failure("Close the active native dialog before sharing.")
273
+ }
274
+ let sheet = UIActivityViewController(activityItems: [destination], applicationActivities: nil)
275
+ sheet.popoverPresentationController?.sourceView = controller.view
276
+ sheet.popoverPresentationController?.sourceRect = CGRect(x: controller.view.bounds.midX, y: controller.view.bounds.midY, width: 1, height: 1)
277
+ sheet.completionWithItemsHandler = { [weak self] _, completed, _, error in
278
+ try? FileManager.default.removeItem(at: directory)
279
+ self?.sharing = false
280
+ if let error = error { call.reject("File sharing failed.", nil, error) }
281
+ else { call.resolve(["completed": completed]) }
282
+ }
283
+ controller.present(sheet, animated: true)
284
+ } catch {
285
+ try? FileManager.default.removeItem(at: temporary)
286
+ try? FileManager.default.removeItem(at: directory)
287
+ sharing = false
288
+ call.reject("The file could not be shared.", nil, error)
289
+ }
290
+ }
291
+
292
+ private func makeRequest(_ call: CAPPluginCall) throws -> URLRequest {
293
+ guard let value = call.getString("url"), let url = URL(string: value), http?.accepts(url) == true else {
294
+ throw initializationError ?? TypeFerryHTTPSession.failure("Untrusted backend URL.")
295
+ }
296
+ var request = URLRequest(url: url)
297
+ let method = call.getString("method") ?? "GET"
298
+ guard ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].contains(method) else {
299
+ throw TypeFerryHTTPSession.failure("Unsupported HTTP method.")
300
+ }
301
+ request.httpMethod = method
302
+ request.httpBody = call.getString("body")?.data(using: .utf8)
303
+ for (name, value) in call.getObject("headers") ?? [:] {
304
+ guard let value = value as? String, !["cookie", "host", "origin", "content-length"].contains(name.lowercased()),
305
+ !name.contains("\r"), !name.contains("\n"), !value.contains("\r"), !value.contains("\n") else {
306
+ throw TypeFerryHTTPSession.failure("Unsupported request header.")
307
+ }
308
+ request.setValue(value, forHTTPHeaderField: name)
309
+ }
310
+ return request
311
+ }
312
+ }
313
+ `;
314
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/native/templates/plugin.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqT5C,CAAA"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Process-wide ownership keeps independent scene/presentation leases isolated.
3
+ */
4
+ export declare const WAKE_LOCKS_SWIFT: string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Process-wide ownership keeps independent scene/presentation leases isolated.
3
+ */
4
+ export const WAKE_LOCKS_SWIFT = String.raw `import Foundation
5
+
6
+ final class TypeFerryWakeLocks {
7
+ private var leases: [String: String] = [:]
8
+ private var activeOwners: Set<String> = []
9
+ private var enabled = false
10
+ private let apply: (Bool) -> Void
11
+
12
+ init(apply: @escaping (Bool) -> Void) { self.apply = apply }
13
+
14
+ func setActive(owner: String, active: Bool) {
15
+ if active { activeOwners.insert(owner) }
16
+ else { activeOwners.remove(owner) }
17
+ update()
18
+ }
19
+
20
+ func acquire(owner: String) -> String {
21
+ let id = UUID().uuidString
22
+ leases[id] = owner
23
+ update()
24
+ return id
25
+ }
26
+
27
+ func release(id: String, owner: String) {
28
+ guard leases[id] == owner else { return }
29
+ leases.removeValue(forKey: id)
30
+ update()
31
+ }
32
+
33
+ func removeOwner(_ owner: String) {
34
+ leases = leases.filter { $0.value != owner }
35
+ activeOwners.remove(owner)
36
+ update()
37
+ }
38
+
39
+ private func update() {
40
+ let next = leases.values.contains { activeOwners.contains($0) }
41
+ guard next != enabled else { return }
42
+ enabled = next
43
+ apply(next)
44
+ }
45
+ }
46
+ `;
47
+ //# sourceMappingURL=wake-locks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake-locks.js","sourceRoot":"","sources":["../../../src/native/templates/wake-locks.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CzC,CAAA"}
@@ -0,0 +1,9 @@
1
+ export interface NativeWakeLock {
2
+ release(): Promise<void>;
3
+ }
4
+ /**
5
+ * Keeps the screen awake while the owning native application is foregrounded.
6
+ * Release on presentation exit/unmount. A late acquisition must also be released
7
+ * if its component was disposed while the native promise was pending.
8
+ */
9
+ export declare function acquireNativeWakeLock(): Promise<NativeWakeLock>;
@@ -0,0 +1,17 @@
1
+ import { TypeFerryNative } from './index.js';
2
+ /**
3
+ * Keeps the screen awake while the owning native application is foregrounded.
4
+ * Release on presentation exit/unmount. A late acquisition must also be released
5
+ * if its component was disposed while the native promise was pending.
6
+ */
7
+ export async function acquireNativeWakeLock() {
8
+ const { id } = await TypeFerryNative.acquireWakeLock();
9
+ let release;
10
+ return {
11
+ release() {
12
+ release ??= TypeFerryNative.releaseWakeLock({ id });
13
+ return release;
14
+ },
15
+ };
16
+ }
17
+ //# sourceMappingURL=wake-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake-lock.js","sourceRoot":"","sources":["../../src/native/wake-lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAMzC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,MAAM,EAAE,EAAE,EAAE,GAAG,MAAM,eAAe,CAAC,eAAe,EAAE,CAAA;IACtD,IAAI,OAAkC,CAAA;IAEtC,OAAO;QACL,OAAO;YACL,OAAO,KAAK,eAAe,CAAC,eAAe,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;YACnD,OAAO,OAAO,CAAA;QAChB,CAAC;KACF,CAAA;AACH,CAAC"}