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.
- package/README.md +9 -0
- package/dist/application/build.d.ts +2 -2
- package/dist/application/build.js +8 -2
- package/dist/application/build.js.map +1 -1
- package/dist/application/cli-arguments.d.ts +6 -1
- package/dist/application/cli-arguments.js +9 -1
- package/dist/application/cli-arguments.js.map +1 -1
- package/dist/application/config.d.ts +47 -1
- package/dist/application/config.js +88 -2
- package/dist/application/config.js.map +1 -1
- package/dist/application/native-project.d.ts +16 -0
- package/dist/application/native-project.js +82 -0
- package/dist/application/native-project.js.map +1 -0
- package/dist/application/native.d.ts +2 -0
- package/dist/application/native.js +86 -0
- package/dist/application/native.js.map +1 -0
- package/dist/application/runtime.d.ts +13 -0
- package/dist/application/runtime.js +15 -0
- package/dist/application/runtime.js.map +1 -0
- package/dist/application/server-build.js +1 -0
- package/dist/application/server-build.js.map +1 -1
- package/dist/application/vite-config.d.ts +2 -2
- package/dist/application/vite-config.js +16 -2
- package/dist/application/vite-config.js.map +1 -1
- package/dist/auth/client/index.d.ts +4 -0
- package/dist/auth/client/index.js +2 -0
- package/dist/auth/client/index.js.map +1 -1
- package/dist/auth/client/native-http.d.ts +24 -0
- package/dist/auth/client/native-http.js +19 -0
- package/dist/auth/client/native-http.js.map +1 -0
- package/dist/auth/client/native-session.d.ts +34 -0
- package/dist/auth/client/native-session.js +65 -0
- package/dist/auth/client/native-session.js.map +1 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +3 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/server/native-handoff.d.ts +69 -0
- package/dist/auth/server/native-handoff.js +65 -0
- package/dist/auth/server/native-handoff.js.map +1 -0
- package/dist/cli/run.js +6 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/client/backend-origin.d.ts +17 -0
- package/dist/client/backend-origin.js +25 -0
- package/dist/client/backend-origin.js.map +1 -0
- package/dist/client/client-http.js +8 -3
- package/dist/client/client-http.js.map +1 -1
- package/dist/client/client-socket.js +6 -1
- package/dist/client/client-socket.js.map +1 -1
- package/dist/client/client.d.ts +9 -0
- package/dist/client/client.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/ejson/clone.js +7 -1
- package/dist/ejson/clone.js.map +1 -1
- package/dist/native/index.d.ts +76 -0
- package/dist/native/index.js +5 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/share-file.d.ts +18 -0
- package/dist/native/share-file.js +41 -0
- package/dist/native/share-file.js.map +1 -0
- package/dist/native/templates/bridge-view-controller.d.ts +4 -0
- package/dist/native/templates/bridge-view-controller.js +86 -0
- package/dist/native/templates/bridge-view-controller.js.map +1 -0
- package/dist/native/templates/file-staging.d.ts +4 -0
- package/dist/native/templates/file-staging.js +77 -0
- package/dist/native/templates/file-staging.js.map +1 -0
- package/dist/native/templates/http-session.d.ts +4 -0
- package/dist/native/templates/http-session.js +117 -0
- package/dist/native/templates/http-session.js.map +1 -0
- package/dist/native/templates/index.d.ts +4 -0
- package/dist/native/templates/index.js +20 -0
- package/dist/native/templates/index.js.map +1 -0
- package/dist/native/templates/keychain.d.ts +4 -0
- package/dist/native/templates/keychain.js +48 -0
- package/dist/native/templates/keychain.js.map +1 -0
- package/dist/native/templates/media-permission-policy.d.ts +4 -0
- package/dist/native/templates/media-permission-policy.js +24 -0
- package/dist/native/templates/media-permission-policy.js.map +1 -0
- package/dist/native/templates/plugin.d.ts +4 -0
- package/dist/native/templates/plugin.js +314 -0
- package/dist/native/templates/plugin.js.map +1 -0
- package/dist/native/templates/wake-locks.d.ts +4 -0
- package/dist/native/templates/wake-locks.js +47 -0
- package/dist/native/templates/wake-locks.js.map +1 -0
- package/dist/native/wake-lock.d.ts +9 -0
- package/dist/native/wake-lock.js +17 -0
- package/dist/native/wake-lock.js.map +1 -0
- 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,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,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,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.
|
|
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",
|