@dvai-bridge/android 4.0.0 → 4.0.2
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/android/build.gradle +252 -165
- package/android/gradle.properties +1 -1
- package/android/src/main/java/co/deepvoiceai/bridge/BoundServer.kt +39 -39
- package/android/src/main/java/co/deepvoiceai/bridge/DVAIBridge.kt +642 -642
- package/android/src/main/java/co/deepvoiceai/bridge/DVAIBridgeConfig.kt +119 -119
- package/android/src/main/java/co/deepvoiceai/bridge/license/Audience.kt +134 -134
- package/android/src/main/java/co/deepvoiceai/bridge/license/Discovery.kt +146 -146
- package/android/src/main/java/co/deepvoiceai/bridge/license/LicenseTypes.kt +158 -158
- package/android/src/main/java/co/deepvoiceai/bridge/license/LicenseValidator.kt +400 -400
- package/android/src/main/java/co/deepvoiceai/bridge/license/PublicKeys.kt +91 -91
- package/android/src/test/java/co/deepvoiceai/bridge/license/LicenseValidatorTest.kt +539 -539
- package/package.json +1 -1
- package/LICENSE +0 -51
- package/README.md +0 -199
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
package co.deepvoiceai.bridge
|
|
2
|
-
|
|
3
|
-
import co.deepvoiceai.bridge.shared.core.CorsConfig
|
|
4
|
-
import co.deepvoiceai.bridge.shared.core.offload.OffloadConfig
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Options accepted by [DVAIBridge.start]. Mirrors the iOS DVAIBridge
|
|
8
|
-
* `StartOptions` struct + the Capacitor JS shim's StartOptions.
|
|
9
|
-
*
|
|
10
|
-
* @param backend Which backend to use. [BackendKind.Auto] resolves
|
|
11
|
-
* via [BackendSelector] at start-time.
|
|
12
|
-
* @param modelPath Filesystem path to the model checkpoint. Required
|
|
13
|
-
* for Llama (.gguf), MediaPipe (.task), and LiteRT
|
|
14
|
-
* (.tflite / .litertlm) backends.
|
|
15
|
-
* @param tokenizerPath Filesystem path to a directory containing
|
|
16
|
-
* tokenizer.json (and optional tokenizer_config.json).
|
|
17
|
-
* Required for the LiteRT backend; ignored otherwise.
|
|
18
|
-
* @param mmprojPath Optional multimodal projector path for Llama
|
|
19
|
-
* (vision/audio LLMs).
|
|
20
|
-
* @param chatTemplate Optional Jinja chat template override (Llama backend
|
|
21
|
-
* only). Falls back to the model's bundled template.
|
|
22
|
-
* @param gpuLayers Llama backend: number of transformer layers to
|
|
23
|
-
* offload to GPU (Vulkan / OpenCL on supported
|
|
24
|
-
* devices). 99 = all layers, 0 = CPU only. Ignored
|
|
25
|
-
* by other backends.
|
|
26
|
-
* @param contextSize Context window in tokens. Defaults to 2048.
|
|
27
|
-
* @param threads CPU thread count for the inference loop. Llama
|
|
28
|
-
* uses this directly; LiteRT/MediaPipe pick their
|
|
29
|
-
* own threading by default.
|
|
30
|
-
* @param embeddingMode Llama backend: open the model in
|
|
31
|
-
* embedding-extraction mode rather than completion
|
|
32
|
-
* mode. Mutually exclusive with chat completion.
|
|
33
|
-
* @param visionEnabled MediaPipe backend: open the LiteRT-LM EngineConfig
|
|
34
|
-
* with `visionBackend` enabled.
|
|
35
|
-
* @param temperature LiteRT backend: sampling temperature (0 = greedy).
|
|
36
|
-
* @param topP LiteRT backend: nucleus sampling cutoff (1 = disabled).
|
|
37
|
-
* @param topK LiteRT backend: top-K truncation (0 = disabled).
|
|
38
|
-
* @param maxNewTokens LiteRT backend: hard cap on tokens generated per request.
|
|
39
|
-
* @param httpBasePort First port the HTTP server tries to bind. Defaults
|
|
40
|
-
* to 38883 (matches the rest of the dvai-bridge family).
|
|
41
|
-
* @param httpMaxPortAttempts Number of consecutive ports to try before giving
|
|
42
|
-
* up. Defaults to 16.
|
|
43
|
-
* @param corsOrigin CORS allow-origin policy. See [CorsConfig].
|
|
44
|
-
* Defaults to wildcard.
|
|
45
|
-
* @param modelId Optional override for the model id surfaced via
|
|
46
|
-
* `/v1/models`. Defaults to the file name minus
|
|
47
|
-
* extension when null.
|
|
48
|
-
*/
|
|
49
|
-
data class StartOptions(
|
|
50
|
-
val backend: BackendKind = BackendKind.Auto,
|
|
51
|
-
val modelPath: String? = null,
|
|
52
|
-
val tokenizerPath: String? = null,
|
|
53
|
-
val mmprojPath: String? = null,
|
|
54
|
-
val chatTemplate: String? = null,
|
|
55
|
-
val gpuLayers: Int = 99,
|
|
56
|
-
val contextSize: Int = 2048,
|
|
57
|
-
val threads: Int = 4,
|
|
58
|
-
val embeddingMode: Boolean = false,
|
|
59
|
-
val visionEnabled: Boolean = false,
|
|
60
|
-
val temperature: Float = 0f,
|
|
61
|
-
val topP: Float = 1f,
|
|
62
|
-
val topK: Int = 0,
|
|
63
|
-
val maxNewTokens: Int = 512,
|
|
64
|
-
val httpBasePort: Int = 38883,
|
|
65
|
-
val httpMaxPortAttempts: Int = 16,
|
|
66
|
-
val corsOrigin: CorsConfig = CorsConfig.Wildcard,
|
|
67
|
-
val modelId: String? = null,
|
|
68
|
-
/**
|
|
69
|
-
* Phase 3 — opt-in distributed inference / device offload. When
|
|
70
|
-
* `enabled = true`, [DVAIBridge] spins up an [NsdDiscovery] +
|
|
71
|
-
* [NsdAdvertiser], a [CapabilityCache], and a [PairingPolicy]
|
|
72
|
-
* whose `requests` Flow is exposed via `DVAIBridge.pairingRequests`
|
|
73
|
-
* for the host UI. Default null = behave exactly like v2.x.
|
|
74
|
-
*/
|
|
75
|
-
val offload: OffloadConfig? = null,
|
|
76
|
-
/**
|
|
77
|
-
* v3.3 — offline JWT license validator config. When non-null, the
|
|
78
|
-
* SDK loads the JWT from this filesystem path and verifies it at
|
|
79
|
-
* startup. Auto-discovery (assets/, res/raw/, filesDir/) runs when
|
|
80
|
-
* BOTH this AND [licenseToken] are null.
|
|
81
|
-
*
|
|
82
|
-
* Production Android builds without a valid license throw
|
|
83
|
-
* [co.deepvoiceai.bridge.license.LicenseRequiredError] from
|
|
84
|
-
* [DVAIBridge.start]. Debug builds (`hostBuildConfigDebug = true`
|
|
85
|
-
* or `ApplicationInfo.FLAG_DEBUGGABLE`) skip validation entirely.
|
|
86
|
-
*/
|
|
87
|
-
val licenseKeyPath: String? = null,
|
|
88
|
-
/**
|
|
89
|
-
* v3.3 — inline JWT license token. Overrides every other discovery
|
|
90
|
-
* source. Useful for CI / test contexts where reading a file isn't
|
|
91
|
-
* practical and operators inject via env var or build config.
|
|
92
|
-
*/
|
|
93
|
-
val licenseToken: String? = null,
|
|
94
|
-
/**
|
|
95
|
-
* v3.3 — the host app's `BuildConfig.DEBUG` value, passed through to
|
|
96
|
-
* the license validator's dev-mode bypass. When true the validator
|
|
97
|
-
* returns `FreeDev` without trying to verify anything; when false (or
|
|
98
|
-
* null) the validator falls back to `ApplicationInfo.FLAG_DEBUGGABLE`.
|
|
99
|
-
*
|
|
100
|
-
* Pass `BuildConfig.DEBUG` from your app module here — the validator
|
|
101
|
-
* lives in this library module whose own `BuildConfig.DEBUG` never
|
|
102
|
-
* reflects the host app's state.
|
|
103
|
-
*/
|
|
104
|
-
val hostBuildConfigDebug: Boolean? = null,
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
/** Options for [DVAIBridge.downloadModel]. */
|
|
108
|
-
data class DownloadOptions(
|
|
109
|
-
val url: String,
|
|
110
|
-
val sha256: String,
|
|
111
|
-
val destFilename: String,
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
/** Result of a successful [DVAIBridge.downloadModel] call. */
|
|
115
|
-
data class DownloadResult(
|
|
116
|
-
val path: String,
|
|
117
|
-
val sha256: String,
|
|
118
|
-
val sizeBytes: Long,
|
|
119
|
-
)
|
|
1
|
+
package co.deepvoiceai.bridge
|
|
2
|
+
|
|
3
|
+
import co.deepvoiceai.bridge.shared.core.CorsConfig
|
|
4
|
+
import co.deepvoiceai.bridge.shared.core.offload.OffloadConfig
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options accepted by [DVAIBridge.start]. Mirrors the iOS DVAIBridge
|
|
8
|
+
* `StartOptions` struct + the Capacitor JS shim's StartOptions.
|
|
9
|
+
*
|
|
10
|
+
* @param backend Which backend to use. [BackendKind.Auto] resolves
|
|
11
|
+
* via [BackendSelector] at start-time.
|
|
12
|
+
* @param modelPath Filesystem path to the model checkpoint. Required
|
|
13
|
+
* for Llama (.gguf), MediaPipe (.task), and LiteRT
|
|
14
|
+
* (.tflite / .litertlm) backends.
|
|
15
|
+
* @param tokenizerPath Filesystem path to a directory containing
|
|
16
|
+
* tokenizer.json (and optional tokenizer_config.json).
|
|
17
|
+
* Required for the LiteRT backend; ignored otherwise.
|
|
18
|
+
* @param mmprojPath Optional multimodal projector path for Llama
|
|
19
|
+
* (vision/audio LLMs).
|
|
20
|
+
* @param chatTemplate Optional Jinja chat template override (Llama backend
|
|
21
|
+
* only). Falls back to the model's bundled template.
|
|
22
|
+
* @param gpuLayers Llama backend: number of transformer layers to
|
|
23
|
+
* offload to GPU (Vulkan / OpenCL on supported
|
|
24
|
+
* devices). 99 = all layers, 0 = CPU only. Ignored
|
|
25
|
+
* by other backends.
|
|
26
|
+
* @param contextSize Context window in tokens. Defaults to 2048.
|
|
27
|
+
* @param threads CPU thread count for the inference loop. Llama
|
|
28
|
+
* uses this directly; LiteRT/MediaPipe pick their
|
|
29
|
+
* own threading by default.
|
|
30
|
+
* @param embeddingMode Llama backend: open the model in
|
|
31
|
+
* embedding-extraction mode rather than completion
|
|
32
|
+
* mode. Mutually exclusive with chat completion.
|
|
33
|
+
* @param visionEnabled MediaPipe backend: open the LiteRT-LM EngineConfig
|
|
34
|
+
* with `visionBackend` enabled.
|
|
35
|
+
* @param temperature LiteRT backend: sampling temperature (0 = greedy).
|
|
36
|
+
* @param topP LiteRT backend: nucleus sampling cutoff (1 = disabled).
|
|
37
|
+
* @param topK LiteRT backend: top-K truncation (0 = disabled).
|
|
38
|
+
* @param maxNewTokens LiteRT backend: hard cap on tokens generated per request.
|
|
39
|
+
* @param httpBasePort First port the HTTP server tries to bind. Defaults
|
|
40
|
+
* to 38883 (matches the rest of the dvai-bridge family).
|
|
41
|
+
* @param httpMaxPortAttempts Number of consecutive ports to try before giving
|
|
42
|
+
* up. Defaults to 16.
|
|
43
|
+
* @param corsOrigin CORS allow-origin policy. See [CorsConfig].
|
|
44
|
+
* Defaults to wildcard.
|
|
45
|
+
* @param modelId Optional override for the model id surfaced via
|
|
46
|
+
* `/v1/models`. Defaults to the file name minus
|
|
47
|
+
* extension when null.
|
|
48
|
+
*/
|
|
49
|
+
data class StartOptions(
|
|
50
|
+
val backend: BackendKind = BackendKind.Auto,
|
|
51
|
+
val modelPath: String? = null,
|
|
52
|
+
val tokenizerPath: String? = null,
|
|
53
|
+
val mmprojPath: String? = null,
|
|
54
|
+
val chatTemplate: String? = null,
|
|
55
|
+
val gpuLayers: Int = 99,
|
|
56
|
+
val contextSize: Int = 2048,
|
|
57
|
+
val threads: Int = 4,
|
|
58
|
+
val embeddingMode: Boolean = false,
|
|
59
|
+
val visionEnabled: Boolean = false,
|
|
60
|
+
val temperature: Float = 0f,
|
|
61
|
+
val topP: Float = 1f,
|
|
62
|
+
val topK: Int = 0,
|
|
63
|
+
val maxNewTokens: Int = 512,
|
|
64
|
+
val httpBasePort: Int = 38883,
|
|
65
|
+
val httpMaxPortAttempts: Int = 16,
|
|
66
|
+
val corsOrigin: CorsConfig = CorsConfig.Wildcard,
|
|
67
|
+
val modelId: String? = null,
|
|
68
|
+
/**
|
|
69
|
+
* Phase 3 — opt-in distributed inference / device offload. When
|
|
70
|
+
* `enabled = true`, [DVAIBridge] spins up an [NsdDiscovery] +
|
|
71
|
+
* [NsdAdvertiser], a [CapabilityCache], and a [PairingPolicy]
|
|
72
|
+
* whose `requests` Flow is exposed via `DVAIBridge.pairingRequests`
|
|
73
|
+
* for the host UI. Default null = behave exactly like v2.x.
|
|
74
|
+
*/
|
|
75
|
+
val offload: OffloadConfig? = null,
|
|
76
|
+
/**
|
|
77
|
+
* v3.3 — offline JWT license validator config. When non-null, the
|
|
78
|
+
* SDK loads the JWT from this filesystem path and verifies it at
|
|
79
|
+
* startup. Auto-discovery (assets/, res/raw/, filesDir/) runs when
|
|
80
|
+
* BOTH this AND [licenseToken] are null.
|
|
81
|
+
*
|
|
82
|
+
* Production Android builds without a valid license throw
|
|
83
|
+
* [co.deepvoiceai.bridge.license.LicenseRequiredError] from
|
|
84
|
+
* [DVAIBridge.start]. Debug builds (`hostBuildConfigDebug = true`
|
|
85
|
+
* or `ApplicationInfo.FLAG_DEBUGGABLE`) skip validation entirely.
|
|
86
|
+
*/
|
|
87
|
+
val licenseKeyPath: String? = null,
|
|
88
|
+
/**
|
|
89
|
+
* v3.3 — inline JWT license token. Overrides every other discovery
|
|
90
|
+
* source. Useful for CI / test contexts where reading a file isn't
|
|
91
|
+
* practical and operators inject via env var or build config.
|
|
92
|
+
*/
|
|
93
|
+
val licenseToken: String? = null,
|
|
94
|
+
/**
|
|
95
|
+
* v3.3 — the host app's `BuildConfig.DEBUG` value, passed through to
|
|
96
|
+
* the license validator's dev-mode bypass. When true the validator
|
|
97
|
+
* returns `FreeDev` without trying to verify anything; when false (or
|
|
98
|
+
* null) the validator falls back to `ApplicationInfo.FLAG_DEBUGGABLE`.
|
|
99
|
+
*
|
|
100
|
+
* Pass `BuildConfig.DEBUG` from your app module here — the validator
|
|
101
|
+
* lives in this library module whose own `BuildConfig.DEBUG` never
|
|
102
|
+
* reflects the host app's state.
|
|
103
|
+
*/
|
|
104
|
+
val hostBuildConfigDebug: Boolean? = null,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
/** Options for [DVAIBridge.downloadModel]. */
|
|
108
|
+
data class DownloadOptions(
|
|
109
|
+
val url: String,
|
|
110
|
+
val sha256: String,
|
|
111
|
+
val destFilename: String,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
/** Result of a successful [DVAIBridge.downloadModel] call. */
|
|
115
|
+
data class DownloadResult(
|
|
116
|
+
val path: String,
|
|
117
|
+
val sha256: String,
|
|
118
|
+
val sizeBytes: Long,
|
|
119
|
+
)
|
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
package co.deepvoiceai.bridge.license
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.content.pm.ApplicationInfo
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Runtime audience + platform + dev-mode detection for the Android SDK.
|
|
8
|
-
*
|
|
9
|
-
* Kotlin port of `packages/dvai-bridge-core/src/license/audience.ts`.
|
|
10
|
-
* The semantics are identical to the JS side but the platform APIs
|
|
11
|
-
* differ — audience is read from [Context.getPackageName] rather than
|
|
12
|
-
* `window.location.hostname`, and dev-mode is detected via
|
|
13
|
-
* `BuildConfig.DEBUG` + `ApplicationInfo.FLAG_DEBUGGABLE` rather than
|
|
14
|
-
* hostname heuristics.
|
|
15
|
-
*
|
|
16
|
-
* "Audience" on Android = the host app's package name (e.g.
|
|
17
|
-
* `"com.acme.app"`). License JWTs bind to package names exactly like
|
|
18
|
-
* iOS bundle ids; subdomain-style `*.acme.com` wildcards work for
|
|
19
|
-
* domain-tail matching too.
|
|
20
|
-
*
|
|
21
|
-
* "Dev mode" detection bypasses license enforcement entirely so
|
|
22
|
-
* developers don't need a license to run the SDK on a debug build.
|
|
23
|
-
* This matches the JS-side `detectDevMode()` behaviour.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/** Detect the current SDK platform identifier. Always [DvaiPlatform.ANDROID]. */
|
|
27
|
-
fun detectPlatform(): DvaiPlatform = DvaiPlatform.ANDROID
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Detect the current audience string the license must bind. Returns
|
|
31
|
-
* the host app's package name from [Context.getPackageName].
|
|
32
|
-
*
|
|
33
|
-
* Returns null only if [context] is null — in practice this never
|
|
34
|
-
* happens because [LicenseValidator] requires a non-null context at
|
|
35
|
-
* construction.
|
|
36
|
-
*/
|
|
37
|
-
fun detectAudience(context: Context?): String? = context?.packageName
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Detect whether the SDK is running in a developer environment where
|
|
41
|
-
* license enforcement should be bypassed. The bypass list is
|
|
42
|
-
* intentionally generous: blocking a developer mid-`./gradlew installDebug`
|
|
43
|
-
* with a license-not-found error would be hostile.
|
|
44
|
-
*
|
|
45
|
-
* Precedence (highest first):
|
|
46
|
-
* 1. `DVAI_FORCE_PROD=1` env var — production mode forced, overrides DEBUG.
|
|
47
|
-
* 2. `DVAI_FORCE_DEV=1` env var — dev mode forced.
|
|
48
|
-
* 3. [hostBuildConfigDebug] (a `BuildConfig.DEBUG` value passed in by the
|
|
49
|
-
* host app via [co.deepvoiceai.bridge.StartOptions]) — dev mode.
|
|
50
|
-
* 4. [ApplicationInfo.FLAG_DEBUGGABLE] on the host app — dev mode.
|
|
51
|
-
* 5. Otherwise production, license required.
|
|
52
|
-
*
|
|
53
|
-
* @param context Application context — used to read
|
|
54
|
-
* `ApplicationInfo.FLAG_DEBUGGABLE`.
|
|
55
|
-
* @param hostBuildConfigDebug The host app's `BuildConfig.DEBUG`, passed in
|
|
56
|
-
* explicitly because the validator lives in a
|
|
57
|
-
* library module whose own `BuildConfig.DEBUG`
|
|
58
|
-
* never reflects the host app's state. Pass null
|
|
59
|
-
* to skip this check.
|
|
60
|
-
*/
|
|
61
|
-
fun detectDevMode(
|
|
62
|
-
context: Context?,
|
|
63
|
-
hostBuildConfigDebug: Boolean? = null,
|
|
64
|
-
): DevModeResult {
|
|
65
|
-
// 1. Explicit env-var overrides win — these mirror the JS side so the
|
|
66
|
-
// same DVAI_FORCE_PROD / DVAI_FORCE_DEV semantics work in CI/test
|
|
67
|
-
// contexts on any platform.
|
|
68
|
-
val forceProd = System.getenv("DVAI_FORCE_PROD")
|
|
69
|
-
if (forceProd == "1" || forceProd == "true") {
|
|
70
|
-
return DevModeResult(isDev = false, reason = "DVAI_FORCE_PROD set")
|
|
71
|
-
}
|
|
72
|
-
val forceDev = System.getenv("DVAI_FORCE_DEV")
|
|
73
|
-
if (forceDev == "1" || forceDev == "true") {
|
|
74
|
-
return DevModeResult(isDev = true, reason = "DVAI_FORCE_DEV set")
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 2. Host app's BuildConfig.DEBUG (explicit, preferred). When the host
|
|
78
|
-
// passes a non-null value we treat it as authoritative — the host
|
|
79
|
-
// knows whether they're a debug build better than we do. When null,
|
|
80
|
-
// we fall through to the manifest-flag heuristic.
|
|
81
|
-
when (hostBuildConfigDebug) {
|
|
82
|
-
true -> return DevModeResult(isDev = true, reason = "BuildConfig.DEBUG=true")
|
|
83
|
-
false -> return DevModeResult(isDev = false, reason = "BuildConfig.DEBUG=false (host-supplied)")
|
|
84
|
-
null -> { /* fall through */ }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 3. ApplicationInfo.FLAG_DEBUGGABLE — set on debug builds and on apps
|
|
88
|
-
// with `android:debuggable="true"` in the manifest. Fallback when the
|
|
89
|
-
// host hasn't wired their BuildConfig.DEBUG through to StartOptions.
|
|
90
|
-
if (context != null) {
|
|
91
|
-
val flags = context.applicationInfo.flags
|
|
92
|
-
if ((flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
|
|
93
|
-
return DevModeResult(isDev = true, reason = "ApplicationInfo.FLAG_DEBUGGABLE set")
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return DevModeResult(isDev = false, reason = "production-class environment")
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Outcome of [detectDevMode]. */
|
|
101
|
-
data class DevModeResult(val isDev: Boolean, val reason: String)
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Decide whether a license-payload `aud` entry matches the current
|
|
105
|
-
* runtime audience. Supports exact match and `*.example.com` wildcard
|
|
106
|
-
* matching for subdomain binding. Returns the matched `aud` pattern
|
|
107
|
-
* on success so it can be recorded for audit, or null on miss.
|
|
108
|
-
*
|
|
109
|
-
* Match rules (identical to the JS side):
|
|
110
|
-
* - `"foo"` matches `"foo"` exactly
|
|
111
|
-
* - `"*.example.com"` matches `"example.com"` AND any `"<sub>.example.com"`
|
|
112
|
-
* - `"*"` matches any non-empty audience (intentionally permissive; use
|
|
113
|
-
* for trial/site licenses that span all of a customer's deployments)
|
|
114
|
-
*
|
|
115
|
-
* Runtime audience of `null` matches `"*"` only.
|
|
116
|
-
*/
|
|
117
|
-
fun matchAudience(runtimeAudience: String?, audClaim: List<String>): String? {
|
|
118
|
-
if (runtimeAudience == null) {
|
|
119
|
-
return if (audClaim.contains("*")) "*" else null
|
|
120
|
-
}
|
|
121
|
-
val runtime = runtimeAudience.lowercase()
|
|
122
|
-
for (pattern in audClaim) {
|
|
123
|
-
val p = pattern.lowercase()
|
|
124
|
-
if (p == "*") return pattern // permissive wildcard
|
|
125
|
-
if (p == runtime) return pattern // exact match
|
|
126
|
-
if (p.startsWith("*.")) {
|
|
127
|
-
val suffix = p.substring(2)
|
|
128
|
-
if (runtime == suffix || runtime.endsWith(".$suffix")) {
|
|
129
|
-
return pattern
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return null
|
|
134
|
-
}
|
|
1
|
+
package co.deepvoiceai.bridge.license
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.ApplicationInfo
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runtime audience + platform + dev-mode detection for the Android SDK.
|
|
8
|
+
*
|
|
9
|
+
* Kotlin port of `packages/dvai-bridge-core/src/license/audience.ts`.
|
|
10
|
+
* The semantics are identical to the JS side but the platform APIs
|
|
11
|
+
* differ — audience is read from [Context.getPackageName] rather than
|
|
12
|
+
* `window.location.hostname`, and dev-mode is detected via
|
|
13
|
+
* `BuildConfig.DEBUG` + `ApplicationInfo.FLAG_DEBUGGABLE` rather than
|
|
14
|
+
* hostname heuristics.
|
|
15
|
+
*
|
|
16
|
+
* "Audience" on Android = the host app's package name (e.g.
|
|
17
|
+
* `"com.acme.app"`). License JWTs bind to package names exactly like
|
|
18
|
+
* iOS bundle ids; subdomain-style `*.acme.com` wildcards work for
|
|
19
|
+
* domain-tail matching too.
|
|
20
|
+
*
|
|
21
|
+
* "Dev mode" detection bypasses license enforcement entirely so
|
|
22
|
+
* developers don't need a license to run the SDK on a debug build.
|
|
23
|
+
* This matches the JS-side `detectDevMode()` behaviour.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Detect the current SDK platform identifier. Always [DvaiPlatform.ANDROID]. */
|
|
27
|
+
fun detectPlatform(): DvaiPlatform = DvaiPlatform.ANDROID
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detect the current audience string the license must bind. Returns
|
|
31
|
+
* the host app's package name from [Context.getPackageName].
|
|
32
|
+
*
|
|
33
|
+
* Returns null only if [context] is null — in practice this never
|
|
34
|
+
* happens because [LicenseValidator] requires a non-null context at
|
|
35
|
+
* construction.
|
|
36
|
+
*/
|
|
37
|
+
fun detectAudience(context: Context?): String? = context?.packageName
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect whether the SDK is running in a developer environment where
|
|
41
|
+
* license enforcement should be bypassed. The bypass list is
|
|
42
|
+
* intentionally generous: blocking a developer mid-`./gradlew installDebug`
|
|
43
|
+
* with a license-not-found error would be hostile.
|
|
44
|
+
*
|
|
45
|
+
* Precedence (highest first):
|
|
46
|
+
* 1. `DVAI_FORCE_PROD=1` env var — production mode forced, overrides DEBUG.
|
|
47
|
+
* 2. `DVAI_FORCE_DEV=1` env var — dev mode forced.
|
|
48
|
+
* 3. [hostBuildConfigDebug] (a `BuildConfig.DEBUG` value passed in by the
|
|
49
|
+
* host app via [co.deepvoiceai.bridge.StartOptions]) — dev mode.
|
|
50
|
+
* 4. [ApplicationInfo.FLAG_DEBUGGABLE] on the host app — dev mode.
|
|
51
|
+
* 5. Otherwise production, license required.
|
|
52
|
+
*
|
|
53
|
+
* @param context Application context — used to read
|
|
54
|
+
* `ApplicationInfo.FLAG_DEBUGGABLE`.
|
|
55
|
+
* @param hostBuildConfigDebug The host app's `BuildConfig.DEBUG`, passed in
|
|
56
|
+
* explicitly because the validator lives in a
|
|
57
|
+
* library module whose own `BuildConfig.DEBUG`
|
|
58
|
+
* never reflects the host app's state. Pass null
|
|
59
|
+
* to skip this check.
|
|
60
|
+
*/
|
|
61
|
+
fun detectDevMode(
|
|
62
|
+
context: Context?,
|
|
63
|
+
hostBuildConfigDebug: Boolean? = null,
|
|
64
|
+
): DevModeResult {
|
|
65
|
+
// 1. Explicit env-var overrides win — these mirror the JS side so the
|
|
66
|
+
// same DVAI_FORCE_PROD / DVAI_FORCE_DEV semantics work in CI/test
|
|
67
|
+
// contexts on any platform.
|
|
68
|
+
val forceProd = System.getenv("DVAI_FORCE_PROD")
|
|
69
|
+
if (forceProd == "1" || forceProd == "true") {
|
|
70
|
+
return DevModeResult(isDev = false, reason = "DVAI_FORCE_PROD set")
|
|
71
|
+
}
|
|
72
|
+
val forceDev = System.getenv("DVAI_FORCE_DEV")
|
|
73
|
+
if (forceDev == "1" || forceDev == "true") {
|
|
74
|
+
return DevModeResult(isDev = true, reason = "DVAI_FORCE_DEV set")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Host app's BuildConfig.DEBUG (explicit, preferred). When the host
|
|
78
|
+
// passes a non-null value we treat it as authoritative — the host
|
|
79
|
+
// knows whether they're a debug build better than we do. When null,
|
|
80
|
+
// we fall through to the manifest-flag heuristic.
|
|
81
|
+
when (hostBuildConfigDebug) {
|
|
82
|
+
true -> return DevModeResult(isDev = true, reason = "BuildConfig.DEBUG=true")
|
|
83
|
+
false -> return DevModeResult(isDev = false, reason = "BuildConfig.DEBUG=false (host-supplied)")
|
|
84
|
+
null -> { /* fall through */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. ApplicationInfo.FLAG_DEBUGGABLE — set on debug builds and on apps
|
|
88
|
+
// with `android:debuggable="true"` in the manifest. Fallback when the
|
|
89
|
+
// host hasn't wired their BuildConfig.DEBUG through to StartOptions.
|
|
90
|
+
if (context != null) {
|
|
91
|
+
val flags = context.applicationInfo.flags
|
|
92
|
+
if ((flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
|
|
93
|
+
return DevModeResult(isDev = true, reason = "ApplicationInfo.FLAG_DEBUGGABLE set")
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return DevModeResult(isDev = false, reason = "production-class environment")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Outcome of [detectDevMode]. */
|
|
101
|
+
data class DevModeResult(val isDev: Boolean, val reason: String)
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Decide whether a license-payload `aud` entry matches the current
|
|
105
|
+
* runtime audience. Supports exact match and `*.example.com` wildcard
|
|
106
|
+
* matching for subdomain binding. Returns the matched `aud` pattern
|
|
107
|
+
* on success so it can be recorded for audit, or null on miss.
|
|
108
|
+
*
|
|
109
|
+
* Match rules (identical to the JS side):
|
|
110
|
+
* - `"foo"` matches `"foo"` exactly
|
|
111
|
+
* - `"*.example.com"` matches `"example.com"` AND any `"<sub>.example.com"`
|
|
112
|
+
* - `"*"` matches any non-empty audience (intentionally permissive; use
|
|
113
|
+
* for trial/site licenses that span all of a customer's deployments)
|
|
114
|
+
*
|
|
115
|
+
* Runtime audience of `null` matches `"*"` only.
|
|
116
|
+
*/
|
|
117
|
+
fun matchAudience(runtimeAudience: String?, audClaim: List<String>): String? {
|
|
118
|
+
if (runtimeAudience == null) {
|
|
119
|
+
return if (audClaim.contains("*")) "*" else null
|
|
120
|
+
}
|
|
121
|
+
val runtime = runtimeAudience.lowercase()
|
|
122
|
+
for (pattern in audClaim) {
|
|
123
|
+
val p = pattern.lowercase()
|
|
124
|
+
if (p == "*") return pattern // permissive wildcard
|
|
125
|
+
if (p == runtime) return pattern // exact match
|
|
126
|
+
if (p.startsWith("*.")) {
|
|
127
|
+
val suffix = p.substring(2)
|
|
128
|
+
if (runtime == suffix || runtime.endsWith(".$suffix")) {
|
|
129
|
+
return pattern
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
}
|