@tinkerlandai/longplay-react-native 0.10.11 → 0.10.13

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.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinkerlandai/longplay-react-native",
3
- "version": "0.10.11",
3
+ "version": "0.10.13",
4
4
  "description": "Longplay React Native Customer SDK with growth, analytics, commerce, Funnel, content and native store integrations.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -1,81 +0,0 @@
1
- package ai.tinkerland.indiana
2
-
3
- import android.content.Context
4
- import android.security.keystore.KeyGenParameterSpec
5
- import android.security.keystore.KeyProperties
6
- import android.util.AtomicFile
7
- import android.util.Base64
8
- import org.json.JSONObject
9
- import java.io.File
10
- import java.security.KeyStore
11
- import java.security.MessageDigest
12
- import java.util.UUID
13
- import javax.crypto.Cipher
14
- import javax.crypto.KeyGenerator
15
- import javax.crypto.SecretKey
16
- import javax.crypto.spec.GCMParameterSpec
17
-
18
- internal data class IndianaNativePendingOrder(val runId: UUID, val orderId: UUID, val receiptId: UUID, val productId: String, val accountId: String, val profileId: String, val basePlanId: String?, val offerId: String?) {
19
- fun json(): JSONObject = JSONObject().put("runId", runId.toString()).put("orderId", orderId.toString()).put("receiptId", receiptId.toString()).put("productId", productId).put("obfuscatedAccountId", accountId).put("obfuscatedProfileId", profileId).put("basePlanId", basePlanId ?: JSONObject.NULL).put("offerId", offerId ?: JSONObject.NULL)
20
- companion object {
21
- fun parse(value: JSONObject, runId: UUID = UUID.fromString(value.getString("runId"))): IndianaNativePendingOrder {
22
- val result = IndianaNativePendingOrder(runId, UUID.fromString(value.getString("orderId")), UUID.fromString(value.getString("receiptId")), value.getString("productId"), value.getString("obfuscatedAccountId"), value.getString("obfuscatedProfileId"), value.optString("basePlanId").takeIf { value.has("basePlanId") && !value.isNull("basePlanId") }, value.optString("offerId").takeIf { value.has("offerId") && !value.isNull("offerId") })
23
- if (result.productId.length !in 1..255 || result.accountId.length !in 1..64 || result.profileId.length !in 1..64) throw IndianaClientException("invalid_response")
24
- return result
25
- }
26
- }
27
- }
28
- /** Current order metadata only. No token/JWS/customer credential or legacy storage is read. */
29
- internal interface IndianaNativePendingStorage {
30
- fun read(order: UUID): IndianaNativePendingOrder?
31
- fun orders(): List<IndianaNativePendingOrder>
32
- fun remove(order: UUID)
33
- }
34
- internal class IndianaNativePendingKeystore(context: Context, scope: IndianaScope): IndianaNativePendingStorage {
35
- private val namespace = "${context.packageName}:${scope.projectId}:${scope.environment.value}"
36
- private val hash = MessageDigest.getInstance("SHA-256").digest(namespace.toByteArray()).joinToString("") { "%02x".format(it) }
37
- private val directory = File(context.noBackupFilesDir, "grow-funnel-orders-$hash")
38
- private val alias = "grow.funnel.orders.v1.$hash"
39
- companion object { private val lock = Any() }
40
- private fun file(order: UUID) = AtomicFile(File(directory, "$order.json"))
41
- private fun aad(order: UUID) = "grow_funnel_order_v1\u0000$namespace\u0000$order".toByteArray()
42
- private fun key(create: Boolean): SecretKey {
43
- val store = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
44
- (store.getKey(alias, null) as? SecretKey)?.let { return it }
45
- if (!create || directory.listFiles()?.any { it.isFile } == true) throw IndianaClientException("protected_storage_unavailable")
46
- val spec = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT).setKeySize(256).setBlockModes(KeyProperties.BLOCK_MODE_GCM).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE).setRandomizedEncryptionRequired(true).build()
47
- return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").apply { init(spec) }.generateKey()
48
- }
49
- override fun read(order: UUID): IndianaNativePendingOrder? = synchronized(lock) {
50
- try {
51
- if (!file(order).baseFile.exists()) return@synchronized null
52
- val bytes = file(order).openRead().use { input -> val output = java.io.ByteArrayOutputStream(); val buffer = ByteArray(1024); while (true) { val count = input.read(buffer); if (count < 0) break; if (output.size() + count > 8192) throw IndianaClientException("protected_storage_unavailable"); output.write(buffer, 0, count) }; output.toByteArray() }
53
- val value = JSONObject(bytes.toString(Charsets.UTF_8)); if (value.keys().asSequence().toSet() != setOf("format", "nonce", "ciphertext") || value.getString("format") != "grow_funnel_order_v1") throw IndianaClientException("protected_storage_unavailable")
54
- val nonce = Base64.decode(value.getString("nonce"), Base64.NO_WRAP); if (nonce.size != 12) throw IndianaClientException("protected_storage_unavailable")
55
- val cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, key(false), GCMParameterSpec(128, nonce)); cipher.updateAAD(aad(order))
56
- val plaintext = cipher.doFinal(Base64.decode(value.getString("ciphertext"), Base64.NO_WRAP))
57
- try { IndianaNativePendingOrder.parse(JSONObject(plaintext.toString(Charsets.UTF_8))).also { if (it.orderId != order) throw IndianaClientException("protected_storage_unavailable") } } finally { plaintext.fill(0) }
58
- } catch (_: Exception) { throw IndianaClientException("protected_storage_unavailable") }
59
- }
60
- override fun orders(): List<IndianaNativePendingOrder> = synchronized(lock) {
61
- val files = directory.listFiles()?.filter { it.name.endsWith(".json") } ?: emptyList()
62
- if (files.size > 100) throw IndianaClientException("protected_storage_unavailable")
63
- files.mapNotNull { read(UUID.fromString(it.name.removeSuffix(".json"))) }
64
- }
65
- fun orders(runId: UUID): List<IndianaNativePendingOrder> = orders().filter { it.runId == runId }
66
- fun contains(record: IndianaNativePendingOrder): Boolean { val previous = read(record.orderId) ?: return false; if (previous != record) throw IndianaClientException("protected_storage_unavailable"); return true }
67
- fun put(record: IndianaNativePendingOrder) = synchronized(lock) {
68
- if (contains(record)) return@synchronized
69
- if ((directory.listFiles()?.count { it.name.endsWith(".json") } ?: 0) >= 100) throw IndianaClientException("protected_storage_unavailable")
70
- val currentKey = key(true)
71
- if (!directory.exists() && !directory.mkdirs()) throw IndianaClientException("protected_storage_unavailable")
72
- val plaintext = record.json().toString().toByteArray()
73
- try {
74
- val cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, currentKey); cipher.updateAAD(aad(record.orderId))
75
- val bytes = JSONObject().put("format", "grow_funnel_order_v1").put("nonce", Base64.encodeToString(cipher.iv, Base64.NO_WRAP)).put("ciphertext", Base64.encodeToString(cipher.doFinal(plaintext), Base64.NO_WRAP)).toString().toByteArray()
76
- val atomic = file(record.orderId); val stream = atomic.startWrite()
77
- try { stream.write(bytes); atomic.finishWrite(stream) } catch (error: Exception) { atomic.failWrite(stream); throw error }
78
- } finally { plaintext.fill(0) }
79
- }
80
- override fun remove(order: UUID) = synchronized(lock) { file(order).delete() }
81
- }
@@ -1,52 +0,0 @@
1
- import Foundation
2
- import Security
3
-
4
- struct IndianaNativePendingOrder: Codable, Sendable, Equatable {
5
- let runId: UUID; let orderId: UUID; let receiptId: UUID; let productId: String; let accountToken: UUID
6
- }
7
- /// Stores only current order bindings; never a run token, customer token or JWS.
8
- protocol IndianaNativePendingStorage: Sendable {
9
- func read(_ order: UUID) async throws -> IndianaNativePendingOrder?
10
- func orders() async throws -> [IndianaNativePendingOrder]
11
- func orders(runId: UUID) async throws -> [IndianaNativePendingOrder]
12
- func contains(_ record: IndianaNativePendingOrder) async throws -> Bool
13
- func put(_ record: IndianaNativePendingOrder) async throws
14
- func remove(_ order: UUID) async throws
15
- }
16
- actor IndianaNativePendingKeychain: IndianaNativePendingStorage {
17
- private let service: String
18
- init(scope: IndianaScope) { service = "\(Bundle.main.bundleIdentifier ?? "Indiana").grow.funnel.orders.v1.\(scope.projectId.uuidString.lowercased()).\(scope.environment.rawValue)" }
19
- private func query(_ order: UUID) -> [String: Any] { [kSecClass as String:kSecClassGenericPassword,kSecAttrService as String:service,kSecAttrAccount as String:order.uuidString.lowercased()] }
20
- func read(_ order: UUID) throws -> IndianaNativePendingOrder? {
21
- var result: CFTypeRef?
22
- let status = SecItemCopyMatching(query(order).merging([kSecReturnData as String:true]) { _, new in new } as CFDictionary, &result)
23
- if status == errSecItemNotFound { return nil }
24
- guard status == errSecSuccess, let data = result as? Data, data.count <= 4096, let original = try? JSONDecoder().decode(IndianaNativePendingOrder.self, from: data), original.orderId == order else { throw IndianaClientError.protectedStorageUnavailable }
25
- return original
26
- }
27
- private func allOrders() throws -> [IndianaNativePendingOrder] {
28
- let query: [String: Any] = [kSecClass as String:kSecClassGenericPassword,kSecAttrService as String:service,kSecReturnData as String:true,kSecMatchLimit as String:kSecMatchLimitAll]
29
- var result: CFTypeRef?
30
- let status = SecItemCopyMatching(query as CFDictionary, &result)
31
- if status == errSecItemNotFound { return [] }
32
- guard status == errSecSuccess, let bytes = result as? [Data], bytes.count <= 100 else { throw IndianaClientError.protectedStorageUnavailable }
33
- return try bytes.map { data in
34
- guard data.count <= 4096 else { throw IndianaClientError.protectedStorageUnavailable }
35
- return try JSONDecoder().decode(IndianaNativePendingOrder.self, from: data)
36
- }
37
- }
38
- func orders() throws -> [IndianaNativePendingOrder] { try allOrders() }
39
- func orders(runId: UUID) throws -> [IndianaNativePendingOrder] { try allOrders().filter { $0.runId == runId } }
40
- func contains(_ record: IndianaNativePendingOrder) throws -> Bool {
41
- guard let original = try read(record.orderId) else { return false }; guard original == record else { throw IndianaClientError.protectedStorageUnavailable }; return true
42
- }
43
- func put(_ record: IndianaNativePendingOrder) throws {
44
- if try contains(record) { return }
45
- guard try allOrders().count < 100 else { throw IndianaClientError.protectedStorageUnavailable }
46
- let bytes = try JSONEncoder().encode(record)
47
- let status = SecItemAdd(query(record.orderId).merging([kSecValueData as String:bytes,kSecAttrAccessible as String:kSecAttrAccessibleWhenUnlockedThisDeviceOnly]) { _, new in new } as CFDictionary, nil)
48
- if status == errSecDuplicateItem { guard try contains(record) else { throw IndianaClientError.protectedStorageUnavailable }; return }
49
- guard status == errSecSuccess else { throw IndianaClientError.protectedStorageUnavailable }
50
- }
51
- func remove(_ order: UUID) throws { let status = SecItemDelete(query(order) as CFDictionary); guard status == errSecSuccess || status == errSecItemNotFound else { throw IndianaClientError.protectedStorageUnavailable } }
52
- }