@dvai-bridge/ios 4.0.0 → 4.0.1
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/Package.swift +104 -104
- package/ios/Sources/DVAIBridge/BackendKind.swift +23 -23
- package/ios/Sources/DVAIBridge/BoundServer.swift +46 -46
- package/ios/Sources/DVAIBridge/DVAIBridge.swift +658 -658
- package/ios/Sources/DVAIBridge/DVAIBridgeConfig.swift +86 -86
- package/ios/Sources/DVAIBridge/DVAIBridgeError.swift +33 -33
- package/ios/Sources/DVAIBridge/Internal/BackendSelector.swift +59 -59
- package/ios/Sources/DVAIBridge/Internal/ProgressBroadcaster.swift +84 -84
- package/ios/Sources/DVAIBridge/License/Audience.swift +133 -133
- package/ios/Sources/DVAIBridge/License/Discovery.swift +164 -164
- package/ios/Sources/DVAIBridge/License/LicenseValidator.swift +392 -392
- package/ios/Sources/DVAIBridge/License/PublicKeys.swift +114 -114
- package/ios/Sources/DVAIBridge/License/Types.swift +195 -195
- package/ios/Sources/DVAIBridge/Offload/OffloadConfig.swift +118 -118
- package/ios/Sources/DVAIBridge/ProgressEvent.swift +34 -34
- package/ios/Sources/DVAICoreMLCore/CoreMLBackendError.swift +19 -19
- package/ios/Sources/DVAICoreMLCore/CoreMLHandlers.swift +123 -123
- package/ios/Sources/DVAICoreMLCore/CoreMLPluginState.swift +130 -130
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLEngine.swift +137 -137
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLGenerator.swift +108 -108
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLSampler.swift +96 -96
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLTokenizer.swift +69 -69
- package/ios/Tests/DVAIBridgeTests/BackendSelectorTests.swift +53 -53
- package/ios/Tests/DVAIBridgeTests/CoreMLEngineTests.swift +18 -18
- package/ios/Tests/DVAIBridgeTests/CoreMLGeneratorShapeTests.swift +11 -11
- package/ios/Tests/DVAIBridgeTests/CoreMLHandlersTests.swift +32 -32
- package/ios/Tests/DVAIBridgeTests/CoreMLPluginStateTests.swift +41 -41
- package/ios/Tests/DVAIBridgeTests/CoreMLSamplerTests.swift +40 -40
- package/ios/Tests/DVAIBridgeTests/CoreMLTokenizerTests.swift +19 -19
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeAPIShapeTests.swift +37 -37
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeConfigTests.swift +52 -52
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeErrorTests.swift +33 -33
- package/ios/Tests/DVAIBridgeTests/LicenseValidatorTests.swift +658 -658
- package/ios/Tests/DVAIBridgeTests/ProgressBroadcasterTests.swift +69 -69
- package/ios/Tests/DVAIBridgeTests/ProgressEventTests.swift +25 -25
- package/ios/Tests/DVAIBridgeTests/ReactiveStateTests.swift +45 -45
- package/ios/Tests/DVAIBridgeTests/RealModelIntegrationTest.swift +385 -359
- package/package.json +3 -4
- package/DVAIBridge.podspec +0 -120
- package/LICENSE +0 -51
- package/README.md +0 -199
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Runtime audience + platform + dev-mode detection for the iOS SDK.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `packages/dvai-bridge-core/src/license/audience.ts`. The
|
|
5
|
-
* semantics are identical; only the platform APIs differ.
|
|
6
|
-
*
|
|
7
|
-
* - audience = `Bundle.main.bundleIdentifier` (may be `nil` in some
|
|
8
|
-
* unit-test hosts; null is treated like the JS side does
|
|
9
|
-
* — only `"*"` aud entries match)
|
|
10
|
-
* - platform = always `.ios` from this SDK (the Capacitor consumers
|
|
11
|
-
* use the Capacitor SDK, which detects `.capacitor`
|
|
12
|
-
* from JS-side; this iOS SDK is for native iOS apps)
|
|
13
|
-
* - dev mode = DEBUG build OR simulator OR DVAI_FORCE_DEV=1
|
|
14
|
-
* (DVAI_FORCE_PROD=1 overrides DEBUG / simulator)
|
|
15
|
-
*/
|
|
16
|
-
import Foundation
|
|
17
|
-
|
|
18
|
-
/// Detect the current SDK platform identifier. Always `.ios` on iOS —
|
|
19
|
-
/// Capacitor consumers use a different SDK (the Capacitor plugin's
|
|
20
|
-
/// JS-side validator detects `capacitor`). React Native consumers
|
|
21
|
-
/// likewise use a different SDK.
|
|
22
|
-
public func detectPlatform() -> DvaiPlatform {
|
|
23
|
-
return .ios
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/// Read an environment variable via C's `getenv` so we observe live
|
|
27
|
-
/// updates (`setenv` in tests). `ProcessInfo.environment` caches its
|
|
28
|
-
/// dict on Apple's Foundation, which makes per-test env mutation
|
|
29
|
-
/// unreliable.
|
|
30
|
-
internal func envVar(_ name: String) -> String? {
|
|
31
|
-
guard let cstr = getenv(name) else { return nil }
|
|
32
|
-
let value = String(cString: cstr)
|
|
33
|
-
return value.isEmpty ? nil : value
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/// Detect the current audience string the license must bind. On iOS
|
|
37
|
-
/// this is the running app's bundle identifier (e.g. `"com.acme.app"`).
|
|
38
|
-
///
|
|
39
|
-
/// Returns `nil` only when no bundle identifier is available — uncommon
|
|
40
|
-
/// in production, but possible in some SwiftPM test hosts. Null is
|
|
41
|
-
/// handled by the validator: it matches only the `"*"` aud entry.
|
|
42
|
-
///
|
|
43
|
-
/// An explicit `DVAI_AUDIENCE` environment variable overrides the bundle
|
|
44
|
-
/// id (matches the JS-side override; mostly useful for tests).
|
|
45
|
-
public func detectAudience() -> String? {
|
|
46
|
-
if let override = envVar("DVAI_AUDIENCE") {
|
|
47
|
-
return override
|
|
48
|
-
}
|
|
49
|
-
if let bundleId = Bundle.main.bundleIdentifier, !bundleId.isEmpty {
|
|
50
|
-
return bundleId
|
|
51
|
-
}
|
|
52
|
-
return nil
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/// Detect whether the SDK is running in a developer environment where
|
|
56
|
-
/// license enforcement should be bypassed. The bypass list is
|
|
57
|
-
/// intentionally generous: blocking a developer mid-Xcode-run with a
|
|
58
|
-
/// license-not-found error would be hostile.
|
|
59
|
-
///
|
|
60
|
-
/// Order (first match wins):
|
|
61
|
-
/// 1. `DVAI_FORCE_PROD=1` → force production (overrides DEBUG +
|
|
62
|
-
/// simulator). The host app's CI sets this to exercise license
|
|
63
|
-
/// enforcement in non-RELEASE builds.
|
|
64
|
-
/// 2. `DVAI_FORCE_DEV=1` → force dev. Explicit operator opt-in.
|
|
65
|
-
/// 3. `#if DEBUG` → dev. Compile-time check — the DEBUG configuration
|
|
66
|
-
/// bypasses licensing entirely so the in-Xcode dev loop never
|
|
67
|
-
/// surfaces a license error.
|
|
68
|
-
/// 4. `targetEnvironment(simulator)` → dev. Same intent: a simulator
|
|
69
|
-
/// run is by construction a dev environment.
|
|
70
|
-
/// 5. otherwise → production, license required.
|
|
71
|
-
public func detectDevMode() -> (isDev: Bool, reason: String) {
|
|
72
|
-
// 1. Explicit env-var overrides (operator-level, runtime). Use
|
|
73
|
-
// `envVar` (libc getenv) so DVAI_FORCE_* mutated mid-process —
|
|
74
|
-
// e.g. by tests — is picked up.
|
|
75
|
-
let forceProd = envVar("DVAI_FORCE_PROD")
|
|
76
|
-
if forceProd == "1" || forceProd == "true" {
|
|
77
|
-
return (false, "DVAI_FORCE_PROD set")
|
|
78
|
-
}
|
|
79
|
-
let forceDev = envVar("DVAI_FORCE_DEV")
|
|
80
|
-
if forceDev == "1" || forceDev == "true" {
|
|
81
|
-
return (true, "DVAI_FORCE_DEV set")
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 2. DEBUG configuration.
|
|
85
|
-
#if DEBUG
|
|
86
|
-
return (true, "DEBUG build configuration")
|
|
87
|
-
#else
|
|
88
|
-
// 3. Simulator (DEBUG would have already caught most simulator
|
|
89
|
-
// runs; this branch matters for a Release-configuration build
|
|
90
|
-
// happening to run on the simulator — rare, but harmless to
|
|
91
|
-
// bypass).
|
|
92
|
-
#if targetEnvironment(simulator)
|
|
93
|
-
return (true, "iOS Simulator")
|
|
94
|
-
#else
|
|
95
|
-
return (false, "production-class environment")
|
|
96
|
-
#endif
|
|
97
|
-
#endif
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/// Decide whether a license-payload `aud` entry matches the current
|
|
101
|
-
/// runtime audience. Supports exact match and `*.example.com` wildcard
|
|
102
|
-
/// matching for subdomain (here: bundle-id-suffix) binding. Returns
|
|
103
|
-
/// the matched `aud` pattern on success so it can be recorded for
|
|
104
|
-
/// audit, or `nil` on miss.
|
|
105
|
-
///
|
|
106
|
-
/// Match rules (identical to the JS-side `matchAudience`):
|
|
107
|
-
/// - `"foo"` matches `"foo"` exactly (case-insensitive)
|
|
108
|
-
/// - `"*.example.com"` matches `"example.com"` AND any
|
|
109
|
-
/// `"<sub>.example.com"`
|
|
110
|
-
/// - `"*"` matches any non-nil audience (intentionally permissive;
|
|
111
|
-
/// used for trial / site licenses that span all of a customer's
|
|
112
|
-
/// deployments)
|
|
113
|
-
///
|
|
114
|
-
/// A `nil` runtime audience matches only `"*"` entries — a build with
|
|
115
|
-
/// no bundle identifier can activate any-domain licenses but not
|
|
116
|
-
/// bundle-bound ones.
|
|
117
|
-
public func matchAudience(runtimeAudience: String?, audClaim: [String]) -> String? {
|
|
118
|
-
guard let runtime = runtimeAudience?.lowercased(), !runtime.isEmpty else {
|
|
119
|
-
return audClaim.contains("*") ? "*" : nil
|
|
120
|
-
}
|
|
121
|
-
for pattern in audClaim {
|
|
122
|
-
let p = pattern.lowercased()
|
|
123
|
-
if p == "*" { return pattern } // permissive wildcard
|
|
124
|
-
if p == runtime { return pattern } // exact match
|
|
125
|
-
if p.hasPrefix("*.") {
|
|
126
|
-
let suffix = String(p.dropFirst(2))
|
|
127
|
-
if runtime == suffix || runtime.hasSuffix("." + suffix) {
|
|
128
|
-
return pattern
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return nil
|
|
133
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* Runtime audience + platform + dev-mode detection for the iOS SDK.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `packages/dvai-bridge-core/src/license/audience.ts`. The
|
|
5
|
+
* semantics are identical; only the platform APIs differ.
|
|
6
|
+
*
|
|
7
|
+
* - audience = `Bundle.main.bundleIdentifier` (may be `nil` in some
|
|
8
|
+
* unit-test hosts; null is treated like the JS side does
|
|
9
|
+
* — only `"*"` aud entries match)
|
|
10
|
+
* - platform = always `.ios` from this SDK (the Capacitor consumers
|
|
11
|
+
* use the Capacitor SDK, which detects `.capacitor`
|
|
12
|
+
* from JS-side; this iOS SDK is for native iOS apps)
|
|
13
|
+
* - dev mode = DEBUG build OR simulator OR DVAI_FORCE_DEV=1
|
|
14
|
+
* (DVAI_FORCE_PROD=1 overrides DEBUG / simulator)
|
|
15
|
+
*/
|
|
16
|
+
import Foundation
|
|
17
|
+
|
|
18
|
+
/// Detect the current SDK platform identifier. Always `.ios` on iOS —
|
|
19
|
+
/// Capacitor consumers use a different SDK (the Capacitor plugin's
|
|
20
|
+
/// JS-side validator detects `capacitor`). React Native consumers
|
|
21
|
+
/// likewise use a different SDK.
|
|
22
|
+
public func detectPlatform() -> DvaiPlatform {
|
|
23
|
+
return .ios
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Read an environment variable via C's `getenv` so we observe live
|
|
27
|
+
/// updates (`setenv` in tests). `ProcessInfo.environment` caches its
|
|
28
|
+
/// dict on Apple's Foundation, which makes per-test env mutation
|
|
29
|
+
/// unreliable.
|
|
30
|
+
internal func envVar(_ name: String) -> String? {
|
|
31
|
+
guard let cstr = getenv(name) else { return nil }
|
|
32
|
+
let value = String(cString: cstr)
|
|
33
|
+
return value.isEmpty ? nil : value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Detect the current audience string the license must bind. On iOS
|
|
37
|
+
/// this is the running app's bundle identifier (e.g. `"com.acme.app"`).
|
|
38
|
+
///
|
|
39
|
+
/// Returns `nil` only when no bundle identifier is available — uncommon
|
|
40
|
+
/// in production, but possible in some SwiftPM test hosts. Null is
|
|
41
|
+
/// handled by the validator: it matches only the `"*"` aud entry.
|
|
42
|
+
///
|
|
43
|
+
/// An explicit `DVAI_AUDIENCE` environment variable overrides the bundle
|
|
44
|
+
/// id (matches the JS-side override; mostly useful for tests).
|
|
45
|
+
public func detectAudience() -> String? {
|
|
46
|
+
if let override = envVar("DVAI_AUDIENCE") {
|
|
47
|
+
return override
|
|
48
|
+
}
|
|
49
|
+
if let bundleId = Bundle.main.bundleIdentifier, !bundleId.isEmpty {
|
|
50
|
+
return bundleId
|
|
51
|
+
}
|
|
52
|
+
return nil
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Detect whether the SDK is running in a developer environment where
|
|
56
|
+
/// license enforcement should be bypassed. The bypass list is
|
|
57
|
+
/// intentionally generous: blocking a developer mid-Xcode-run with a
|
|
58
|
+
/// license-not-found error would be hostile.
|
|
59
|
+
///
|
|
60
|
+
/// Order (first match wins):
|
|
61
|
+
/// 1. `DVAI_FORCE_PROD=1` → force production (overrides DEBUG +
|
|
62
|
+
/// simulator). The host app's CI sets this to exercise license
|
|
63
|
+
/// enforcement in non-RELEASE builds.
|
|
64
|
+
/// 2. `DVAI_FORCE_DEV=1` → force dev. Explicit operator opt-in.
|
|
65
|
+
/// 3. `#if DEBUG` → dev. Compile-time check — the DEBUG configuration
|
|
66
|
+
/// bypasses licensing entirely so the in-Xcode dev loop never
|
|
67
|
+
/// surfaces a license error.
|
|
68
|
+
/// 4. `targetEnvironment(simulator)` → dev. Same intent: a simulator
|
|
69
|
+
/// run is by construction a dev environment.
|
|
70
|
+
/// 5. otherwise → production, license required.
|
|
71
|
+
public func detectDevMode() -> (isDev: Bool, reason: String) {
|
|
72
|
+
// 1. Explicit env-var overrides (operator-level, runtime). Use
|
|
73
|
+
// `envVar` (libc getenv) so DVAI_FORCE_* mutated mid-process —
|
|
74
|
+
// e.g. by tests — is picked up.
|
|
75
|
+
let forceProd = envVar("DVAI_FORCE_PROD")
|
|
76
|
+
if forceProd == "1" || forceProd == "true" {
|
|
77
|
+
return (false, "DVAI_FORCE_PROD set")
|
|
78
|
+
}
|
|
79
|
+
let forceDev = envVar("DVAI_FORCE_DEV")
|
|
80
|
+
if forceDev == "1" || forceDev == "true" {
|
|
81
|
+
return (true, "DVAI_FORCE_DEV set")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2. DEBUG configuration.
|
|
85
|
+
#if DEBUG
|
|
86
|
+
return (true, "DEBUG build configuration")
|
|
87
|
+
#else
|
|
88
|
+
// 3. Simulator (DEBUG would have already caught most simulator
|
|
89
|
+
// runs; this branch matters for a Release-configuration build
|
|
90
|
+
// happening to run on the simulator — rare, but harmless to
|
|
91
|
+
// bypass).
|
|
92
|
+
#if targetEnvironment(simulator)
|
|
93
|
+
return (true, "iOS Simulator")
|
|
94
|
+
#else
|
|
95
|
+
return (false, "production-class environment")
|
|
96
|
+
#endif
|
|
97
|
+
#endif
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Decide whether a license-payload `aud` entry matches the current
|
|
101
|
+
/// runtime audience. Supports exact match and `*.example.com` wildcard
|
|
102
|
+
/// matching for subdomain (here: bundle-id-suffix) binding. Returns
|
|
103
|
+
/// the matched `aud` pattern on success so it can be recorded for
|
|
104
|
+
/// audit, or `nil` on miss.
|
|
105
|
+
///
|
|
106
|
+
/// Match rules (identical to the JS-side `matchAudience`):
|
|
107
|
+
/// - `"foo"` matches `"foo"` exactly (case-insensitive)
|
|
108
|
+
/// - `"*.example.com"` matches `"example.com"` AND any
|
|
109
|
+
/// `"<sub>.example.com"`
|
|
110
|
+
/// - `"*"` matches any non-nil audience (intentionally permissive;
|
|
111
|
+
/// used for trial / site licenses that span all of a customer's
|
|
112
|
+
/// deployments)
|
|
113
|
+
///
|
|
114
|
+
/// A `nil` runtime audience matches only `"*"` entries — a build with
|
|
115
|
+
/// no bundle identifier can activate any-domain licenses but not
|
|
116
|
+
/// bundle-bound ones.
|
|
117
|
+
public func matchAudience(runtimeAudience: String?, audClaim: [String]) -> String? {
|
|
118
|
+
guard let runtime = runtimeAudience?.lowercased(), !runtime.isEmpty else {
|
|
119
|
+
return audClaim.contains("*") ? "*" : nil
|
|
120
|
+
}
|
|
121
|
+
for pattern in audClaim {
|
|
122
|
+
let p = pattern.lowercased()
|
|
123
|
+
if p == "*" { return pattern } // permissive wildcard
|
|
124
|
+
if p == runtime { return pattern } // exact match
|
|
125
|
+
if p.hasPrefix("*.") {
|
|
126
|
+
let suffix = String(p.dropFirst(2))
|
|
127
|
+
if runtime == suffix || runtime.hasSuffix("." + suffix) {
|
|
128
|
+
return pattern
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return nil
|
|
133
|
+
}
|
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* License-file discovery for the iOS SDK.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `packages/dvai-bridge-core/src/license/discovery.ts` but with
|
|
5
|
-
* iOS-native locations (Bundle resources, Documents directory, App Group
|
|
6
|
-
* containers) in place of the JS side's CWD-based filesystem walk.
|
|
7
|
-
*
|
|
8
|
-
* Priority order (first hit wins):
|
|
9
|
-
*
|
|
10
|
-
* 1. `LicenseDiscoveryOptions.token` — inline JWT string. Useful when
|
|
11
|
-
* the host app fetches its license over the network at runtime
|
|
12
|
-
* and wants to inject the result without touching disk.
|
|
13
|
-
*
|
|
14
|
-
* 2. `LicenseDiscoveryOptions.path` — explicit file path. Useful for
|
|
15
|
-
* tests or for hosts that store the .jwt outside the standard
|
|
16
|
-
* locations. If this path is set but the file isn't there or
|
|
17
|
-
* isn't readable, this is a real miss (we do NOT silently fall
|
|
18
|
-
* through to auto-discovery) — same semantics as the JS side.
|
|
19
|
-
*
|
|
20
|
-
* 3. `DVAI_LICENSE_PATH` env var. Operator-supplied path; helpful
|
|
21
|
-
* for CI / TestFlight where you don't want to ship the .jwt in
|
|
22
|
-
* the bundle. Misses fall through to (5).
|
|
23
|
-
*
|
|
24
|
-
* 4. `DVAI_LICENSE_TOKEN` env var. Inline JWT in the environment.
|
|
25
|
-
*
|
|
26
|
-
* 5. `dvai-license.jwt` resource in `Bundle.main`. The dev-friendly
|
|
27
|
-
* happy path: add the file to the app's "Copy Bundle Resources"
|
|
28
|
-
* phase, the SDK picks it up automatically.
|
|
29
|
-
*
|
|
30
|
-
* 6. `Application Support/dvai-bridge/dvai-license.jwt` in the app's
|
|
31
|
-
* sandbox. Useful for licenses fetched after install (e.g. via
|
|
32
|
-
* an in-app purchase flow).
|
|
33
|
-
*
|
|
34
|
-
* 7. `Documents/dvai-license.jwt`. Last-resort fallback — visible
|
|
35
|
-
* via iTunes File Sharing if the app opts in, so handy for
|
|
36
|
-
* side-loading a license during development.
|
|
37
|
-
*
|
|
38
|
-
* Returning `nil` means "no license token found"; the validator treats
|
|
39
|
-
* that as the free-tier case (after the dev-mode bypass).
|
|
40
|
-
*/
|
|
41
|
-
import Foundation
|
|
42
|
-
|
|
43
|
-
/// Default filename the SDK looks for in Bundle / Documents / App Support.
|
|
44
|
-
/// Chosen to be self-documenting and to encourage commit-to-vcs (so the
|
|
45
|
-
/// license travels with the code).
|
|
46
|
-
public let DVAI_DEFAULT_LICENSE_FILENAME = "dvai-license.jwt"
|
|
47
|
-
|
|
48
|
-
public struct LicenseDiscoveryOptions: Sendable {
|
|
49
|
-
/// Pre-loaded JWT string (skips all filesystem lookups).
|
|
50
|
-
public var token: String?
|
|
51
|
-
/// Explicit path to load from. Overrides auto-discovery.
|
|
52
|
-
public var path: String?
|
|
53
|
-
/// App Group identifier to also search (e.g. for shared licenses
|
|
54
|
-
/// across an app + extension). Optional; default `nil` skips the
|
|
55
|
-
/// App Group lookup.
|
|
56
|
-
public var appGroupIdentifier: String?
|
|
57
|
-
|
|
58
|
-
public init(
|
|
59
|
-
token: String? = nil,
|
|
60
|
-
path: String? = nil,
|
|
61
|
-
appGroupIdentifier: String? = nil
|
|
62
|
-
) {
|
|
63
|
-
self.token = token
|
|
64
|
-
self.path = path
|
|
65
|
-
self.appGroupIdentifier = appGroupIdentifier
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/// Best-effort load of a license JWT. Returns the raw token string +
|
|
70
|
-
/// source description on success, or nil on miss. Errors during loading
|
|
71
|
-
/// (file not found, permission denied) collapse to nil — the
|
|
72
|
-
/// validator's responsibility is to handle the no-license case
|
|
73
|
-
/// gracefully, not the discovery layer's.
|
|
74
|
-
///
|
|
75
|
-
/// Source descriptions are surfaced in `LicenseStatus.freeProd.reason`
|
|
76
|
-
/// for debug, and shown in dashboards so the developer can see which
|
|
77
|
-
/// of the seven locations resolved.
|
|
78
|
-
public func discoverLicenseToken(
|
|
79
|
-
options: LicenseDiscoveryOptions = LicenseDiscoveryOptions()
|
|
80
|
-
) -> (token: String, source: String)? {
|
|
81
|
-
// 1. Explicit token wins.
|
|
82
|
-
if let token = options.token, !token.isEmpty {
|
|
83
|
-
return (token.trimmingCharacters(in: .whitespacesAndNewlines), "options.token")
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 2. Explicit path.
|
|
87
|
-
if let path = options.path, !path.isEmpty {
|
|
88
|
-
if let loaded = tryLoadFromPath(path) {
|
|
89
|
-
return (loaded, path)
|
|
90
|
-
}
|
|
91
|
-
return nil // explicit miss — do NOT fall through
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 3. Env-var path. Use `envVar` (libc getenv) so DVAI_LICENSE_PATH
|
|
95
|
-
// mutated mid-process — e.g. by tests — is picked up.
|
|
96
|
-
if let envPath = envVar("DVAI_LICENSE_PATH") {
|
|
97
|
-
if let loaded = tryLoadFromPath(envPath) {
|
|
98
|
-
return (loaded, "DVAI_LICENSE_PATH=\(envPath)")
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 4. Env-var inline token.
|
|
103
|
-
if let envToken = envVar("DVAI_LICENSE_TOKEN") {
|
|
104
|
-
return (
|
|
105
|
-
envToken.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
106
|
-
"DVAI_LICENSE_TOKEN env var"
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// 5. Bundle resource (dvai-license.jwt next to the app's other
|
|
111
|
-
// bundled resources).
|
|
112
|
-
if let bundleURL = Bundle.main.url(forResource: "dvai-license", withExtension: "jwt") {
|
|
113
|
-
if let loaded = tryLoadFromPath(bundleURL.path) {
|
|
114
|
-
return (loaded, "Bundle.main resource dvai-license.jwt")
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 6. Application Support / dvai-bridge / dvai-license.jwt.
|
|
119
|
-
if let appSupportURL = (try? FileManager.default.url(
|
|
120
|
-
for: .applicationSupportDirectory,
|
|
121
|
-
in: .userDomainMask,
|
|
122
|
-
appropriateFor: nil,
|
|
123
|
-
create: false
|
|
124
|
-
)) {
|
|
125
|
-
let candidate = appSupportURL
|
|
126
|
-
.appendingPathComponent("dvai-bridge", isDirectory: true)
|
|
127
|
-
.appendingPathComponent(DVAI_DEFAULT_LICENSE_FILENAME)
|
|
128
|
-
if let loaded = tryLoadFromPath(candidate.path) {
|
|
129
|
-
return (loaded, candidate.path)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 7. Documents directory.
|
|
134
|
-
if let docsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
|
135
|
-
let candidate = docsURL.appendingPathComponent(DVAI_DEFAULT_LICENSE_FILENAME)
|
|
136
|
-
if let loaded = tryLoadFromPath(candidate.path) {
|
|
137
|
-
return (loaded, candidate.path)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// 8. (Optional) App Group container, if configured.
|
|
142
|
-
if let groupId = options.appGroupIdentifier, !groupId.isEmpty {
|
|
143
|
-
if let groupURL = FileManager.default
|
|
144
|
-
.containerURL(forSecurityApplicationGroupIdentifier: groupId)?
|
|
145
|
-
.appendingPathComponent(DVAI_DEFAULT_LICENSE_FILENAME) {
|
|
146
|
-
if let loaded = tryLoadFromPath(groupURL.path) {
|
|
147
|
-
return (loaded, groupURL.path)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return nil
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/// Read a file at `path` and return its trimmed UTF-8 contents, or
|
|
156
|
-
/// nil on any error (missing / permission / encoding).
|
|
157
|
-
private func tryLoadFromPath(_ path: String) -> String? {
|
|
158
|
-
let fm = FileManager.default
|
|
159
|
-
guard fm.fileExists(atPath: path) else { return nil }
|
|
160
|
-
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return nil }
|
|
161
|
-
guard let text = String(data: data, encoding: .utf8) else { return nil }
|
|
162
|
-
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
163
|
-
return trimmed.isEmpty ? nil : trimmed
|
|
164
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* License-file discovery for the iOS SDK.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `packages/dvai-bridge-core/src/license/discovery.ts` but with
|
|
5
|
+
* iOS-native locations (Bundle resources, Documents directory, App Group
|
|
6
|
+
* containers) in place of the JS side's CWD-based filesystem walk.
|
|
7
|
+
*
|
|
8
|
+
* Priority order (first hit wins):
|
|
9
|
+
*
|
|
10
|
+
* 1. `LicenseDiscoveryOptions.token` — inline JWT string. Useful when
|
|
11
|
+
* the host app fetches its license over the network at runtime
|
|
12
|
+
* and wants to inject the result without touching disk.
|
|
13
|
+
*
|
|
14
|
+
* 2. `LicenseDiscoveryOptions.path` — explicit file path. Useful for
|
|
15
|
+
* tests or for hosts that store the .jwt outside the standard
|
|
16
|
+
* locations. If this path is set but the file isn't there or
|
|
17
|
+
* isn't readable, this is a real miss (we do NOT silently fall
|
|
18
|
+
* through to auto-discovery) — same semantics as the JS side.
|
|
19
|
+
*
|
|
20
|
+
* 3. `DVAI_LICENSE_PATH` env var. Operator-supplied path; helpful
|
|
21
|
+
* for CI / TestFlight where you don't want to ship the .jwt in
|
|
22
|
+
* the bundle. Misses fall through to (5).
|
|
23
|
+
*
|
|
24
|
+
* 4. `DVAI_LICENSE_TOKEN` env var. Inline JWT in the environment.
|
|
25
|
+
*
|
|
26
|
+
* 5. `dvai-license.jwt` resource in `Bundle.main`. The dev-friendly
|
|
27
|
+
* happy path: add the file to the app's "Copy Bundle Resources"
|
|
28
|
+
* phase, the SDK picks it up automatically.
|
|
29
|
+
*
|
|
30
|
+
* 6. `Application Support/dvai-bridge/dvai-license.jwt` in the app's
|
|
31
|
+
* sandbox. Useful for licenses fetched after install (e.g. via
|
|
32
|
+
* an in-app purchase flow).
|
|
33
|
+
*
|
|
34
|
+
* 7. `Documents/dvai-license.jwt`. Last-resort fallback — visible
|
|
35
|
+
* via iTunes File Sharing if the app opts in, so handy for
|
|
36
|
+
* side-loading a license during development.
|
|
37
|
+
*
|
|
38
|
+
* Returning `nil` means "no license token found"; the validator treats
|
|
39
|
+
* that as the free-tier case (after the dev-mode bypass).
|
|
40
|
+
*/
|
|
41
|
+
import Foundation
|
|
42
|
+
|
|
43
|
+
/// Default filename the SDK looks for in Bundle / Documents / App Support.
|
|
44
|
+
/// Chosen to be self-documenting and to encourage commit-to-vcs (so the
|
|
45
|
+
/// license travels with the code).
|
|
46
|
+
public let DVAI_DEFAULT_LICENSE_FILENAME = "dvai-license.jwt"
|
|
47
|
+
|
|
48
|
+
public struct LicenseDiscoveryOptions: Sendable {
|
|
49
|
+
/// Pre-loaded JWT string (skips all filesystem lookups).
|
|
50
|
+
public var token: String?
|
|
51
|
+
/// Explicit path to load from. Overrides auto-discovery.
|
|
52
|
+
public var path: String?
|
|
53
|
+
/// App Group identifier to also search (e.g. for shared licenses
|
|
54
|
+
/// across an app + extension). Optional; default `nil` skips the
|
|
55
|
+
/// App Group lookup.
|
|
56
|
+
public var appGroupIdentifier: String?
|
|
57
|
+
|
|
58
|
+
public init(
|
|
59
|
+
token: String? = nil,
|
|
60
|
+
path: String? = nil,
|
|
61
|
+
appGroupIdentifier: String? = nil
|
|
62
|
+
) {
|
|
63
|
+
self.token = token
|
|
64
|
+
self.path = path
|
|
65
|
+
self.appGroupIdentifier = appGroupIdentifier
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Best-effort load of a license JWT. Returns the raw token string +
|
|
70
|
+
/// source description on success, or nil on miss. Errors during loading
|
|
71
|
+
/// (file not found, permission denied) collapse to nil — the
|
|
72
|
+
/// validator's responsibility is to handle the no-license case
|
|
73
|
+
/// gracefully, not the discovery layer's.
|
|
74
|
+
///
|
|
75
|
+
/// Source descriptions are surfaced in `LicenseStatus.freeProd.reason`
|
|
76
|
+
/// for debug, and shown in dashboards so the developer can see which
|
|
77
|
+
/// of the seven locations resolved.
|
|
78
|
+
public func discoverLicenseToken(
|
|
79
|
+
options: LicenseDiscoveryOptions = LicenseDiscoveryOptions()
|
|
80
|
+
) -> (token: String, source: String)? {
|
|
81
|
+
// 1. Explicit token wins.
|
|
82
|
+
if let token = options.token, !token.isEmpty {
|
|
83
|
+
return (token.trimmingCharacters(in: .whitespacesAndNewlines), "options.token")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2. Explicit path.
|
|
87
|
+
if let path = options.path, !path.isEmpty {
|
|
88
|
+
if let loaded = tryLoadFromPath(path) {
|
|
89
|
+
return (loaded, path)
|
|
90
|
+
}
|
|
91
|
+
return nil // explicit miss — do NOT fall through
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. Env-var path. Use `envVar` (libc getenv) so DVAI_LICENSE_PATH
|
|
95
|
+
// mutated mid-process — e.g. by tests — is picked up.
|
|
96
|
+
if let envPath = envVar("DVAI_LICENSE_PATH") {
|
|
97
|
+
if let loaded = tryLoadFromPath(envPath) {
|
|
98
|
+
return (loaded, "DVAI_LICENSE_PATH=\(envPath)")
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 4. Env-var inline token.
|
|
103
|
+
if let envToken = envVar("DVAI_LICENSE_TOKEN") {
|
|
104
|
+
return (
|
|
105
|
+
envToken.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
106
|
+
"DVAI_LICENSE_TOKEN env var"
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 5. Bundle resource (dvai-license.jwt next to the app's other
|
|
111
|
+
// bundled resources).
|
|
112
|
+
if let bundleURL = Bundle.main.url(forResource: "dvai-license", withExtension: "jwt") {
|
|
113
|
+
if let loaded = tryLoadFromPath(bundleURL.path) {
|
|
114
|
+
return (loaded, "Bundle.main resource dvai-license.jwt")
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 6. Application Support / dvai-bridge / dvai-license.jwt.
|
|
119
|
+
if let appSupportURL = (try? FileManager.default.url(
|
|
120
|
+
for: .applicationSupportDirectory,
|
|
121
|
+
in: .userDomainMask,
|
|
122
|
+
appropriateFor: nil,
|
|
123
|
+
create: false
|
|
124
|
+
)) {
|
|
125
|
+
let candidate = appSupportURL
|
|
126
|
+
.appendingPathComponent("dvai-bridge", isDirectory: true)
|
|
127
|
+
.appendingPathComponent(DVAI_DEFAULT_LICENSE_FILENAME)
|
|
128
|
+
if let loaded = tryLoadFromPath(candidate.path) {
|
|
129
|
+
return (loaded, candidate.path)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 7. Documents directory.
|
|
134
|
+
if let docsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
|
135
|
+
let candidate = docsURL.appendingPathComponent(DVAI_DEFAULT_LICENSE_FILENAME)
|
|
136
|
+
if let loaded = tryLoadFromPath(candidate.path) {
|
|
137
|
+
return (loaded, candidate.path)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 8. (Optional) App Group container, if configured.
|
|
142
|
+
if let groupId = options.appGroupIdentifier, !groupId.isEmpty {
|
|
143
|
+
if let groupURL = FileManager.default
|
|
144
|
+
.containerURL(forSecurityApplicationGroupIdentifier: groupId)?
|
|
145
|
+
.appendingPathComponent(DVAI_DEFAULT_LICENSE_FILENAME) {
|
|
146
|
+
if let loaded = tryLoadFromPath(groupURL.path) {
|
|
147
|
+
return (loaded, groupURL.path)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return nil
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Read a file at `path` and return its trimmed UTF-8 contents, or
|
|
156
|
+
/// nil on any error (missing / permission / encoding).
|
|
157
|
+
private func tryLoadFromPath(_ path: String) -> String? {
|
|
158
|
+
let fm = FileManager.default
|
|
159
|
+
guard fm.fileExists(atPath: path) else { return nil }
|
|
160
|
+
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return nil }
|
|
161
|
+
guard let text = String(data: data, encoding: .utf8) else { return nil }
|
|
162
|
+
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
163
|
+
return trimmed.isEmpty ? nil : trimmed
|
|
164
|
+
}
|