typeferry 0.9.1 → 0.11.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 (89) hide show
  1. package/README.md +9 -0
  2. package/dist/application/build.d.ts +2 -2
  3. package/dist/application/build.js +8 -2
  4. package/dist/application/build.js.map +1 -1
  5. package/dist/application/cli-arguments.d.ts +6 -1
  6. package/dist/application/cli-arguments.js +9 -1
  7. package/dist/application/cli-arguments.js.map +1 -1
  8. package/dist/application/config.d.ts +47 -1
  9. package/dist/application/config.js +88 -2
  10. package/dist/application/config.js.map +1 -1
  11. package/dist/application/native-project.d.ts +16 -0
  12. package/dist/application/native-project.js +82 -0
  13. package/dist/application/native-project.js.map +1 -0
  14. package/dist/application/native.d.ts +2 -0
  15. package/dist/application/native.js +86 -0
  16. package/dist/application/native.js.map +1 -0
  17. package/dist/application/runtime.d.ts +13 -0
  18. package/dist/application/runtime.js +15 -0
  19. package/dist/application/runtime.js.map +1 -0
  20. package/dist/application/server-build.js +1 -0
  21. package/dist/application/server-build.js.map +1 -1
  22. package/dist/application/vite-config.d.ts +2 -2
  23. package/dist/application/vite-config.js +16 -2
  24. package/dist/application/vite-config.js.map +1 -1
  25. package/dist/auth/client/index.d.ts +4 -0
  26. package/dist/auth/client/index.js +2 -0
  27. package/dist/auth/client/index.js.map +1 -1
  28. package/dist/auth/client/native-http.d.ts +24 -0
  29. package/dist/auth/client/native-http.js +19 -0
  30. package/dist/auth/client/native-http.js.map +1 -0
  31. package/dist/auth/client/native-session.d.ts +34 -0
  32. package/dist/auth/client/native-session.js +65 -0
  33. package/dist/auth/client/native-session.js.map +1 -0
  34. package/dist/auth/index.d.ts +6 -0
  35. package/dist/auth/index.js +3 -0
  36. package/dist/auth/index.js.map +1 -1
  37. package/dist/auth/server/native-handoff.d.ts +69 -0
  38. package/dist/auth/server/native-handoff.js +65 -0
  39. package/dist/auth/server/native-handoff.js.map +1 -0
  40. package/dist/cli/run.js +6 -1
  41. package/dist/cli/run.js.map +1 -1
  42. package/dist/client/backend-origin.d.ts +17 -0
  43. package/dist/client/backend-origin.js +25 -0
  44. package/dist/client/backend-origin.js.map +1 -0
  45. package/dist/client/client-http.js +8 -3
  46. package/dist/client/client-http.js.map +1 -1
  47. package/dist/client/client-socket.js +6 -1
  48. package/dist/client/client-socket.js.map +1 -1
  49. package/dist/client/client.d.ts +9 -0
  50. package/dist/client/client.js.map +1 -1
  51. package/dist/client/index.d.ts +1 -0
  52. package/dist/client/index.js +1 -0
  53. package/dist/client/index.js.map +1 -1
  54. package/dist/ejson/clone.js +7 -1
  55. package/dist/ejson/clone.js.map +1 -1
  56. package/dist/native/index.d.ts +76 -0
  57. package/dist/native/index.js +5 -0
  58. package/dist/native/index.js.map +1 -0
  59. package/dist/native/share-file.d.ts +18 -0
  60. package/dist/native/share-file.js +41 -0
  61. package/dist/native/share-file.js.map +1 -0
  62. package/dist/native/templates/bridge-view-controller.d.ts +4 -0
  63. package/dist/native/templates/bridge-view-controller.js +86 -0
  64. package/dist/native/templates/bridge-view-controller.js.map +1 -0
  65. package/dist/native/templates/file-staging.d.ts +4 -0
  66. package/dist/native/templates/file-staging.js +77 -0
  67. package/dist/native/templates/file-staging.js.map +1 -0
  68. package/dist/native/templates/http-session.d.ts +4 -0
  69. package/dist/native/templates/http-session.js +117 -0
  70. package/dist/native/templates/http-session.js.map +1 -0
  71. package/dist/native/templates/index.d.ts +4 -0
  72. package/dist/native/templates/index.js +20 -0
  73. package/dist/native/templates/index.js.map +1 -0
  74. package/dist/native/templates/keychain.d.ts +4 -0
  75. package/dist/native/templates/keychain.js +48 -0
  76. package/dist/native/templates/keychain.js.map +1 -0
  77. package/dist/native/templates/media-permission-policy.d.ts +4 -0
  78. package/dist/native/templates/media-permission-policy.js +24 -0
  79. package/dist/native/templates/media-permission-policy.js.map +1 -0
  80. package/dist/native/templates/plugin.d.ts +4 -0
  81. package/dist/native/templates/plugin.js +314 -0
  82. package/dist/native/templates/plugin.js.map +1 -0
  83. package/dist/native/templates/wake-locks.d.ts +4 -0
  84. package/dist/native/templates/wake-locks.js +47 -0
  85. package/dist/native/templates/wake-locks.js.map +1 -0
  86. package/dist/native/wake-lock.d.ts +9 -0
  87. package/dist/native/wake-lock.js +17 -0
  88. package/dist/native/wake-lock.js.map +1 -0
  89. package/package.json +18 -2
@@ -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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typeferry",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Type-safe real-time RPC framework for TypeScript",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -71,6 +71,14 @@
71
71
  "./*": {
72
72
  "types": "./dist/*.d.ts",
73
73
  "import": "./dist/*.js"
74
+ },
75
+ "./native": {
76
+ "types": "./dist/native/index.d.ts",
77
+ "import": "./dist/native/index.js"
78
+ },
79
+ "./auth/client": {
80
+ "types": "./dist/auth/client/index.d.ts",
81
+ "import": "./dist/auth/client/index.js"
74
82
  }
75
83
  },
76
84
  "scripts": {
@@ -120,6 +128,7 @@
120
128
  "zod": "^4.4.3"
121
129
  },
122
130
  "peerDependencies": {
131
+ "@capacitor/core": ">=8 <9",
123
132
  "@react-oauth/google": ">=0.12",
124
133
  "mongodb": ">=6",
125
134
  "react": ">=18"
@@ -133,9 +142,15 @@
133
142
  },
134
143
  "@react-oauth/google": {
135
144
  "optional": true
145
+ },
146
+ "@capacitor/core": {
147
+ "optional": true
136
148
  }
137
149
  },
138
150
  "devDependencies": {
151
+ "@capacitor/cli": "^8.5.1",
152
+ "@capacitor/core": "^8.5.1",
153
+ "@capacitor/ios": "^8.5.1",
139
154
  "@eslint/js": "^10.0.1",
140
155
  "@react-oauth/google": "^0.12.2",
141
156
  "@testing-library/react": "^16.3.2",
@@ -167,7 +182,8 @@
167
182
  "nanoid": "^6.0.1",
168
183
  "postcss": "^8.5.26",
169
184
  "qs": "^6.15.3",
170
- "undici": "^7.29.0"
185
+ "undici": "^7.29.0",
186
+ "uuid": "^11.1.1"
171
187
  },
172
188
  "engines": {
173
189
  "node": ">=24.19.0 <27",