@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.
Files changed (41) hide show
  1. package/Package.swift +104 -104
  2. package/ios/Sources/DVAIBridge/BackendKind.swift +23 -23
  3. package/ios/Sources/DVAIBridge/BoundServer.swift +46 -46
  4. package/ios/Sources/DVAIBridge/DVAIBridge.swift +658 -658
  5. package/ios/Sources/DVAIBridge/DVAIBridgeConfig.swift +86 -86
  6. package/ios/Sources/DVAIBridge/DVAIBridgeError.swift +33 -33
  7. package/ios/Sources/DVAIBridge/Internal/BackendSelector.swift +59 -59
  8. package/ios/Sources/DVAIBridge/Internal/ProgressBroadcaster.swift +84 -84
  9. package/ios/Sources/DVAIBridge/License/Audience.swift +133 -133
  10. package/ios/Sources/DVAIBridge/License/Discovery.swift +164 -164
  11. package/ios/Sources/DVAIBridge/License/LicenseValidator.swift +392 -392
  12. package/ios/Sources/DVAIBridge/License/PublicKeys.swift +114 -114
  13. package/ios/Sources/DVAIBridge/License/Types.swift +195 -195
  14. package/ios/Sources/DVAIBridge/Offload/OffloadConfig.swift +118 -118
  15. package/ios/Sources/DVAIBridge/ProgressEvent.swift +34 -34
  16. package/ios/Sources/DVAICoreMLCore/CoreMLBackendError.swift +19 -19
  17. package/ios/Sources/DVAICoreMLCore/CoreMLHandlers.swift +123 -123
  18. package/ios/Sources/DVAICoreMLCore/CoreMLPluginState.swift +130 -130
  19. package/ios/Sources/DVAICoreMLCore/Internal/CoreMLEngine.swift +137 -137
  20. package/ios/Sources/DVAICoreMLCore/Internal/CoreMLGenerator.swift +108 -108
  21. package/ios/Sources/DVAICoreMLCore/Internal/CoreMLSampler.swift +96 -96
  22. package/ios/Sources/DVAICoreMLCore/Internal/CoreMLTokenizer.swift +69 -69
  23. package/ios/Tests/DVAIBridgeTests/BackendSelectorTests.swift +53 -53
  24. package/ios/Tests/DVAIBridgeTests/CoreMLEngineTests.swift +18 -18
  25. package/ios/Tests/DVAIBridgeTests/CoreMLGeneratorShapeTests.swift +11 -11
  26. package/ios/Tests/DVAIBridgeTests/CoreMLHandlersTests.swift +32 -32
  27. package/ios/Tests/DVAIBridgeTests/CoreMLPluginStateTests.swift +41 -41
  28. package/ios/Tests/DVAIBridgeTests/CoreMLSamplerTests.swift +40 -40
  29. package/ios/Tests/DVAIBridgeTests/CoreMLTokenizerTests.swift +19 -19
  30. package/ios/Tests/DVAIBridgeTests/DVAIBridgeAPIShapeTests.swift +37 -37
  31. package/ios/Tests/DVAIBridgeTests/DVAIBridgeConfigTests.swift +52 -52
  32. package/ios/Tests/DVAIBridgeTests/DVAIBridgeErrorTests.swift +33 -33
  33. package/ios/Tests/DVAIBridgeTests/LicenseValidatorTests.swift +658 -658
  34. package/ios/Tests/DVAIBridgeTests/ProgressBroadcasterTests.swift +69 -69
  35. package/ios/Tests/DVAIBridgeTests/ProgressEventTests.swift +25 -25
  36. package/ios/Tests/DVAIBridgeTests/ReactiveStateTests.swift +45 -45
  37. package/ios/Tests/DVAIBridgeTests/RealModelIntegrationTest.swift +385 -359
  38. package/package.json +3 -4
  39. package/DVAIBridge.podspec +0 -120
  40. package/LICENSE +0 -51
  41. 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
+ }