@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,392 +1,392 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* DVAI-Bridge license validator — offline JWT verification on iOS.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `packages/dvai-bridge-core/src/license/LicenseValidator.ts`:
|
|
5
|
-
*
|
|
6
|
-
* 1. The license is a signed JWT (header + payload + ECDSA P-256
|
|
7
|
-
* signature), issued by the operator's own license-generator
|
|
8
|
-
* service from a private key they hold.
|
|
9
|
-
* 2. The SDK ships only with public keys (see `PublicKeys.swift`) and
|
|
10
|
-
* cannot itself produce valid licenses — so reverse-engineering
|
|
11
|
-
* the bundled SDK gains nothing.
|
|
12
|
-
* 3. At runtime, the validator does signature + expiry + audience +
|
|
13
|
-
* platform binding checks. Failure of any check collapses to
|
|
14
|
-
* free-tier (with attribution badge), not a hard error — the SDK
|
|
15
|
-
* stays usable for hobbyists / community use UNLESS the caller
|
|
16
|
-
* uses `validateAndAssert()` (which throws — used by
|
|
17
|
-
* `DVAIBridge.start(...)` to enforce BSL 1.1).
|
|
18
|
-
*
|
|
19
|
-
* Network calls: zero. The whole flow is offline by design.
|
|
20
|
-
*
|
|
21
|
-
* Algorithm-confusion defense: this validator ONLY accepts ES256.
|
|
22
|
-
* `alg: none`, `HS256`, `RS256` etc. are rejected at the header-parsing
|
|
23
|
-
* step BEFORE handing the token to JWTKit, so a forged header can't
|
|
24
|
-
* trick us into validating against an unintended key.
|
|
25
|
-
*/
|
|
26
|
-
import Foundation
|
|
27
|
-
#if !COCOAPODS
|
|
28
|
-
import JWTKit
|
|
29
|
-
#endif
|
|
30
|
-
|
|
31
|
-
public struct LicenseValidatorOptions: Sendable {
|
|
32
|
-
/// Pre-loaded JWT string. Skips filesystem lookups.
|
|
33
|
-
public var token: String?
|
|
34
|
-
/// Explicit path to load from. Overrides auto-discovery.
|
|
35
|
-
public var path: String?
|
|
36
|
-
/// Override the public-key registry. Defaults to `DVAI_PUBLIC_KEYS`
|
|
37
|
-
/// from `PublicKeys.swift`. Tests inject their own keypair via this
|
|
38
|
-
/// option so they can sign + verify against a deterministic key
|
|
39
|
-
/// without polluting the production registry.
|
|
40
|
-
public var publicKeys: [String: DvaiPublicKeyJwk]?
|
|
41
|
-
/// If true, accept tokens signed under `DVAI_PLACEHOLDER_KID`
|
|
42
|
-
/// (i.e. the built-in placeholder public key). Off by default — a
|
|
43
|
-
/// real production build must replace the placeholder with a
|
|
44
|
-
/// generated key. Tests set this to true.
|
|
45
|
-
public var allowPlaceholderKey: Bool
|
|
46
|
-
/// App Group identifier to also search during discovery.
|
|
47
|
-
public var appGroupIdentifier: String?
|
|
48
|
-
|
|
49
|
-
public init(
|
|
50
|
-
token: String? = nil,
|
|
51
|
-
path: String? = nil,
|
|
52
|
-
publicKeys: [String: DvaiPublicKeyJwk]? = nil,
|
|
53
|
-
allowPlaceholderKey: Bool = false,
|
|
54
|
-
appGroupIdentifier: String? = nil
|
|
55
|
-
) {
|
|
56
|
-
self.token = token
|
|
57
|
-
self.path = path
|
|
58
|
-
self.publicKeys = publicKeys
|
|
59
|
-
self.allowPlaceholderKey = allowPlaceholderKey
|
|
60
|
-
self.appGroupIdentifier = appGroupIdentifier
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/// Validate a DVAI-Bridge license once at SDK startup. The returned
|
|
65
|
-
/// `LicenseStatus` is the discriminated value the rest of the SDK
|
|
66
|
-
/// dispatches on. `validate()` never throws on validation failure —
|
|
67
|
-
/// it returns `.freeProd` / `.freeExpired`. `validateAndAssert()`
|
|
68
|
-
/// throws `LicenseRequiredError` for those two cases — used by
|
|
69
|
-
/// `DVAIBridge.start(...)` to enforce BSL 1.1.
|
|
70
|
-
public final class LicenseValidator: Sendable {
|
|
71
|
-
private let opts: LicenseValidatorOptions
|
|
72
|
-
|
|
73
|
-
public init(options: LicenseValidatorOptions = LicenseValidatorOptions()) {
|
|
74
|
-
self.opts = options
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/// Validate WITHOUT throwing. Returns a `LicenseStatus` describing
|
|
78
|
-
/// what the validator determined; never throws on missing /
|
|
79
|
-
/// invalid / expired licenses.
|
|
80
|
-
///
|
|
81
|
-
/// Useful for host-app dashboards that want to display the
|
|
82
|
-
/// licensee / expiry / fallback reason without halting SDK
|
|
83
|
-
/// startup, and for tests. The SDK's `start(_:)` calls
|
|
84
|
-
/// `validateAndAssert()` instead — which throws.
|
|
85
|
-
///
|
|
86
|
-
/// Idempotent; safe to call multiple times.
|
|
87
|
-
public func validate() async -> LicenseStatus {
|
|
88
|
-
// 1. Dev-mode bypass — license required only in production.
|
|
89
|
-
let dev = detectDevMode()
|
|
90
|
-
if dev.isDev {
|
|
91
|
-
return .freeDev(reason: dev.reason)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 2. Discover the token. Returns nil when no license source is
|
|
95
|
-
// configured AND auto-discovery fails — fall through to
|
|
96
|
-
// free-prod so the SDK still works for community / hobbyist
|
|
97
|
-
// users (and the assert variant can then throw a clear error).
|
|
98
|
-
let discovery = LicenseDiscoveryOptions(
|
|
99
|
-
token: opts.token,
|
|
100
|
-
path: opts.path,
|
|
101
|
-
appGroupIdentifier: opts.appGroupIdentifier
|
|
102
|
-
)
|
|
103
|
-
guard let discovered = discoverLicenseToken(options: discovery) else {
|
|
104
|
-
return .freeProd(reason:
|
|
105
|
-
"no license token found; checked options.token, options.path, " +
|
|
106
|
-
"DVAI_LICENSE_PATH env, DVAI_LICENSE_TOKEN env, Bundle.main " +
|
|
107
|
-
"resource dvai-license.jwt, Application Support / dvai-bridge / " +
|
|
108
|
-
"dvai-license.jwt, and Documents / dvai-license.jwt"
|
|
109
|
-
)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 3. Verify signature + claims.
|
|
113
|
-
let platform = detectPlatform()
|
|
114
|
-
let audience = detectAudience()
|
|
115
|
-
return await verifyToken(discovered.token, platform: platform, runtimeAudience: audience)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/// Strict validation entry point used by the SDK at startup. Returns
|
|
119
|
-
/// `LicenseStatus` on success (`commercial`, `trial`, `freeDev`) and
|
|
120
|
-
/// THROWS `LicenseRequiredError` on `freeProd` / `freeExpired`.
|
|
121
|
-
///
|
|
122
|
-
/// This is the BSL 1.1 enforcement point: in production / release
|
|
123
|
-
/// builds (any non-dev-mode environment), the SDK refuses to operate
|
|
124
|
-
/// without a valid commercial or trial license. Developers running
|
|
125
|
-
/// in DEBUG / simulator / DVAI_FORCE_DEV are unaffected — those
|
|
126
|
-
/// return a `.freeDev` status and the SDK proceeds normally.
|
|
127
|
-
///
|
|
128
|
-
/// Use `validate()` when you want to inspect the status without
|
|
129
|
-
/// halting startup (host-app dashboards, test fixtures).
|
|
130
|
-
@discardableResult
|
|
131
|
-
public func validateAndAssert() async throws -> LicenseStatus {
|
|
132
|
-
let status = await validate()
|
|
133
|
-
switch status {
|
|
134
|
-
case .freeProd, .freeExpired:
|
|
135
|
-
throw LicenseRequiredError(
|
|
136
|
-
message: buildRequiredErrorMessage(status: status),
|
|
137
|
-
status: status
|
|
138
|
-
)
|
|
139
|
-
default:
|
|
140
|
-
return status
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// MARK: - Internal: token verification
|
|
145
|
-
|
|
146
|
-
private func verifyToken(
|
|
147
|
-
_ token: String,
|
|
148
|
-
platform: DvaiPlatform,
|
|
149
|
-
runtimeAudience: String?
|
|
150
|
-
) async -> LicenseStatus {
|
|
151
|
-
#if COCOAPODS
|
|
152
|
-
// CocoaPods fallback — commercial licenses require SwiftPM.
|
|
153
|
-
// This is documented in PUBLISHING.md.
|
|
154
|
-
return .freeProd(reason: "Commercial license validation requires SwiftPM (JWTKit). " +
|
|
155
|
-
"CocoaPods builds only support community/hobbyist use.")
|
|
156
|
-
#else
|
|
157
|
-
let registry = opts.publicKeys ?? DVAI_PUBLIC_KEYS
|
|
158
|
-
|
|
159
|
-
// Parse the header ourselves to (a) refuse non-ES256 alg early
|
|
160
|
-
// (algorithm-confusion defense — we never even hand a non-ES256
|
|
161
|
-
// token to the JWT library), and (b) pick the right public key
|
|
162
|
-
// by kid before invoking signature verification.
|
|
163
|
-
let parts = token.split(separator: ".", omittingEmptySubsequences: false)
|
|
164
|
-
if parts.count != 3 || parts[0].isEmpty {
|
|
165
|
-
return .freeProd(reason: "license token is not a well-formed JWT (need 3 segments)")
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let headerJSON: [String: Any]
|
|
169
|
-
do {
|
|
170
|
-
guard let headerData = base64UrlDecode(String(parts[0])) else {
|
|
171
|
-
return .freeProd(reason: "license token header is not base64url-decodable")
|
|
172
|
-
}
|
|
173
|
-
guard let obj = try JSONSerialization.jsonObject(with: headerData) as? [String: Any] else {
|
|
174
|
-
return .freeProd(reason: "license token header is not a JSON object")
|
|
175
|
-
}
|
|
176
|
-
headerJSON = obj
|
|
177
|
-
} catch {
|
|
178
|
-
return .freeProd(reason: "license token header is not parseable JSON: \(error.localizedDescription)")
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
let headerAlg = headerJSON["alg"] as? String
|
|
182
|
-
if headerAlg != "ES256" {
|
|
183
|
-
// Refuse `alg: none` and any non-ES256 algorithm. Critical
|
|
184
|
-
// defense against the classic JWT algorithm-confusion
|
|
185
|
-
// vulnerability.
|
|
186
|
-
return .freeProd(reason:
|
|
187
|
-
"license token uses unsupported alg \"\(headerAlg ?? "(missing)")\", expected ES256"
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
guard let kid = headerJSON["kid"] as? String, !kid.isEmpty else {
|
|
192
|
-
return .freeProd(reason: "license token header missing kid; cannot select verification key")
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
guard let jwk = registry[kid] else {
|
|
196
|
-
return .freeProd(reason:
|
|
197
|
-
"license token kid \"\(kid)\" is not in the SDK's public-key " +
|
|
198
|
-
"registry; either the key was rotated and you're on an old SDK, " +
|
|
199
|
-
"or the token was signed with a key we don't recognise"
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if kid == DVAI_PLACEHOLDER_KID && !opts.allowPlaceholderKey {
|
|
204
|
-
return .freeProd(reason:
|
|
205
|
-
"license token signed with the placeholder key (kid \"\(DVAI_PLACEHOLDER_KID)\"); " +
|
|
206
|
-
"replace the placeholder in PublicKeys.swift with a real key generated " +
|
|
207
|
-
"via scripts/license/generate-keypair.mjs before issuing real licenses"
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Hand the (alg-vetted, kid-known) token to JWTKit for signature
|
|
212
|
-
// verification. We register only the matched key — under that
|
|
213
|
-
// kid — so the verifier has no chance to pick anything else.
|
|
214
|
-
let keys = JWTKeyCollection()
|
|
215
|
-
do {
|
|
216
|
-
// ES256PublicKey is JWTKit's public typealias for
|
|
217
|
-
// `ECDSA.PublicKey<P256>` — avoids leaking the swift-crypto
|
|
218
|
-
// `P256` symbol into our source.
|
|
219
|
-
let ecdsaPublic = try ES256PublicKey(parameters: (x: jwk.x, y: jwk.y))
|
|
220
|
-
await keys.add(ecdsa: ecdsaPublic, kid: JWKIdentifier(string: kid))
|
|
221
|
-
} catch {
|
|
222
|
-
return .freeProd(reason:
|
|
223
|
-
"license token kid \"\(kid)\" points at a malformed public key in the SDK's " +
|
|
224
|
-
"registry (could not parse x/y coordinates): \(error.localizedDescription)"
|
|
225
|
-
)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
let payload: DvaiLicensePayload
|
|
229
|
-
do {
|
|
230
|
-
payload = try await keys.verify(token, as: DvaiLicensePayload.self)
|
|
231
|
-
} catch let jwtError as JWTError {
|
|
232
|
-
// JWTKit's signatureVerificationFailed is what we get for a
|
|
233
|
-
// tampered token. Surface it specifically.
|
|
234
|
-
switch jwtError.errorType {
|
|
235
|
-
case .signatureVerificationFailed:
|
|
236
|
-
return .freeProd(reason:
|
|
237
|
-
"license token signature did not verify against kid \"\(kid)\"; " +
|
|
238
|
-
"the token may have been tampered with or was signed by a different key"
|
|
239
|
-
)
|
|
240
|
-
case .malformedToken:
|
|
241
|
-
return .freeProd(reason: "license token is malformed: \(jwtError.reason ?? "(no detail)")")
|
|
242
|
-
default:
|
|
243
|
-
return .freeProd(reason: "license token verification failed: \(jwtError)")
|
|
244
|
-
}
|
|
245
|
-
} catch {
|
|
246
|
-
return .freeProd(reason: "license token verification failed: \(error.localizedDescription)")
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// -----------------------------------------------------------------
|
|
250
|
-
// Signature passed. Now run our own claim checks so each failure
|
|
251
|
-
// mode gets a specific, actionable error message.
|
|
252
|
-
// -----------------------------------------------------------------
|
|
253
|
-
|
|
254
|
-
// Issuer must be exactly "DVAI-Bridge".
|
|
255
|
-
if payload.iss != "DVAI-Bridge" {
|
|
256
|
-
return .freeProd(reason:
|
|
257
|
-
"license token issuer is \"\(payload.iss)\", expected \"DVAI-Bridge\""
|
|
258
|
-
)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Expiry: if exp is in the past, surface a specific freeExpired
|
|
262
|
-
// status (the licensee + when so the dashboard can prompt
|
|
263
|
-
// renewal).
|
|
264
|
-
let nowSeconds = Int64(Date().timeIntervalSince1970)
|
|
265
|
-
if payload.exp <= nowSeconds {
|
|
266
|
-
return .freeExpired(licensee: payload.licensee, expiredAt: payload.exp)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Tier must be one of the live tiers.
|
|
270
|
-
let tier: LicenseTier
|
|
271
|
-
switch payload.tier {
|
|
272
|
-
case "commercial": tier = .commercial
|
|
273
|
-
case "trial": tier = .trial
|
|
274
|
-
default:
|
|
275
|
-
return .freeProd(reason:
|
|
276
|
-
"license token tier \"\(payload.tier)\" is not recognised; " +
|
|
277
|
-
"expected \"commercial\" or \"trial\""
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Platforms must include our runtime platform.
|
|
282
|
-
if !payload.platforms.contains(platform.rawValue) {
|
|
283
|
-
return .freeProd(reason:
|
|
284
|
-
"license token does not authorise platform \"\(platform.rawValue)\"; " +
|
|
285
|
-
"the token covers [\(payload.platforms.joined(separator: ", "))]"
|
|
286
|
-
)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Audience must match (exact / wildcard / *).
|
|
290
|
-
guard let matched = matchAudience(runtimeAudience: runtimeAudience, audClaim: payload.aud) else {
|
|
291
|
-
let noneSuffix = runtimeAudience == nil
|
|
292
|
-
? " — set DVAI_AUDIENCE in your environment, or use a \"*\" aud entry for any-domain licenses"
|
|
293
|
-
: ""
|
|
294
|
-
return .freeProd(reason:
|
|
295
|
-
"license token's audience entries [\(payload.aud.joined(separator: ", "))] " +
|
|
296
|
-
"do not match the current runtime audience \"\(runtimeAudience ?? "(none)")\"" +
|
|
297
|
-
noneSuffix
|
|
298
|
-
)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
switch tier {
|
|
302
|
-
case .commercial:
|
|
303
|
-
return .commercial(
|
|
304
|
-
licensee: payload.licensee,
|
|
305
|
-
expiresAt: payload.exp,
|
|
306
|
-
platform: platform,
|
|
307
|
-
audienceMatched: matched
|
|
308
|
-
)
|
|
309
|
-
case .trial:
|
|
310
|
-
return .trial(
|
|
311
|
-
licensee: payload.licensee,
|
|
312
|
-
expiresAt: payload.exp,
|
|
313
|
-
platform: platform,
|
|
314
|
-
audienceMatched: matched
|
|
315
|
-
)
|
|
316
|
-
default:
|
|
317
|
-
// Unreachable (we filtered above), but the compiler can't
|
|
318
|
-
// see that.
|
|
319
|
-
return .freeProd(reason: "internal validator state error")
|
|
320
|
-
}
|
|
321
|
-
#endif
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// MARK: - Helpers
|
|
326
|
-
|
|
327
|
-
/// Decode a base64url string (RFC 4648 §5) into raw bytes. The JWT
|
|
328
|
-
/// header / payload segments use this encoding (no padding, `-` and `_`
|
|
329
|
-
/// instead of `+` and `/`).
|
|
330
|
-
private func base64UrlDecode(_ s: String) -> Data? {
|
|
331
|
-
var b64 = s.replacingOccurrences(of: "-", with: "+")
|
|
332
|
-
.replacingOccurrences(of: "_", with: "/")
|
|
333
|
-
let pad = b64.count % 4
|
|
334
|
-
if pad > 0 {
|
|
335
|
-
b64 += String(repeating: "=", count: 4 - pad)
|
|
336
|
-
}
|
|
337
|
-
return Data(base64Encoded: b64)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/// Build the developer-facing error message for `LicenseRequiredError`.
|
|
341
|
-
/// Intentionally verbose: it tells the developer exactly what failed,
|
|
342
|
-
/// how to resolve it, where to put the license file, and how to bypass
|
|
343
|
-
/// for local development. This message will be printed to Xcode's
|
|
344
|
-
/// console or a crash log — make it readable in both.
|
|
345
|
-
internal func buildRequiredErrorMessage(status: LicenseStatus) -> String {
|
|
346
|
-
let header = """
|
|
347
|
-
|
|
348
|
-
DVAI-Bridge Commercial License Required
|
|
349
|
-
=======================================
|
|
350
|
-
|
|
351
|
-
"""
|
|
352
|
-
|
|
353
|
-
let reason: String
|
|
354
|
-
switch status {
|
|
355
|
-
case .freeExpired(let licensee, let expiredAt):
|
|
356
|
-
let date = Date(timeIntervalSince1970: TimeInterval(expiredAt))
|
|
357
|
-
let formatter = ISO8601DateFormatter()
|
|
358
|
-
formatter.formatOptions = [.withInternetDateTime]
|
|
359
|
-
reason = "License for \"\(licensee)\" expired at \(formatter.string(from: date))."
|
|
360
|
-
case .freeProd(let r):
|
|
361
|
-
reason = r
|
|
362
|
-
default:
|
|
363
|
-
reason = "(unknown status)"
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
let remediation = """
|
|
367
|
-
|
|
368
|
-
This SDK is licensed under BSL 1.1 and requires a valid commercial
|
|
369
|
-
or trial license to run in production / release builds.
|
|
370
|
-
|
|
371
|
-
To resolve:
|
|
372
|
-
1. Obtain a license at https://deepvoiceai.com/dvai-bridge/license
|
|
373
|
-
2. Place the file at one of these locations (any will work):
|
|
374
|
-
- <App.bundle>/dvai-license.jwt (add to Copy Bundle Resources)
|
|
375
|
-
- the path you pass as LicenseValidatorOptions.path
|
|
376
|
-
- the path in $DVAI_LICENSE_PATH
|
|
377
|
-
- inline JWT in LicenseValidatorOptions.token or $DVAI_LICENSE_TOKEN
|
|
378
|
-
- <Application Support>/dvai-bridge/dvai-license.jwt
|
|
379
|
-
- <Documents>/dvai-license.jwt
|
|
380
|
-
3. Re-run.
|
|
381
|
-
|
|
382
|
-
Developing locally? The SDK auto-detects dev mode on:
|
|
383
|
-
- DEBUG build configuration (compile-time #if DEBUG)
|
|
384
|
-
- iOS Simulator (compile-time targetEnvironment(simulator))
|
|
385
|
-
- DVAI_FORCE_DEV=1 environment variable (explicit override)
|
|
386
|
-
Any of these silences this error and lets the SDK run without a
|
|
387
|
-
license.
|
|
388
|
-
|
|
389
|
-
"""
|
|
390
|
-
|
|
391
|
-
return header + "\n" + reason + "\n" + remediation
|
|
392
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* DVAI-Bridge license validator — offline JWT verification on iOS.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `packages/dvai-bridge-core/src/license/LicenseValidator.ts`:
|
|
5
|
+
*
|
|
6
|
+
* 1. The license is a signed JWT (header + payload + ECDSA P-256
|
|
7
|
+
* signature), issued by the operator's own license-generator
|
|
8
|
+
* service from a private key they hold.
|
|
9
|
+
* 2. The SDK ships only with public keys (see `PublicKeys.swift`) and
|
|
10
|
+
* cannot itself produce valid licenses — so reverse-engineering
|
|
11
|
+
* the bundled SDK gains nothing.
|
|
12
|
+
* 3. At runtime, the validator does signature + expiry + audience +
|
|
13
|
+
* platform binding checks. Failure of any check collapses to
|
|
14
|
+
* free-tier (with attribution badge), not a hard error — the SDK
|
|
15
|
+
* stays usable for hobbyists / community use UNLESS the caller
|
|
16
|
+
* uses `validateAndAssert()` (which throws — used by
|
|
17
|
+
* `DVAIBridge.start(...)` to enforce BSL 1.1).
|
|
18
|
+
*
|
|
19
|
+
* Network calls: zero. The whole flow is offline by design.
|
|
20
|
+
*
|
|
21
|
+
* Algorithm-confusion defense: this validator ONLY accepts ES256.
|
|
22
|
+
* `alg: none`, `HS256`, `RS256` etc. are rejected at the header-parsing
|
|
23
|
+
* step BEFORE handing the token to JWTKit, so a forged header can't
|
|
24
|
+
* trick us into validating against an unintended key.
|
|
25
|
+
*/
|
|
26
|
+
import Foundation
|
|
27
|
+
#if !COCOAPODS
|
|
28
|
+
import JWTKit
|
|
29
|
+
#endif
|
|
30
|
+
|
|
31
|
+
public struct LicenseValidatorOptions: Sendable {
|
|
32
|
+
/// Pre-loaded JWT string. Skips filesystem lookups.
|
|
33
|
+
public var token: String?
|
|
34
|
+
/// Explicit path to load from. Overrides auto-discovery.
|
|
35
|
+
public var path: String?
|
|
36
|
+
/// Override the public-key registry. Defaults to `DVAI_PUBLIC_KEYS`
|
|
37
|
+
/// from `PublicKeys.swift`. Tests inject their own keypair via this
|
|
38
|
+
/// option so they can sign + verify against a deterministic key
|
|
39
|
+
/// without polluting the production registry.
|
|
40
|
+
public var publicKeys: [String: DvaiPublicKeyJwk]?
|
|
41
|
+
/// If true, accept tokens signed under `DVAI_PLACEHOLDER_KID`
|
|
42
|
+
/// (i.e. the built-in placeholder public key). Off by default — a
|
|
43
|
+
/// real production build must replace the placeholder with a
|
|
44
|
+
/// generated key. Tests set this to true.
|
|
45
|
+
public var allowPlaceholderKey: Bool
|
|
46
|
+
/// App Group identifier to also search during discovery.
|
|
47
|
+
public var appGroupIdentifier: String?
|
|
48
|
+
|
|
49
|
+
public init(
|
|
50
|
+
token: String? = nil,
|
|
51
|
+
path: String? = nil,
|
|
52
|
+
publicKeys: [String: DvaiPublicKeyJwk]? = nil,
|
|
53
|
+
allowPlaceholderKey: Bool = false,
|
|
54
|
+
appGroupIdentifier: String? = nil
|
|
55
|
+
) {
|
|
56
|
+
self.token = token
|
|
57
|
+
self.path = path
|
|
58
|
+
self.publicKeys = publicKeys
|
|
59
|
+
self.allowPlaceholderKey = allowPlaceholderKey
|
|
60
|
+
self.appGroupIdentifier = appGroupIdentifier
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Validate a DVAI-Bridge license once at SDK startup. The returned
|
|
65
|
+
/// `LicenseStatus` is the discriminated value the rest of the SDK
|
|
66
|
+
/// dispatches on. `validate()` never throws on validation failure —
|
|
67
|
+
/// it returns `.freeProd` / `.freeExpired`. `validateAndAssert()`
|
|
68
|
+
/// throws `LicenseRequiredError` for those two cases — used by
|
|
69
|
+
/// `DVAIBridge.start(...)` to enforce BSL 1.1.
|
|
70
|
+
public final class LicenseValidator: Sendable {
|
|
71
|
+
private let opts: LicenseValidatorOptions
|
|
72
|
+
|
|
73
|
+
public init(options: LicenseValidatorOptions = LicenseValidatorOptions()) {
|
|
74
|
+
self.opts = options
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Validate WITHOUT throwing. Returns a `LicenseStatus` describing
|
|
78
|
+
/// what the validator determined; never throws on missing /
|
|
79
|
+
/// invalid / expired licenses.
|
|
80
|
+
///
|
|
81
|
+
/// Useful for host-app dashboards that want to display the
|
|
82
|
+
/// licensee / expiry / fallback reason without halting SDK
|
|
83
|
+
/// startup, and for tests. The SDK's `start(_:)` calls
|
|
84
|
+
/// `validateAndAssert()` instead — which throws.
|
|
85
|
+
///
|
|
86
|
+
/// Idempotent; safe to call multiple times.
|
|
87
|
+
public func validate() async -> LicenseStatus {
|
|
88
|
+
// 1. Dev-mode bypass — license required only in production.
|
|
89
|
+
let dev = detectDevMode()
|
|
90
|
+
if dev.isDev {
|
|
91
|
+
return .freeDev(reason: dev.reason)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. Discover the token. Returns nil when no license source is
|
|
95
|
+
// configured AND auto-discovery fails — fall through to
|
|
96
|
+
// free-prod so the SDK still works for community / hobbyist
|
|
97
|
+
// users (and the assert variant can then throw a clear error).
|
|
98
|
+
let discovery = LicenseDiscoveryOptions(
|
|
99
|
+
token: opts.token,
|
|
100
|
+
path: opts.path,
|
|
101
|
+
appGroupIdentifier: opts.appGroupIdentifier
|
|
102
|
+
)
|
|
103
|
+
guard let discovered = discoverLicenseToken(options: discovery) else {
|
|
104
|
+
return .freeProd(reason:
|
|
105
|
+
"no license token found; checked options.token, options.path, " +
|
|
106
|
+
"DVAI_LICENSE_PATH env, DVAI_LICENSE_TOKEN env, Bundle.main " +
|
|
107
|
+
"resource dvai-license.jwt, Application Support / dvai-bridge / " +
|
|
108
|
+
"dvai-license.jwt, and Documents / dvai-license.jwt"
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 3. Verify signature + claims.
|
|
113
|
+
let platform = detectPlatform()
|
|
114
|
+
let audience = detectAudience()
|
|
115
|
+
return await verifyToken(discovered.token, platform: platform, runtimeAudience: audience)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Strict validation entry point used by the SDK at startup. Returns
|
|
119
|
+
/// `LicenseStatus` on success (`commercial`, `trial`, `freeDev`) and
|
|
120
|
+
/// THROWS `LicenseRequiredError` on `freeProd` / `freeExpired`.
|
|
121
|
+
///
|
|
122
|
+
/// This is the BSL 1.1 enforcement point: in production / release
|
|
123
|
+
/// builds (any non-dev-mode environment), the SDK refuses to operate
|
|
124
|
+
/// without a valid commercial or trial license. Developers running
|
|
125
|
+
/// in DEBUG / simulator / DVAI_FORCE_DEV are unaffected — those
|
|
126
|
+
/// return a `.freeDev` status and the SDK proceeds normally.
|
|
127
|
+
///
|
|
128
|
+
/// Use `validate()` when you want to inspect the status without
|
|
129
|
+
/// halting startup (host-app dashboards, test fixtures).
|
|
130
|
+
@discardableResult
|
|
131
|
+
public func validateAndAssert() async throws -> LicenseStatus {
|
|
132
|
+
let status = await validate()
|
|
133
|
+
switch status {
|
|
134
|
+
case .freeProd, .freeExpired:
|
|
135
|
+
throw LicenseRequiredError(
|
|
136
|
+
message: buildRequiredErrorMessage(status: status),
|
|
137
|
+
status: status
|
|
138
|
+
)
|
|
139
|
+
default:
|
|
140
|
+
return status
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// MARK: - Internal: token verification
|
|
145
|
+
|
|
146
|
+
private func verifyToken(
|
|
147
|
+
_ token: String,
|
|
148
|
+
platform: DvaiPlatform,
|
|
149
|
+
runtimeAudience: String?
|
|
150
|
+
) async -> LicenseStatus {
|
|
151
|
+
#if COCOAPODS
|
|
152
|
+
// CocoaPods fallback — commercial licenses require SwiftPM.
|
|
153
|
+
// This is documented in PUBLISHING.md.
|
|
154
|
+
return .freeProd(reason: "Commercial license validation requires SwiftPM (JWTKit). " +
|
|
155
|
+
"CocoaPods builds only support community/hobbyist use.")
|
|
156
|
+
#else
|
|
157
|
+
let registry = opts.publicKeys ?? DVAI_PUBLIC_KEYS
|
|
158
|
+
|
|
159
|
+
// Parse the header ourselves to (a) refuse non-ES256 alg early
|
|
160
|
+
// (algorithm-confusion defense — we never even hand a non-ES256
|
|
161
|
+
// token to the JWT library), and (b) pick the right public key
|
|
162
|
+
// by kid before invoking signature verification.
|
|
163
|
+
let parts = token.split(separator: ".", omittingEmptySubsequences: false)
|
|
164
|
+
if parts.count != 3 || parts[0].isEmpty {
|
|
165
|
+
return .freeProd(reason: "license token is not a well-formed JWT (need 3 segments)")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let headerJSON: [String: Any]
|
|
169
|
+
do {
|
|
170
|
+
guard let headerData = base64UrlDecode(String(parts[0])) else {
|
|
171
|
+
return .freeProd(reason: "license token header is not base64url-decodable")
|
|
172
|
+
}
|
|
173
|
+
guard let obj = try JSONSerialization.jsonObject(with: headerData) as? [String: Any] else {
|
|
174
|
+
return .freeProd(reason: "license token header is not a JSON object")
|
|
175
|
+
}
|
|
176
|
+
headerJSON = obj
|
|
177
|
+
} catch {
|
|
178
|
+
return .freeProd(reason: "license token header is not parseable JSON: \(error.localizedDescription)")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let headerAlg = headerJSON["alg"] as? String
|
|
182
|
+
if headerAlg != "ES256" {
|
|
183
|
+
// Refuse `alg: none` and any non-ES256 algorithm. Critical
|
|
184
|
+
// defense against the classic JWT algorithm-confusion
|
|
185
|
+
// vulnerability.
|
|
186
|
+
return .freeProd(reason:
|
|
187
|
+
"license token uses unsupported alg \"\(headerAlg ?? "(missing)")\", expected ES256"
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
guard let kid = headerJSON["kid"] as? String, !kid.isEmpty else {
|
|
192
|
+
return .freeProd(reason: "license token header missing kid; cannot select verification key")
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
guard let jwk = registry[kid] else {
|
|
196
|
+
return .freeProd(reason:
|
|
197
|
+
"license token kid \"\(kid)\" is not in the SDK's public-key " +
|
|
198
|
+
"registry; either the key was rotated and you're on an old SDK, " +
|
|
199
|
+
"or the token was signed with a key we don't recognise"
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if kid == DVAI_PLACEHOLDER_KID && !opts.allowPlaceholderKey {
|
|
204
|
+
return .freeProd(reason:
|
|
205
|
+
"license token signed with the placeholder key (kid \"\(DVAI_PLACEHOLDER_KID)\"); " +
|
|
206
|
+
"replace the placeholder in PublicKeys.swift with a real key generated " +
|
|
207
|
+
"via scripts/license/generate-keypair.mjs before issuing real licenses"
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Hand the (alg-vetted, kid-known) token to JWTKit for signature
|
|
212
|
+
// verification. We register only the matched key — under that
|
|
213
|
+
// kid — so the verifier has no chance to pick anything else.
|
|
214
|
+
let keys = JWTKeyCollection()
|
|
215
|
+
do {
|
|
216
|
+
// ES256PublicKey is JWTKit's public typealias for
|
|
217
|
+
// `ECDSA.PublicKey<P256>` — avoids leaking the swift-crypto
|
|
218
|
+
// `P256` symbol into our source.
|
|
219
|
+
let ecdsaPublic = try ES256PublicKey(parameters: (x: jwk.x, y: jwk.y))
|
|
220
|
+
await keys.add(ecdsa: ecdsaPublic, kid: JWKIdentifier(string: kid))
|
|
221
|
+
} catch {
|
|
222
|
+
return .freeProd(reason:
|
|
223
|
+
"license token kid \"\(kid)\" points at a malformed public key in the SDK's " +
|
|
224
|
+
"registry (could not parse x/y coordinates): \(error.localizedDescription)"
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let payload: DvaiLicensePayload
|
|
229
|
+
do {
|
|
230
|
+
payload = try await keys.verify(token, as: DvaiLicensePayload.self)
|
|
231
|
+
} catch let jwtError as JWTError {
|
|
232
|
+
// JWTKit's signatureVerificationFailed is what we get for a
|
|
233
|
+
// tampered token. Surface it specifically.
|
|
234
|
+
switch jwtError.errorType {
|
|
235
|
+
case .signatureVerificationFailed:
|
|
236
|
+
return .freeProd(reason:
|
|
237
|
+
"license token signature did not verify against kid \"\(kid)\"; " +
|
|
238
|
+
"the token may have been tampered with or was signed by a different key"
|
|
239
|
+
)
|
|
240
|
+
case .malformedToken:
|
|
241
|
+
return .freeProd(reason: "license token is malformed: \(jwtError.reason ?? "(no detail)")")
|
|
242
|
+
default:
|
|
243
|
+
return .freeProd(reason: "license token verification failed: \(jwtError)")
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
return .freeProd(reason: "license token verification failed: \(error.localizedDescription)")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// -----------------------------------------------------------------
|
|
250
|
+
// Signature passed. Now run our own claim checks so each failure
|
|
251
|
+
// mode gets a specific, actionable error message.
|
|
252
|
+
// -----------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
// Issuer must be exactly "DVAI-Bridge".
|
|
255
|
+
if payload.iss != "DVAI-Bridge" {
|
|
256
|
+
return .freeProd(reason:
|
|
257
|
+
"license token issuer is \"\(payload.iss)\", expected \"DVAI-Bridge\""
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Expiry: if exp is in the past, surface a specific freeExpired
|
|
262
|
+
// status (the licensee + when so the dashboard can prompt
|
|
263
|
+
// renewal).
|
|
264
|
+
let nowSeconds = Int64(Date().timeIntervalSince1970)
|
|
265
|
+
if payload.exp <= nowSeconds {
|
|
266
|
+
return .freeExpired(licensee: payload.licensee, expiredAt: payload.exp)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Tier must be one of the live tiers.
|
|
270
|
+
let tier: LicenseTier
|
|
271
|
+
switch payload.tier {
|
|
272
|
+
case "commercial": tier = .commercial
|
|
273
|
+
case "trial": tier = .trial
|
|
274
|
+
default:
|
|
275
|
+
return .freeProd(reason:
|
|
276
|
+
"license token tier \"\(payload.tier)\" is not recognised; " +
|
|
277
|
+
"expected \"commercial\" or \"trial\""
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Platforms must include our runtime platform.
|
|
282
|
+
if !payload.platforms.contains(platform.rawValue) {
|
|
283
|
+
return .freeProd(reason:
|
|
284
|
+
"license token does not authorise platform \"\(platform.rawValue)\"; " +
|
|
285
|
+
"the token covers [\(payload.platforms.joined(separator: ", "))]"
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Audience must match (exact / wildcard / *).
|
|
290
|
+
guard let matched = matchAudience(runtimeAudience: runtimeAudience, audClaim: payload.aud) else {
|
|
291
|
+
let noneSuffix = runtimeAudience == nil
|
|
292
|
+
? " — set DVAI_AUDIENCE in your environment, or use a \"*\" aud entry for any-domain licenses"
|
|
293
|
+
: ""
|
|
294
|
+
return .freeProd(reason:
|
|
295
|
+
"license token's audience entries [\(payload.aud.joined(separator: ", "))] " +
|
|
296
|
+
"do not match the current runtime audience \"\(runtimeAudience ?? "(none)")\"" +
|
|
297
|
+
noneSuffix
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
switch tier {
|
|
302
|
+
case .commercial:
|
|
303
|
+
return .commercial(
|
|
304
|
+
licensee: payload.licensee,
|
|
305
|
+
expiresAt: payload.exp,
|
|
306
|
+
platform: platform,
|
|
307
|
+
audienceMatched: matched
|
|
308
|
+
)
|
|
309
|
+
case .trial:
|
|
310
|
+
return .trial(
|
|
311
|
+
licensee: payload.licensee,
|
|
312
|
+
expiresAt: payload.exp,
|
|
313
|
+
platform: platform,
|
|
314
|
+
audienceMatched: matched
|
|
315
|
+
)
|
|
316
|
+
default:
|
|
317
|
+
// Unreachable (we filtered above), but the compiler can't
|
|
318
|
+
// see that.
|
|
319
|
+
return .freeProd(reason: "internal validator state error")
|
|
320
|
+
}
|
|
321
|
+
#endif
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// MARK: - Helpers
|
|
326
|
+
|
|
327
|
+
/// Decode a base64url string (RFC 4648 §5) into raw bytes. The JWT
|
|
328
|
+
/// header / payload segments use this encoding (no padding, `-` and `_`
|
|
329
|
+
/// instead of `+` and `/`).
|
|
330
|
+
private func base64UrlDecode(_ s: String) -> Data? {
|
|
331
|
+
var b64 = s.replacingOccurrences(of: "-", with: "+")
|
|
332
|
+
.replacingOccurrences(of: "_", with: "/")
|
|
333
|
+
let pad = b64.count % 4
|
|
334
|
+
if pad > 0 {
|
|
335
|
+
b64 += String(repeating: "=", count: 4 - pad)
|
|
336
|
+
}
|
|
337
|
+
return Data(base64Encoded: b64)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// Build the developer-facing error message for `LicenseRequiredError`.
|
|
341
|
+
/// Intentionally verbose: it tells the developer exactly what failed,
|
|
342
|
+
/// how to resolve it, where to put the license file, and how to bypass
|
|
343
|
+
/// for local development. This message will be printed to Xcode's
|
|
344
|
+
/// console or a crash log — make it readable in both.
|
|
345
|
+
internal func buildRequiredErrorMessage(status: LicenseStatus) -> String {
|
|
346
|
+
let header = """
|
|
347
|
+
|
|
348
|
+
DVAI-Bridge Commercial License Required
|
|
349
|
+
=======================================
|
|
350
|
+
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
let reason: String
|
|
354
|
+
switch status {
|
|
355
|
+
case .freeExpired(let licensee, let expiredAt):
|
|
356
|
+
let date = Date(timeIntervalSince1970: TimeInterval(expiredAt))
|
|
357
|
+
let formatter = ISO8601DateFormatter()
|
|
358
|
+
formatter.formatOptions = [.withInternetDateTime]
|
|
359
|
+
reason = "License for \"\(licensee)\" expired at \(formatter.string(from: date))."
|
|
360
|
+
case .freeProd(let r):
|
|
361
|
+
reason = r
|
|
362
|
+
default:
|
|
363
|
+
reason = "(unknown status)"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let remediation = """
|
|
367
|
+
|
|
368
|
+
This SDK is licensed under BSL 1.1 and requires a valid commercial
|
|
369
|
+
or trial license to run in production / release builds.
|
|
370
|
+
|
|
371
|
+
To resolve:
|
|
372
|
+
1. Obtain a license at https://deepvoiceai.com/dvai-bridge/license
|
|
373
|
+
2. Place the file at one of these locations (any will work):
|
|
374
|
+
- <App.bundle>/dvai-license.jwt (add to Copy Bundle Resources)
|
|
375
|
+
- the path you pass as LicenseValidatorOptions.path
|
|
376
|
+
- the path in $DVAI_LICENSE_PATH
|
|
377
|
+
- inline JWT in LicenseValidatorOptions.token or $DVAI_LICENSE_TOKEN
|
|
378
|
+
- <Application Support>/dvai-bridge/dvai-license.jwt
|
|
379
|
+
- <Documents>/dvai-license.jwt
|
|
380
|
+
3. Re-run.
|
|
381
|
+
|
|
382
|
+
Developing locally? The SDK auto-detects dev mode on:
|
|
383
|
+
- DEBUG build configuration (compile-time #if DEBUG)
|
|
384
|
+
- iOS Simulator (compile-time targetEnvironment(simulator))
|
|
385
|
+
- DVAI_FORCE_DEV=1 environment variable (explicit override)
|
|
386
|
+
Any of these silences this error and lets the SDK run without a
|
|
387
|
+
license.
|
|
388
|
+
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
return header + "\n" + reason + "\n" + remediation
|
|
392
|
+
}
|