Pre-init license inspection
Run the license validator on its own — outside the SDK's startup sequence. Three good reasons:
- Show license state in a dashboard or settings UI — the licensee, the expiry, the tier — without paying the full cost of
DVAI.initialize()/DVAIBridge.start()(which loads models, starts the embedded HTTP server, runs backend init). - Verify before a download — a setup wizard checks the license file before the user commits to a model download.
- Smoke-test in CI — confirm a license artifact is valid before the full integration test runs.
Same LicenseValidator class the SDK uses inside initialize(). Same JWT format. Same claim checks. Same dev-mode bypass rules. Everywhere.
TypeScript / Node / Browser / Capacitor JS layer
@dvai-bridge/core:
import { LicenseValidator, type LicenseStatus } from "@dvai-bridge/core";
const status: LicenseStatus = await new LicenseValidator().validate();
// status.kind ∈ "commercial" | "trial" | "free-dev" | "free-prod" | "free-expired"
switch (status.kind) {
case "commercial":
case "trial":
console.log(`Licensed to ${status.licensee} until ${new Date(status.expiresAt * 1000).toISOString()}`);
break;
case "free-dev":
console.log("Running in dev mode — no license required");
break;
case "free-prod":
console.warn(`License needed before SDK can start: ${status.reason}`);
break;
case "free-expired":
console.warn(`License expired for ${status.licensee}`);
break;
}validate() never throws. Use validateAndAssert() for the same throw-on-prod behavior the SDK runs at startup.
Swift (iOS / macOS / Mac Catalyst)
DVAIBridge:
import DVAIBridge
let validator = LicenseValidator()
let status = await validator.validate()
switch status {
case .commercial(let licensee, let expiresAt, _, _):
print("Licensed to \(licensee), expires \(Date(timeIntervalSince1970: TimeInterval(expiresAt)))")
case .trial(let licensee, let expiresAt, _, _):
print("Trial license for \(licensee) until \(Date(timeIntervalSince1970: TimeInterval(expiresAt)))")
case .freeDev(let reason):
print("Dev mode: \(reason)")
case .freeProd(let reason):
print("License required: \(reason)")
case .freeExpired(let licensee, let expiredAt):
print("Expired license for \(licensee) at \(Date(timeIntervalSince1970: TimeInterval(expiredAt)))")
}Throw variant: try await validator.validateAndAssert().
Kotlin (Android)
co.deepvoiceai.bridge.license.LicenseValidator:
import android.content.Context
import co.deepvoiceai.bridge.license.LicenseStatus
import co.deepvoiceai.bridge.license.LicenseValidator
// In a Coroutine scope or suspend function:
suspend fun checkLicense(context: Context, isDebugBuild: Boolean) {
val validator = LicenseValidator(
context = context.applicationContext,
hostBuildConfigDebug = isDebugBuild, // pass your app's BuildConfig.DEBUG
)
when (val status = validator.validate()) {
is LicenseStatus.Commercial ->
Log.i("DVAI", "Licensed to ${status.licensee} until ${status.expiresAt}")
is LicenseStatus.Trial ->
Log.i("DVAI", "Trial license for ${status.licensee}")
is LicenseStatus.FreeDev ->
Log.d("DVAI", "Dev mode: ${status.reason}")
is LicenseStatus.FreeProd ->
Log.w("DVAI", "License required: ${status.reason}")
is LicenseStatus.FreeExpired ->
Log.w("DVAI", "Expired license for ${status.licensee}")
}
}The Kotlin validator needs the Context — it reads packageName for audience binding and discovers assets / raw resources. Pass your host app's BuildConfig.DEBUG so the dev-mode bypass picks up the right value. The library module's own BuildConfig.DEBUG doesn't reflect your app's build variant.
C# / .NET (MAUI, Avalonia, WinUI, Desktop)
DVAIBridge.License:
using DVAIBridge.License;
var validator = new LicenseValidator();
var status = await validator.ValidateAsync();
switch (status)
{
case LicenseStatus.Commercial c:
Console.WriteLine($"Licensed to {c.Licensee} until {DateTimeOffset.FromUnixTimeSeconds(c.ExpiresAt)}");
break;
case LicenseStatus.Trial t:
Console.WriteLine($"Trial license for {t.Licensee}");
break;
case LicenseStatus.FreeDev d:
Console.WriteLine($"Dev mode: {d.Reason}");
break;
case LicenseStatus.FreeProd p:
Console.WriteLine($"License required: {p.Reason}");
break;
case LicenseStatus.FreeExpired e:
Console.WriteLine($"Expired license for {e.Licensee}");
break;
}Throw variant: await validator.ValidateAndAssertAsync() — throws LicenseRequiredException.
Dart (Flutter)
package:dvai_bridge/dvai_bridge.dart:
import 'package:dvai_bridge/dvai_bridge.dart';
final validator = LicenseValidator();
final status = await validator.validate();
// status is a sealed class — switch on the runtime type:
switch (status) {
case Commercial(:final licensee, :final expiresAt):
print('Licensed to $licensee until ${DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000)}');
case Trial(:final licensee):
print('Trial license for $licensee');
case FreeDev(:final reason):
print('Dev mode: $reason');
case FreeProd(:final reason):
print('License required: $reason');
case FreeExpired(:final licensee):
print('Expired license for $licensee');
}Throw variant: await validator.validateAndAssert() — throws LicenseRequiredException.
React Native + Capacitor
These wrappers don't ship a JS-side LicenseValidator. The license check happens on the native side (Swift / Kotlin) at startup.
To run pre-init validation from the JS layer:
- Capacitor — install
@dvai-bridge/coreas a regular dependency alongside@dvai-bridge/capacitorand use the TypeScript example above. Core's validator reads the samedvai-license.jwtyour app already ships — a same-origin fetch frompublic/. - React Native — same approach. Install
@dvai-bridge/coreand use the TypeScript example. One catch — audience binding from a Node-style context has nowindow.location.hostname. Either passDVAI_AUDIENCEvia aprocess.envshim, or pre-read your bundle id via the native module and pass it throughaudienceOverride.
Same JWT file works everywhere
All five SDK validators read the same dvai-license.jwt format. They verify with the same kid-keyed public-key registry. One license issued by dvai-license-generator with platforms ["ios", "android", "web", "dotnet", "flutter", "react-native", "capacitor", "node"] activates across every SDK that consumes it.
See also
- License setup overview — per-platform file-drop walkthrough
- Migration v3.x → v4.0 — the throw-on-prod policy + field rename context
