@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,114 +1,114 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Public-key registry for DVAI-Bridge license JWT verification on iOS.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `packages/dvai-bridge-core/src/license/publicKeys.ts`.
|
|
5
|
-
*
|
|
6
|
-
* Each entry is keyed by `kid` (key id, written by the license generator
|
|
7
|
-
* into the JWT header). The SDK looks up the matching entry by kid when
|
|
8
|
-
* verifying a license token. Multiple entries can coexist so that key
|
|
9
|
-
* rotation is non-disruptive: ship the new key in a release alongside
|
|
10
|
-
* the old, leave the old in place for ~12 months while previously-
|
|
11
|
-
* issued licenses naturally expire or get re-issued, then prune.
|
|
12
|
-
*
|
|
13
|
-
* THE PRIVATE KEY DOES NOT LIVE HERE. It belongs in your secrets
|
|
14
|
-
* manager (1Password / AWS Secrets Manager / Vault), accessible only
|
|
15
|
-
* to the license-generator service that produces signed JWTs. The
|
|
16
|
-
* mathematics of ECDSA P-256 guarantee that a holder of the public
|
|
17
|
-
* key alone cannot forge a signature.
|
|
18
|
-
*
|
|
19
|
-
* To populate this registry:
|
|
20
|
-
* 1. Run `node scripts/license/generate-keypair.mjs` (see that
|
|
21
|
-
* script's comment for full instructions)
|
|
22
|
-
* 2. Paste the printed PUBLIC key JWK as an entry below — and into
|
|
23
|
-
* the matching `publicKeys.ts` for the JS-side validator
|
|
24
|
-
* 3. Move the printed PRIVATE key into your secrets store
|
|
25
|
-
* 4. Wire your license-generator backend to use the private key
|
|
26
|
-
*/
|
|
27
|
-
import Foundation
|
|
28
|
-
|
|
29
|
-
/// ES256 (ECDSA P-256) public key in JWK form. The shape matches the
|
|
30
|
-
/// IANA JWK spec — x/y are base64url-encoded 32-byte big-endian
|
|
31
|
-
/// coordinates. Same fields as the JS-side `DvaiPublicKeyJwk`.
|
|
32
|
-
public struct DvaiPublicKeyJwk: Sendable, Equatable {
|
|
33
|
-
/// Always `"EC"` for ECDSA keys.
|
|
34
|
-
public let kty: String
|
|
35
|
-
/// Always `"P-256"` for ES256.
|
|
36
|
-
public let crv: String
|
|
37
|
-
/// Base64url-encoded X coordinate (32 bytes).
|
|
38
|
-
public let x: String
|
|
39
|
-
/// Base64url-encoded Y coordinate (32 bytes).
|
|
40
|
-
public let y: String
|
|
41
|
-
/// Algorithm hint. Should be `"ES256"` for our keys.
|
|
42
|
-
public let alg: String?
|
|
43
|
-
/// Key use hint. Should be `"sig"`.
|
|
44
|
-
public let use: String?
|
|
45
|
-
/// Key identifier — must match the JWT header `kid` to be selected.
|
|
46
|
-
public let kid: String?
|
|
47
|
-
|
|
48
|
-
public init(
|
|
49
|
-
kty: String = "EC",
|
|
50
|
-
crv: String = "P-256",
|
|
51
|
-
x: String,
|
|
52
|
-
y: String,
|
|
53
|
-
alg: String? = "ES256",
|
|
54
|
-
use: String? = "sig",
|
|
55
|
-
kid: String? = nil
|
|
56
|
-
) {
|
|
57
|
-
self.kty = kty
|
|
58
|
-
self.crv = crv
|
|
59
|
-
self.x = x
|
|
60
|
-
self.y = y
|
|
61
|
-
self.alg = alg
|
|
62
|
-
self.use = use
|
|
63
|
-
self.kid = kid
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// `kid` reserved for the placeholder key below. The validator refuses
|
|
68
|
-
/// to accept tokens signed with this kid unless the caller explicitly
|
|
69
|
-
/// opts in (`allowPlaceholderKey: true` passed to the validator
|
|
70
|
-
/// constructor). Used by tests and by the sample license printed by
|
|
71
|
-
/// `generate-keypair.mjs`.
|
|
72
|
-
public let DVAI_PLACEHOLDER_KID = "placeholder-do-not-ship"
|
|
73
|
-
|
|
74
|
-
/// Registry mapping `kid` → public key JWK.
|
|
75
|
-
///
|
|
76
|
-
/// WARNING: The entry below is a **placeholder** — it is a published,
|
|
77
|
-
/// well-known test keypair and DOES NOT verify any real production
|
|
78
|
-
/// license. Before shipping licenses to customers, replace it with the
|
|
79
|
-
/// output of `scripts/license/generate-keypair.mjs`. The SDK refuses
|
|
80
|
-
/// to validate licenses against the placeholder kid
|
|
81
|
-
/// `"placeholder-do-not-ship"` unless `allowPlaceholderKey: true` is
|
|
82
|
-
/// passed to the validator (test-only escape hatch).
|
|
83
|
-
///
|
|
84
|
-
/// Adding a new key for rotation:
|
|
85
|
-
///
|
|
86
|
-
/// public let DVAI_PUBLIC_KEYS: [String: DvaiPublicKeyJwk] = [
|
|
87
|
-
/// "2026-05": DvaiPublicKeyJwk(x: "...", y: "...", kid: "2026-05"),
|
|
88
|
-
/// "2027-01": DvaiPublicKeyJwk(x: "...", y: "...", kid: "2027-01"),
|
|
89
|
-
/// ]
|
|
90
|
-
public let DVAI_PUBLIC_KEYS: [String: DvaiPublicKeyJwk] = [
|
|
91
|
-
// Production key, kid `2026-05`. Generated 2026-05-15 by
|
|
92
|
-
// scripts/license/generate-keypair.mjs. The matching private key
|
|
93
|
-
// lives in the operator's secrets manager.
|
|
94
|
-
"2026-05": DvaiPublicKeyJwk(
|
|
95
|
-
x: "2Y8TuhnlE4tiVDtliozYTgc1TAqi4_TBTI6FHe1p_Vw",
|
|
96
|
-
y: "pyxMJHj10HPe2hnpJvMpnZ4AzpYZRfqGEMhpBr1-Oto",
|
|
97
|
-
alg: "ES256",
|
|
98
|
-
use: "sig",
|
|
99
|
-
kid: "2026-05"
|
|
100
|
-
),
|
|
101
|
-
// PLACEHOLDER — used by the SDK's own unit tests and by the sample
|
|
102
|
-
// license printed by `generate-keypair.mjs`. The validator REFUSES
|
|
103
|
-
// to accept tokens signed under this kid unless `allowPlaceholderKey:
|
|
104
|
-
// true` is passed to the validator constructor (test-only escape
|
|
105
|
-
// hatch). Safe to keep in production builds; remove only if you
|
|
106
|
-
// want test fixtures to stop working.
|
|
107
|
-
DVAI_PLACEHOLDER_KID: DvaiPublicKeyJwk(
|
|
108
|
-
x: "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
|
|
109
|
-
y: "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
|
|
110
|
-
alg: "ES256",
|
|
111
|
-
use: "sig",
|
|
112
|
-
kid: DVAI_PLACEHOLDER_KID
|
|
113
|
-
),
|
|
114
|
-
]
|
|
1
|
+
/*
|
|
2
|
+
* Public-key registry for DVAI-Bridge license JWT verification on iOS.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `packages/dvai-bridge-core/src/license/publicKeys.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Each entry is keyed by `kid` (key id, written by the license generator
|
|
7
|
+
* into the JWT header). The SDK looks up the matching entry by kid when
|
|
8
|
+
* verifying a license token. Multiple entries can coexist so that key
|
|
9
|
+
* rotation is non-disruptive: ship the new key in a release alongside
|
|
10
|
+
* the old, leave the old in place for ~12 months while previously-
|
|
11
|
+
* issued licenses naturally expire or get re-issued, then prune.
|
|
12
|
+
*
|
|
13
|
+
* THE PRIVATE KEY DOES NOT LIVE HERE. It belongs in your secrets
|
|
14
|
+
* manager (1Password / AWS Secrets Manager / Vault), accessible only
|
|
15
|
+
* to the license-generator service that produces signed JWTs. The
|
|
16
|
+
* mathematics of ECDSA P-256 guarantee that a holder of the public
|
|
17
|
+
* key alone cannot forge a signature.
|
|
18
|
+
*
|
|
19
|
+
* To populate this registry:
|
|
20
|
+
* 1. Run `node scripts/license/generate-keypair.mjs` (see that
|
|
21
|
+
* script's comment for full instructions)
|
|
22
|
+
* 2. Paste the printed PUBLIC key JWK as an entry below — and into
|
|
23
|
+
* the matching `publicKeys.ts` for the JS-side validator
|
|
24
|
+
* 3. Move the printed PRIVATE key into your secrets store
|
|
25
|
+
* 4. Wire your license-generator backend to use the private key
|
|
26
|
+
*/
|
|
27
|
+
import Foundation
|
|
28
|
+
|
|
29
|
+
/// ES256 (ECDSA P-256) public key in JWK form. The shape matches the
|
|
30
|
+
/// IANA JWK spec — x/y are base64url-encoded 32-byte big-endian
|
|
31
|
+
/// coordinates. Same fields as the JS-side `DvaiPublicKeyJwk`.
|
|
32
|
+
public struct DvaiPublicKeyJwk: Sendable, Equatable {
|
|
33
|
+
/// Always `"EC"` for ECDSA keys.
|
|
34
|
+
public let kty: String
|
|
35
|
+
/// Always `"P-256"` for ES256.
|
|
36
|
+
public let crv: String
|
|
37
|
+
/// Base64url-encoded X coordinate (32 bytes).
|
|
38
|
+
public let x: String
|
|
39
|
+
/// Base64url-encoded Y coordinate (32 bytes).
|
|
40
|
+
public let y: String
|
|
41
|
+
/// Algorithm hint. Should be `"ES256"` for our keys.
|
|
42
|
+
public let alg: String?
|
|
43
|
+
/// Key use hint. Should be `"sig"`.
|
|
44
|
+
public let use: String?
|
|
45
|
+
/// Key identifier — must match the JWT header `kid` to be selected.
|
|
46
|
+
public let kid: String?
|
|
47
|
+
|
|
48
|
+
public init(
|
|
49
|
+
kty: String = "EC",
|
|
50
|
+
crv: String = "P-256",
|
|
51
|
+
x: String,
|
|
52
|
+
y: String,
|
|
53
|
+
alg: String? = "ES256",
|
|
54
|
+
use: String? = "sig",
|
|
55
|
+
kid: String? = nil
|
|
56
|
+
) {
|
|
57
|
+
self.kty = kty
|
|
58
|
+
self.crv = crv
|
|
59
|
+
self.x = x
|
|
60
|
+
self.y = y
|
|
61
|
+
self.alg = alg
|
|
62
|
+
self.use = use
|
|
63
|
+
self.kid = kid
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// `kid` reserved for the placeholder key below. The validator refuses
|
|
68
|
+
/// to accept tokens signed with this kid unless the caller explicitly
|
|
69
|
+
/// opts in (`allowPlaceholderKey: true` passed to the validator
|
|
70
|
+
/// constructor). Used by tests and by the sample license printed by
|
|
71
|
+
/// `generate-keypair.mjs`.
|
|
72
|
+
public let DVAI_PLACEHOLDER_KID = "placeholder-do-not-ship"
|
|
73
|
+
|
|
74
|
+
/// Registry mapping `kid` → public key JWK.
|
|
75
|
+
///
|
|
76
|
+
/// WARNING: The entry below is a **placeholder** — it is a published,
|
|
77
|
+
/// well-known test keypair and DOES NOT verify any real production
|
|
78
|
+
/// license. Before shipping licenses to customers, replace it with the
|
|
79
|
+
/// output of `scripts/license/generate-keypair.mjs`. The SDK refuses
|
|
80
|
+
/// to validate licenses against the placeholder kid
|
|
81
|
+
/// `"placeholder-do-not-ship"` unless `allowPlaceholderKey: true` is
|
|
82
|
+
/// passed to the validator (test-only escape hatch).
|
|
83
|
+
///
|
|
84
|
+
/// Adding a new key for rotation:
|
|
85
|
+
///
|
|
86
|
+
/// public let DVAI_PUBLIC_KEYS: [String: DvaiPublicKeyJwk] = [
|
|
87
|
+
/// "2026-05": DvaiPublicKeyJwk(x: "...", y: "...", kid: "2026-05"),
|
|
88
|
+
/// "2027-01": DvaiPublicKeyJwk(x: "...", y: "...", kid: "2027-01"),
|
|
89
|
+
/// ]
|
|
90
|
+
public let DVAI_PUBLIC_KEYS: [String: DvaiPublicKeyJwk] = [
|
|
91
|
+
// Production key, kid `2026-05`. Generated 2026-05-15 by
|
|
92
|
+
// scripts/license/generate-keypair.mjs. The matching private key
|
|
93
|
+
// lives in the operator's secrets manager.
|
|
94
|
+
"2026-05": DvaiPublicKeyJwk(
|
|
95
|
+
x: "2Y8TuhnlE4tiVDtliozYTgc1TAqi4_TBTI6FHe1p_Vw",
|
|
96
|
+
y: "pyxMJHj10HPe2hnpJvMpnZ4AzpYZRfqGEMhpBr1-Oto",
|
|
97
|
+
alg: "ES256",
|
|
98
|
+
use: "sig",
|
|
99
|
+
kid: "2026-05"
|
|
100
|
+
),
|
|
101
|
+
// PLACEHOLDER — used by the SDK's own unit tests and by the sample
|
|
102
|
+
// license printed by `generate-keypair.mjs`. The validator REFUSES
|
|
103
|
+
// to accept tokens signed under this kid unless `allowPlaceholderKey:
|
|
104
|
+
// true` is passed to the validator constructor (test-only escape
|
|
105
|
+
// hatch). Safe to keep in production builds; remove only if you
|
|
106
|
+
// want test fixtures to stop working.
|
|
107
|
+
DVAI_PLACEHOLDER_KID: DvaiPublicKeyJwk(
|
|
108
|
+
x: "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
|
|
109
|
+
y: "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
|
|
110
|
+
alg: "ES256",
|
|
111
|
+
use: "sig",
|
|
112
|
+
kid: DVAI_PLACEHOLDER_KID
|
|
113
|
+
),
|
|
114
|
+
]
|
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Type surface for the DVAI-Bridge offline JWT license system on iOS.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `packages/dvai-bridge-core/src/license/types.ts`. The whole
|
|
5
|
-
* license flow is deliberately small:
|
|
6
|
-
* 1. A signed JWT (produced server-side by your license generator) is
|
|
7
|
-
* either dropped at a platform-default path (e.g. bundled as a
|
|
8
|
-
* `dvai-license.jwt` resource), pointed at via
|
|
9
|
-
* `LicenseValidatorOptions.path`, or pasted directly into
|
|
10
|
-
* `LicenseValidatorOptions.token`.
|
|
11
|
-
* 2. The SDK reads it, verifies the ECDSA P-256 signature against the
|
|
12
|
-
* key registry in `PublicKeys.swift`, and checks four runtime
|
|
13
|
-
* claims:
|
|
14
|
-
* - signature must verify against a known kid
|
|
15
|
-
* - `exp` must be in the future
|
|
16
|
-
* - `aud` must include the current audience (Bundle.main.bundleIdentifier)
|
|
17
|
-
* - `platforms` must include the current SDK platform (.ios)
|
|
18
|
-
* 3. The outcome is summarised in a `LicenseStatus` value the rest of
|
|
19
|
-
* the SDK can dispatch on.
|
|
20
|
-
*
|
|
21
|
-
* Nothing in this file makes network calls. The entire flow is offline.
|
|
22
|
-
*
|
|
23
|
-
* The wire format (JWT header + payload + ES256 signature) is byte-for-
|
|
24
|
-
* byte equivalent to the JS-side validator — the same .jwt file works
|
|
25
|
-
* across iOS, Android, .NET, Flutter, RN, and JS SDKs.
|
|
26
|
-
*/
|
|
27
|
-
import Foundation
|
|
28
|
-
#if !COCOAPODS
|
|
29
|
-
import JWTKit
|
|
30
|
-
#endif
|
|
31
|
-
|
|
32
|
-
/// Recognised license tiers. `commercial` and `trial` come from the
|
|
33
|
-
/// signed token's `tier` claim; the `free*` variants are produced
|
|
34
|
-
/// internally by the validator. Anything unknown collapses to
|
|
35
|
-
/// `.freeProd` defensively.
|
|
36
|
-
public enum LicenseTier: String, Sendable, Codable, Equatable {
|
|
37
|
-
case commercial
|
|
38
|
-
case trial
|
|
39
|
-
/// Running in DEBUG / simulator / DVAI_FORCE_DEV — no badge required.
|
|
40
|
-
case freeDev = "free-dev"
|
|
41
|
-
/// Production build with no valid license — badge required.
|
|
42
|
-
case freeProd = "free-prod"
|
|
43
|
-
/// Had a valid license but `exp` is past — badge required + warn.
|
|
44
|
-
case freeExpired = "free-expired"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/// Platform identifiers the SDK recognises in license `platforms` claims.
|
|
48
|
-
/// Matches the JS-side enum exactly so a single .jwt activates across
|
|
49
|
-
/// every SDK that lists its identifier.
|
|
50
|
-
public enum DvaiPlatform: String, Sendable, Codable, Equatable {
|
|
51
|
-
case web
|
|
52
|
-
case node
|
|
53
|
-
case ios
|
|
54
|
-
case android
|
|
55
|
-
case dotnet
|
|
56
|
-
case flutter
|
|
57
|
-
case reactNative = "react-native"
|
|
58
|
-
case capacitor
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/// Result of license validation. Use the `kind` case to dispatch — the
|
|
62
|
-
/// associated values vary per case (matching the JS-side discriminated
|
|
63
|
-
/// union exactly).
|
|
64
|
-
public enum LicenseStatus: Sendable, Equatable {
|
|
65
|
-
/// Active commercial license. Audience + platform binding verified.
|
|
66
|
-
case commercial(licensee: String, expiresAt: Int64, platform: DvaiPlatform, audienceMatched: String)
|
|
67
|
-
|
|
68
|
-
/// Active trial license. Same shape as commercial — the SDK treats
|
|
69
|
-
/// both as "premium" (no attribution badge).
|
|
70
|
-
case trial(licensee: String, expiresAt: Int64, platform: DvaiPlatform, audienceMatched: String)
|
|
71
|
-
|
|
72
|
-
/// Running in a developer environment — license enforcement bypassed.
|
|
73
|
-
/// `reason` describes which heuristic matched (for logs/dashboard).
|
|
74
|
-
case freeDev(reason: String)
|
|
75
|
-
|
|
76
|
-
/// Production build with no valid license. `reason` is the human-
|
|
77
|
-
/// readable explanation: missing file, signature didn't verify,
|
|
78
|
-
/// audience mismatch, etc. The SDK throws on this status in
|
|
79
|
-
/// `validateAndAssert()`.
|
|
80
|
-
case freeProd(reason: String)
|
|
81
|
-
|
|
82
|
-
/// Had a valid license but `exp` is past. Surfaced separately so the
|
|
83
|
-
/// developer/dashboard knows whose renewal to chase. Throws in
|
|
84
|
-
/// `validateAndAssert()`.
|
|
85
|
-
case freeExpired(licensee: String, expiredAt: Int64)
|
|
86
|
-
|
|
87
|
-
/// True when the status represents a paid / unwatermarked tier.
|
|
88
|
-
public var isPaid: Bool {
|
|
89
|
-
switch self {
|
|
90
|
-
case .commercial, .trial: return true
|
|
91
|
-
case .freeDev, .freeProd, .freeExpired: return false
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/// Stable string identifier per case — useful for logging and for
|
|
96
|
-
/// matching the JS-side `status.kind` field 1:1.
|
|
97
|
-
public var kind: String {
|
|
98
|
-
switch self {
|
|
99
|
-
case .commercial: return "commercial"
|
|
100
|
-
case .trial: return "trial"
|
|
101
|
-
case .freeDev: return "free-dev"
|
|
102
|
-
case .freeProd: return "free-prod"
|
|
103
|
-
case .freeExpired: return "free-expired"
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/// JWT payload shape we issue. Conforms to `JWTPayload` so JWTKit can
|
|
109
|
-
/// decode it; the `verify(using:)` method is a no-op because we run our
|
|
110
|
-
/// own claim verification at the `LicenseValidator` level (so we can
|
|
111
|
-
/// produce specific free-prod reasons rather than generic JWT errors).
|
|
112
|
-
///
|
|
113
|
-
/// The wire field names use `aud` / `iss` etc. directly so the JSON is
|
|
114
|
-
/// identical to the JS-side payload.
|
|
115
|
-
public struct DvaiLicensePayload: Codable, Equatable {
|
|
116
|
-
/// Standard JWT issuer. Must equal `"DVAI-Bridge"`.
|
|
117
|
-
public let iss: String
|
|
118
|
-
/// Standard subject — internal license id.
|
|
119
|
-
public let sub: String
|
|
120
|
-
/// Audience binding: exact strings OR `*.example.com` wildcard subdomain
|
|
121
|
-
/// patterns OR `*` (any-domain). Matched against
|
|
122
|
-
/// `Bundle.main.bundleIdentifier` at runtime.
|
|
123
|
-
public let aud: [String]
|
|
124
|
-
/// Tier the license grants. Only `commercial` / `trial` are valid
|
|
125
|
-
/// here — `free*` is computed by the validator, never claimed.
|
|
126
|
-
public let tier: String
|
|
127
|
-
/// Which SDK platforms this license activates. Current runtime must
|
|
128
|
-
/// be in this list for the license to apply.
|
|
129
|
-
public let platforms: [String]
|
|
130
|
-
/// Display name of the licensee, for audit logs and dashboards.
|
|
131
|
-
public let licensee: String
|
|
132
|
-
/// Standard JWT issued-at (seconds since Unix epoch).
|
|
133
|
-
public let iat: Int64
|
|
134
|
-
/// Standard JWT expiry (seconds since Unix epoch).
|
|
135
|
-
public let exp: Int64
|
|
136
|
-
|
|
137
|
-
public init(
|
|
138
|
-
iss: String,
|
|
139
|
-
sub: String,
|
|
140
|
-
aud: [String],
|
|
141
|
-
tier: String,
|
|
142
|
-
platforms: [String],
|
|
143
|
-
licensee: String,
|
|
144
|
-
iat: Int64,
|
|
145
|
-
exp: Int64
|
|
146
|
-
) {
|
|
147
|
-
self.iss = iss
|
|
148
|
-
self.sub = sub
|
|
149
|
-
self.aud = aud
|
|
150
|
-
self.tier = tier
|
|
151
|
-
self.platforms = platforms
|
|
152
|
-
self.licensee = licensee
|
|
153
|
-
self.iat = iat
|
|
154
|
-
self.exp = exp
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
#if !COCOAPODS
|
|
159
|
-
extension DvaiLicensePayload: JWTPayload {
|
|
160
|
-
/// JWTKit hook — we intentionally do NOT verify exp/aud here, because
|
|
161
|
-
/// the validator wants specific failure reasons per claim. JWTKit's
|
|
162
|
-
/// `verify(_:as:)` will still verify the signature; the claim
|
|
163
|
-
/// verification happens in `LicenseValidator.verifyToken`.
|
|
164
|
-
public func verify(using algorithm: some JWTAlgorithm) async throws {
|
|
165
|
-
// No-op: see comment above.
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
#endif
|
|
169
|
-
|
|
170
|
-
/// Thrown by `LicenseValidator.validateAndAssert()` (and propagated from
|
|
171
|
-
/// `DVAIBridge.start(...)`) when an SDK consumer attempts to run the
|
|
172
|
-
/// library in a production / release context without a valid commercial
|
|
173
|
-
/// or trial license.
|
|
174
|
-
///
|
|
175
|
-
/// The error message is intentionally verbose: it tells the developer
|
|
176
|
-
/// exactly which check failed, how to resolve it, where to put the
|
|
177
|
-
/// license file, and how to bypass for local development. This is the
|
|
178
|
-
/// front line of the BSL 1.1 commercial enforcement story — surface it
|
|
179
|
-
/// clearly enough that a developer can unblock themselves without a
|
|
180
|
-
/// support ticket.
|
|
181
|
-
///
|
|
182
|
-
/// The `status` field carries the underlying `LicenseStatus` so
|
|
183
|
-
/// programmatic callers can dispatch on `err.status.kind` if they
|
|
184
|
-
/// want to handle expired vs. missing differently.
|
|
185
|
-
public struct LicenseRequiredError: Error, LocalizedError, Sendable {
|
|
186
|
-
public let message: String
|
|
187
|
-
public let status: LicenseStatus
|
|
188
|
-
|
|
189
|
-
public init(message: String, status: LicenseStatus) {
|
|
190
|
-
self.message = message
|
|
191
|
-
self.status = status
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
public var errorDescription: String? { message }
|
|
195
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* Type surface for the DVAI-Bridge offline JWT license system on iOS.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `packages/dvai-bridge-core/src/license/types.ts`. The whole
|
|
5
|
+
* license flow is deliberately small:
|
|
6
|
+
* 1. A signed JWT (produced server-side by your license generator) is
|
|
7
|
+
* either dropped at a platform-default path (e.g. bundled as a
|
|
8
|
+
* `dvai-license.jwt` resource), pointed at via
|
|
9
|
+
* `LicenseValidatorOptions.path`, or pasted directly into
|
|
10
|
+
* `LicenseValidatorOptions.token`.
|
|
11
|
+
* 2. The SDK reads it, verifies the ECDSA P-256 signature against the
|
|
12
|
+
* key registry in `PublicKeys.swift`, and checks four runtime
|
|
13
|
+
* claims:
|
|
14
|
+
* - signature must verify against a known kid
|
|
15
|
+
* - `exp` must be in the future
|
|
16
|
+
* - `aud` must include the current audience (Bundle.main.bundleIdentifier)
|
|
17
|
+
* - `platforms` must include the current SDK platform (.ios)
|
|
18
|
+
* 3. The outcome is summarised in a `LicenseStatus` value the rest of
|
|
19
|
+
* the SDK can dispatch on.
|
|
20
|
+
*
|
|
21
|
+
* Nothing in this file makes network calls. The entire flow is offline.
|
|
22
|
+
*
|
|
23
|
+
* The wire format (JWT header + payload + ES256 signature) is byte-for-
|
|
24
|
+
* byte equivalent to the JS-side validator — the same .jwt file works
|
|
25
|
+
* across iOS, Android, .NET, Flutter, RN, and JS SDKs.
|
|
26
|
+
*/
|
|
27
|
+
import Foundation
|
|
28
|
+
#if !COCOAPODS
|
|
29
|
+
import JWTKit
|
|
30
|
+
#endif
|
|
31
|
+
|
|
32
|
+
/// Recognised license tiers. `commercial` and `trial` come from the
|
|
33
|
+
/// signed token's `tier` claim; the `free*` variants are produced
|
|
34
|
+
/// internally by the validator. Anything unknown collapses to
|
|
35
|
+
/// `.freeProd` defensively.
|
|
36
|
+
public enum LicenseTier: String, Sendable, Codable, Equatable {
|
|
37
|
+
case commercial
|
|
38
|
+
case trial
|
|
39
|
+
/// Running in DEBUG / simulator / DVAI_FORCE_DEV — no badge required.
|
|
40
|
+
case freeDev = "free-dev"
|
|
41
|
+
/// Production build with no valid license — badge required.
|
|
42
|
+
case freeProd = "free-prod"
|
|
43
|
+
/// Had a valid license but `exp` is past — badge required + warn.
|
|
44
|
+
case freeExpired = "free-expired"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Platform identifiers the SDK recognises in license `platforms` claims.
|
|
48
|
+
/// Matches the JS-side enum exactly so a single .jwt activates across
|
|
49
|
+
/// every SDK that lists its identifier.
|
|
50
|
+
public enum DvaiPlatform: String, Sendable, Codable, Equatable {
|
|
51
|
+
case web
|
|
52
|
+
case node
|
|
53
|
+
case ios
|
|
54
|
+
case android
|
|
55
|
+
case dotnet
|
|
56
|
+
case flutter
|
|
57
|
+
case reactNative = "react-native"
|
|
58
|
+
case capacitor
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Result of license validation. Use the `kind` case to dispatch — the
|
|
62
|
+
/// associated values vary per case (matching the JS-side discriminated
|
|
63
|
+
/// union exactly).
|
|
64
|
+
public enum LicenseStatus: Sendable, Equatable {
|
|
65
|
+
/// Active commercial license. Audience + platform binding verified.
|
|
66
|
+
case commercial(licensee: String, expiresAt: Int64, platform: DvaiPlatform, audienceMatched: String)
|
|
67
|
+
|
|
68
|
+
/// Active trial license. Same shape as commercial — the SDK treats
|
|
69
|
+
/// both as "premium" (no attribution badge).
|
|
70
|
+
case trial(licensee: String, expiresAt: Int64, platform: DvaiPlatform, audienceMatched: String)
|
|
71
|
+
|
|
72
|
+
/// Running in a developer environment — license enforcement bypassed.
|
|
73
|
+
/// `reason` describes which heuristic matched (for logs/dashboard).
|
|
74
|
+
case freeDev(reason: String)
|
|
75
|
+
|
|
76
|
+
/// Production build with no valid license. `reason` is the human-
|
|
77
|
+
/// readable explanation: missing file, signature didn't verify,
|
|
78
|
+
/// audience mismatch, etc. The SDK throws on this status in
|
|
79
|
+
/// `validateAndAssert()`.
|
|
80
|
+
case freeProd(reason: String)
|
|
81
|
+
|
|
82
|
+
/// Had a valid license but `exp` is past. Surfaced separately so the
|
|
83
|
+
/// developer/dashboard knows whose renewal to chase. Throws in
|
|
84
|
+
/// `validateAndAssert()`.
|
|
85
|
+
case freeExpired(licensee: String, expiredAt: Int64)
|
|
86
|
+
|
|
87
|
+
/// True when the status represents a paid / unwatermarked tier.
|
|
88
|
+
public var isPaid: Bool {
|
|
89
|
+
switch self {
|
|
90
|
+
case .commercial, .trial: return true
|
|
91
|
+
case .freeDev, .freeProd, .freeExpired: return false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Stable string identifier per case — useful for logging and for
|
|
96
|
+
/// matching the JS-side `status.kind` field 1:1.
|
|
97
|
+
public var kind: String {
|
|
98
|
+
switch self {
|
|
99
|
+
case .commercial: return "commercial"
|
|
100
|
+
case .trial: return "trial"
|
|
101
|
+
case .freeDev: return "free-dev"
|
|
102
|
+
case .freeProd: return "free-prod"
|
|
103
|
+
case .freeExpired: return "free-expired"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// JWT payload shape we issue. Conforms to `JWTPayload` so JWTKit can
|
|
109
|
+
/// decode it; the `verify(using:)` method is a no-op because we run our
|
|
110
|
+
/// own claim verification at the `LicenseValidator` level (so we can
|
|
111
|
+
/// produce specific free-prod reasons rather than generic JWT errors).
|
|
112
|
+
///
|
|
113
|
+
/// The wire field names use `aud` / `iss` etc. directly so the JSON is
|
|
114
|
+
/// identical to the JS-side payload.
|
|
115
|
+
public struct DvaiLicensePayload: Codable, Equatable {
|
|
116
|
+
/// Standard JWT issuer. Must equal `"DVAI-Bridge"`.
|
|
117
|
+
public let iss: String
|
|
118
|
+
/// Standard subject — internal license id.
|
|
119
|
+
public let sub: String
|
|
120
|
+
/// Audience binding: exact strings OR `*.example.com` wildcard subdomain
|
|
121
|
+
/// patterns OR `*` (any-domain). Matched against
|
|
122
|
+
/// `Bundle.main.bundleIdentifier` at runtime.
|
|
123
|
+
public let aud: [String]
|
|
124
|
+
/// Tier the license grants. Only `commercial` / `trial` are valid
|
|
125
|
+
/// here — `free*` is computed by the validator, never claimed.
|
|
126
|
+
public let tier: String
|
|
127
|
+
/// Which SDK platforms this license activates. Current runtime must
|
|
128
|
+
/// be in this list for the license to apply.
|
|
129
|
+
public let platforms: [String]
|
|
130
|
+
/// Display name of the licensee, for audit logs and dashboards.
|
|
131
|
+
public let licensee: String
|
|
132
|
+
/// Standard JWT issued-at (seconds since Unix epoch).
|
|
133
|
+
public let iat: Int64
|
|
134
|
+
/// Standard JWT expiry (seconds since Unix epoch).
|
|
135
|
+
public let exp: Int64
|
|
136
|
+
|
|
137
|
+
public init(
|
|
138
|
+
iss: String,
|
|
139
|
+
sub: String,
|
|
140
|
+
aud: [String],
|
|
141
|
+
tier: String,
|
|
142
|
+
platforms: [String],
|
|
143
|
+
licensee: String,
|
|
144
|
+
iat: Int64,
|
|
145
|
+
exp: Int64
|
|
146
|
+
) {
|
|
147
|
+
self.iss = iss
|
|
148
|
+
self.sub = sub
|
|
149
|
+
self.aud = aud
|
|
150
|
+
self.tier = tier
|
|
151
|
+
self.platforms = platforms
|
|
152
|
+
self.licensee = licensee
|
|
153
|
+
self.iat = iat
|
|
154
|
+
self.exp = exp
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#if !COCOAPODS
|
|
159
|
+
extension DvaiLicensePayload: JWTPayload {
|
|
160
|
+
/// JWTKit hook — we intentionally do NOT verify exp/aud here, because
|
|
161
|
+
/// the validator wants specific failure reasons per claim. JWTKit's
|
|
162
|
+
/// `verify(_:as:)` will still verify the signature; the claim
|
|
163
|
+
/// verification happens in `LicenseValidator.verifyToken`.
|
|
164
|
+
public func verify(using algorithm: some JWTAlgorithm) async throws {
|
|
165
|
+
// No-op: see comment above.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
#endif
|
|
169
|
+
|
|
170
|
+
/// Thrown by `LicenseValidator.validateAndAssert()` (and propagated from
|
|
171
|
+
/// `DVAIBridge.start(...)`) when an SDK consumer attempts to run the
|
|
172
|
+
/// library in a production / release context without a valid commercial
|
|
173
|
+
/// or trial license.
|
|
174
|
+
///
|
|
175
|
+
/// The error message is intentionally verbose: it tells the developer
|
|
176
|
+
/// exactly which check failed, how to resolve it, where to put the
|
|
177
|
+
/// license file, and how to bypass for local development. This is the
|
|
178
|
+
/// front line of the BSL 1.1 commercial enforcement story — surface it
|
|
179
|
+
/// clearly enough that a developer can unblock themselves without a
|
|
180
|
+
/// support ticket.
|
|
181
|
+
///
|
|
182
|
+
/// The `status` field carries the underlying `LicenseStatus` so
|
|
183
|
+
/// programmatic callers can dispatch on `err.status.kind` if they
|
|
184
|
+
/// want to handle expired vs. missing differently.
|
|
185
|
+
public struct LicenseRequiredError: Error, LocalizedError, Sendable {
|
|
186
|
+
public let message: String
|
|
187
|
+
public let status: LicenseStatus
|
|
188
|
+
|
|
189
|
+
public init(message: String, status: LicenseStatus) {
|
|
190
|
+
self.message = message
|
|
191
|
+
self.status = status
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public var errorDescription: String? { message }
|
|
195
|
+
}
|