react-native-nitro-fetch 1.3.1 → 1.3.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 +12 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +148 -55
- package/ios/NitroAutoPrefetcher.swift +149 -53
- package/lib/module/CurlGenerator.js.map +2 -1
- package/lib/module/HermesProfiler.js.map +1 -2
- package/lib/module/NetworkInspector.js +4 -0
- package/lib/module/NetworkInspector.js.map +1 -1
- package/lib/module/NitroFetch.nitro.js.map +1 -2
- package/lib/module/NitroInstances.js.map +1 -1
- package/lib/module/Request.js.map +2 -1
- package/lib/module/Response.js +3 -1
- package/lib/module/Response.js.map +2 -2
- package/lib/module/fetch.js +8 -10
- package/lib/module/fetch.js.map +1 -1
- package/lib/module/index.js.map +1 -2
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +2 -1
- package/lib/module/tokenRefresh.js +1 -4
- package/lib/module/tokenRefresh.js.map +2 -1
- package/lib/module/utf8.js.map +2 -2
- package/lib/typescript/src/fetch.d.ts.map +1 -1
- package/lib/typescript/src/tokenRefresh.d.ts +14 -0
- package/lib/typescript/src/tokenRefresh.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/CurlGenerator.js +23 -26
- package/src/Headers.js +108 -116
- package/src/HermesProfiler.js +16 -18
- package/src/NetworkInspector.js +171 -179
- package/src/NitroInstances.js +2 -1
- package/src/Request.js +167 -164
- package/src/Response.js +244 -242
- package/src/fetch.js +708 -693
- package/src/fetch.ts +15 -16
- package/src/index.js +17 -2
- package/src/index.web.js +69 -67
- package/src/tokenRefresh.js +75 -77
- package/src/tokenRefresh.ts +16 -0
- package/src/utf8.js +28 -27
package/android/build.gradle
CHANGED
|
@@ -85,6 +85,18 @@ android {
|
|
|
85
85
|
release {
|
|
86
86
|
minifyEnabled false
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
// Reverting due to build failure
|
|
90
|
+
// // The app's RN-managed `debugOptimized` build type (added by the React Native
|
|
91
|
+
// // gradle plugin, app-only) falls back to this library's `release` variant, where
|
|
92
|
+
// // BuildConfig.DEBUG is false — which constant-folds out the native DevTools/CDP
|
|
93
|
+
// // network reporting in NitroFetchClient.kt. Define a matching debuggable variant
|
|
94
|
+
// // so debugOptimized links a BuildConfig.DEBUG=true build and requests surface in
|
|
95
|
+
// // the React Native DevTools Network panel.
|
|
96
|
+
// debugOptimized {
|
|
97
|
+
// initWith debug
|
|
98
|
+
// matchingFallbacks += ["release"]
|
|
99
|
+
// }
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
lintOptions {
|
|
@@ -65,20 +65,14 @@ object AutoPrefetcher {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
if (initialized) {
|
|
68
|
-
// late path — kick a single immediate prefetch with cached
|
|
68
|
+
// late path — kick a single immediate prefetch with cached tokens
|
|
69
69
|
try {
|
|
70
70
|
val prefs = context.applicationContext
|
|
71
71
|
.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
72
|
-
val
|
|
73
|
-
val tokenHeaders: Map<String, String> = if (!cacheRaw.isNullOrEmpty()) {
|
|
74
|
-
try {
|
|
75
|
-
val co = JSONObject(cacheRaw)
|
|
76
|
-
co.keys().asSequence().associateWith { k -> co.optString(k, "") }
|
|
77
|
-
} catch (_: Throwable) { emptyMap() }
|
|
78
|
-
} else emptyMap()
|
|
72
|
+
val tokens = deserializeCache(NitroFetchSecureAtRest.getDecryptedForPrefs(prefs, KEY_TOKEN_CACHE))
|
|
79
73
|
|
|
80
74
|
val single = JSONArray().apply { put(entry) }
|
|
81
|
-
startPrefetches(single,
|
|
75
|
+
startPrefetches(single, tokens)
|
|
82
76
|
} catch (_: Throwable) {}
|
|
83
77
|
}
|
|
84
78
|
}
|
|
@@ -105,13 +99,11 @@ object AutoPrefetcher {
|
|
|
105
99
|
|
|
106
100
|
val refreshed = callTokenRefreshSync(refreshConfig)
|
|
107
101
|
|
|
108
|
-
val
|
|
109
|
-
android.util.Log.d("NitroFetch", "[TokenRefresh] ✅ Success — got ${refreshed.size} header(s)")
|
|
110
|
-
refreshed.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
|
|
111
|
-
// Cache fresh
|
|
112
|
-
|
|
113
|
-
refreshed.forEach { (k, v) -> cacheJson.put(k, v) }
|
|
114
|
-
NitroFetchSecureAtRest.putEncrypted(prefs, KEY_TOKEN_CACHE, cacheJson.toString())
|
|
102
|
+
val tokens: TokenRefreshResult = if (refreshed != null) {
|
|
103
|
+
android.util.Log.d("NitroFetch", "[TokenRefresh] ✅ Success — got ${refreshed.headers.size} header(s)")
|
|
104
|
+
refreshed.headers.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
|
|
105
|
+
// Cache fresh tokens for useStoredHeaders fallback on next cold start
|
|
106
|
+
NitroFetchSecureAtRest.putEncrypted(prefs, KEY_TOKEN_CACHE, serializeCache(refreshed))
|
|
115
107
|
refreshed
|
|
116
108
|
} else {
|
|
117
109
|
android.util.Log.d("NitroFetch", "[TokenRefresh] ❌ Refresh failed — onFailure: $onFailure")
|
|
@@ -119,36 +111,28 @@ object AutoPrefetcher {
|
|
|
119
111
|
android.util.Log.d("NitroFetch", "[TokenRefresh] Skipping all prefetches")
|
|
120
112
|
return@Thread
|
|
121
113
|
}
|
|
122
|
-
// Use last cached
|
|
123
|
-
val
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
val co = JSONObject(cacheRaw)
|
|
127
|
-
co.keys().asSequence().associateWith { k -> co.optString(k, "") }
|
|
128
|
-
} catch (_: Throwable) { emptyMap() }
|
|
129
|
-
} else {
|
|
130
|
-
emptyMap()
|
|
131
|
-
}
|
|
132
|
-
android.util.Log.d("NitroFetch", "[TokenRefresh] Using cached headers (${cached.size} header(s))")
|
|
114
|
+
// Use last cached tokens (or empty if none cached yet)
|
|
115
|
+
val cached = deserializeCache(NitroFetchSecureAtRest.getDecryptedForPrefs(prefs, KEY_TOKEN_CACHE))
|
|
116
|
+
android.util.Log.d("NitroFetch", "[TokenRefresh] Using cached headers (${cached.headers.size} header(s))")
|
|
133
117
|
cached
|
|
134
118
|
}
|
|
135
119
|
|
|
136
120
|
android.util.Log.d("NitroFetch", "[TokenRefresh] Injecting token headers into ${arr.length()} prefetch URL(s)")
|
|
137
|
-
startPrefetches(arr,
|
|
121
|
+
startPrefetches(arr, tokens)
|
|
138
122
|
} catch (_: Throwable) {
|
|
139
123
|
// Best-effort — never crash the app
|
|
140
124
|
}
|
|
141
125
|
}.start()
|
|
142
126
|
} else {
|
|
143
127
|
// No token refresh config — proceed on current thread (Cronet is async)
|
|
144
|
-
startPrefetches(arr,
|
|
128
|
+
startPrefetches(arr, TokenRefreshResult.EMPTY)
|
|
145
129
|
}
|
|
146
130
|
} catch (_: Throwable) {
|
|
147
131
|
// ignore – prefetch-on-start is best-effort
|
|
148
132
|
}
|
|
149
133
|
}
|
|
150
134
|
|
|
151
|
-
private fun startPrefetches(arr: JSONArray,
|
|
135
|
+
private fun startPrefetches(arr: JSONArray, tokens: TokenRefreshResult) {
|
|
152
136
|
for (i in 0 until arr.length()) {
|
|
153
137
|
val o = arr.optJSONObject(i) ?: continue
|
|
154
138
|
val url = o.optString("url", null) ?: continue
|
|
@@ -160,12 +144,12 @@ object AutoPrefetcher {
|
|
|
160
144
|
headersObj.keys().forEachRemaining { k ->
|
|
161
145
|
merged[k] = headersObj.optString(k, "")
|
|
162
146
|
}
|
|
163
|
-
|
|
147
|
+
tokens.headers.forEach { (k, v) -> merged[k] = v }
|
|
164
148
|
merged["prefetchKey"] = prefetchKey
|
|
165
149
|
|
|
166
150
|
android.util.Log.d("NitroFetch", "[TokenRefresh] Prefetching $url with ${merged.size} header(s)")
|
|
167
151
|
merged.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
|
|
168
|
-
val req = buildNitroRequestFromEntry(url, merged, o)
|
|
152
|
+
val req = buildNitroRequestFromEntry(url, merged, o, tokens)
|
|
169
153
|
|
|
170
154
|
if (FetchCache.getPending(prefetchKey) != null) continue
|
|
171
155
|
val entryTtlMs = if (o.has("prefetchCacheTtlMs") && !o.isNull("prefetchCacheTtlMs")) {
|
|
@@ -237,6 +221,7 @@ object AutoPrefetcher {
|
|
|
237
221
|
url: String,
|
|
238
222
|
mergedHeaders: Map<String, String>,
|
|
239
223
|
entry: JSONObject?,
|
|
224
|
+
tokens: TokenRefreshResult = TokenRefreshResult.EMPTY,
|
|
240
225
|
): NitroRequest {
|
|
241
226
|
val headerObjs = mergedHeaders.map { (k, v) -> NitroHeader(k, v) }.toTypedArray()
|
|
242
227
|
|
|
@@ -244,9 +229,10 @@ object AutoPrefetcher {
|
|
|
244
229
|
val method: NitroRequestMethod? = methodStr?.let {
|
|
245
230
|
runCatching { NitroRequestMethod.valueOf(it) }.getOrNull()
|
|
246
231
|
}
|
|
247
|
-
val
|
|
232
|
+
val rawBodyString = entry
|
|
248
233
|
?.takeIf { it.has("bodyString") && !it.isNull("bodyString") }
|
|
249
234
|
?.optString("bodyString")
|
|
235
|
+
val bodyString = injectBodyFields(rawBodyString, tokens.bodyFields)
|
|
250
236
|
val bodyBytes = entry
|
|
251
237
|
?.takeIf { it.has("bodyBytes") && !it.isNull("bodyBytes") }
|
|
252
238
|
?.optString("bodyBytes")
|
|
@@ -261,8 +247,8 @@ object AutoPrefetcher {
|
|
|
261
247
|
?.optDouble("prefetchCacheTtlMs")
|
|
262
248
|
|
|
263
249
|
val formArr = entry?.optJSONArray("bodyFormData")
|
|
264
|
-
val
|
|
265
|
-
|
|
250
|
+
val baseParts: List<NitroFormDataPart> = formArr?.let { ja ->
|
|
251
|
+
List(ja.length()) { i ->
|
|
266
252
|
val p = ja.optJSONObject(i) ?: JSONObject()
|
|
267
253
|
NitroFormDataPart(
|
|
268
254
|
name = p.optString("name", ""),
|
|
@@ -272,7 +258,9 @@ object AutoPrefetcher {
|
|
|
272
258
|
mimeType = if (p.has("mimeType") && !p.isNull("mimeType")) p.optString("mimeType") else null
|
|
273
259
|
)
|
|
274
260
|
}
|
|
275
|
-
}
|
|
261
|
+
} ?: emptyList()
|
|
262
|
+
val bodyFormData: Array<NitroFormDataPart>? =
|
|
263
|
+
injectFormFields(baseParts, tokens.formFields)?.toTypedArray()
|
|
276
264
|
|
|
277
265
|
return NitroRequest(
|
|
278
266
|
url = url,
|
|
@@ -290,7 +278,17 @@ object AutoPrefetcher {
|
|
|
290
278
|
|
|
291
279
|
// MARK: - Token refresh (synchronous, runs on background thread)
|
|
292
280
|
|
|
293
|
-
private
|
|
281
|
+
private data class TokenRefreshResult(
|
|
282
|
+
val headers: Map<String, String>,
|
|
283
|
+
val bodyFields: Map<String, String>,
|
|
284
|
+
val formFields: Map<String, String>,
|
|
285
|
+
) {
|
|
286
|
+
companion object {
|
|
287
|
+
val EMPTY = TokenRefreshResult(emptyMap(), emptyMap(), emptyMap())
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private fun callTokenRefreshSync(config: JSONObject): TokenRefreshResult? {
|
|
294
292
|
return try {
|
|
295
293
|
val urlStr = config.optString("url", null) ?: return null
|
|
296
294
|
val method = config.optString("method", "POST")
|
|
@@ -338,33 +336,31 @@ object AutoPrefetcher {
|
|
|
338
336
|
body: String,
|
|
339
337
|
responseType: String,
|
|
340
338
|
config: JSONObject
|
|
341
|
-
):
|
|
342
|
-
val
|
|
339
|
+
): TokenRefreshResult {
|
|
340
|
+
val headers = mutableMapOf<String, String>()
|
|
341
|
+
val bodyFields = mutableMapOf<String, String>()
|
|
342
|
+
val formFields = mutableMapOf<String, String>()
|
|
343
343
|
|
|
344
344
|
if (responseType == "text") {
|
|
345
345
|
val textHeader = config.optString("textHeader", null)
|
|
346
346
|
if (textHeader != null) {
|
|
347
347
|
val textTemplate = config.optString("textTemplate", null)
|
|
348
|
-
|
|
348
|
+
headers[textHeader] = textTemplate?.replace("{{value}}", body) ?: body
|
|
349
349
|
}
|
|
350
|
-
|
|
350
|
+
config.optString("bodyTextPath", null)?.let { bodyFields[it] = body }
|
|
351
|
+
config.optString("formDataTextField", null)?.let { formFields[it] = body }
|
|
352
|
+
return TokenRefreshResult(headers, bodyFields, formFields)
|
|
351
353
|
}
|
|
352
354
|
|
|
353
355
|
// JSON
|
|
354
|
-
val json = try { JSONObject(body) } catch (_: Throwable) {
|
|
355
|
-
|
|
356
|
-
val mappings = config.optJSONArray("mappings")
|
|
357
|
-
if (mappings != null) {
|
|
358
|
-
for (i in 0 until mappings.length()) {
|
|
359
|
-
val m = mappings.optJSONObject(i) ?: continue
|
|
360
|
-
val jsonPath = m.optString("jsonPath", null) ?: continue
|
|
361
|
-
val header = m.optString("header", null) ?: continue
|
|
362
|
-
val value = getNestedField(json, jsonPath) ?: continue
|
|
363
|
-
val tmpl = m.optString("valueTemplate", null)
|
|
364
|
-
result[header] = tmpl?.replace("{{value}}", value) ?: value
|
|
365
|
-
}
|
|
356
|
+
val json = try { JSONObject(body) } catch (_: Throwable) {
|
|
357
|
+
return TokenRefreshResult(headers, bodyFields, formFields)
|
|
366
358
|
}
|
|
367
359
|
|
|
360
|
+
collectMappings(json, config.optJSONArray("mappings"), "header", headers)
|
|
361
|
+
collectMappings(json, config.optJSONArray("bodyMappings"), "bodyPath", bodyFields)
|
|
362
|
+
collectMappings(json, config.optJSONArray("formDataMappings"), "field", formFields)
|
|
363
|
+
|
|
368
364
|
val compositeHeaders = config.optJSONArray("compositeHeaders")
|
|
369
365
|
if (compositeHeaders != null) {
|
|
370
366
|
for (i in 0 until compositeHeaders.length()) {
|
|
@@ -377,11 +373,29 @@ object AutoPrefetcher {
|
|
|
377
373
|
val val2 = getNestedField(json, paths.optString(ph, ""))
|
|
378
374
|
built = built.replace("{{$ph}}", val2 ?: "")
|
|
379
375
|
}
|
|
380
|
-
|
|
376
|
+
headers[header] = built
|
|
381
377
|
}
|
|
382
378
|
}
|
|
383
379
|
|
|
384
|
-
return
|
|
380
|
+
return TokenRefreshResult(headers, bodyFields, formFields)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// jsonPath -> value (optionally templated), keyed by the mapping's `destKey` field.
|
|
384
|
+
private fun collectMappings(
|
|
385
|
+
json: JSONObject,
|
|
386
|
+
arr: JSONArray?,
|
|
387
|
+
destKey: String,
|
|
388
|
+
into: MutableMap<String, String>,
|
|
389
|
+
) {
|
|
390
|
+
if (arr == null) return
|
|
391
|
+
for (i in 0 until arr.length()) {
|
|
392
|
+
val m = arr.optJSONObject(i) ?: continue
|
|
393
|
+
val jsonPath = m.optString("jsonPath", null) ?: continue
|
|
394
|
+
val dest = m.optString(destKey, null) ?: continue
|
|
395
|
+
val value = getNestedField(json, jsonPath) ?: continue
|
|
396
|
+
val tmpl = m.optString("valueTemplate", null)
|
|
397
|
+
into[dest] = tmpl?.replace("{{value}}", value) ?: value
|
|
398
|
+
}
|
|
385
399
|
}
|
|
386
400
|
|
|
387
401
|
private fun getNestedField(obj: JSONObject, dotPath: String): String? {
|
|
@@ -393,4 +407,83 @@ object AutoPrefetcher {
|
|
|
393
407
|
}
|
|
394
408
|
return current.toString()
|
|
395
409
|
}
|
|
410
|
+
|
|
411
|
+
private fun setNestedField(root: JSONObject, dotPath: String, value: String) {
|
|
412
|
+
val parts = dotPath.split(".")
|
|
413
|
+
var current = root
|
|
414
|
+
for (i in 0 until parts.size - 1) {
|
|
415
|
+
val key = parts[i]
|
|
416
|
+
val existing = current.optJSONObject(key)
|
|
417
|
+
current = existing ?: JSONObject().also { current.put(key, it) }
|
|
418
|
+
}
|
|
419
|
+
current.put(parts.last(), value)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private fun injectBodyFields(rawBody: String?, fields: Map<String, String>): String? {
|
|
423
|
+
if (fields.isEmpty()) return rawBody
|
|
424
|
+
// Don't synthesize a JSON body where there wasn't one (e.g. a GET or a
|
|
425
|
+
// form-data request) — only rewrite an existing JSON body.
|
|
426
|
+
if (rawBody.isNullOrEmpty()) return rawBody
|
|
427
|
+
val root = try {
|
|
428
|
+
JSONObject(rawBody)
|
|
429
|
+
} catch (_: Throwable) {
|
|
430
|
+
return rawBody
|
|
431
|
+
}
|
|
432
|
+
fields.forEach { (path, value) -> setNestedField(root, path, value) }
|
|
433
|
+
return root.toString()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private fun injectFormFields(
|
|
437
|
+
parts: List<NitroFormDataPart>,
|
|
438
|
+
fields: Map<String, String>,
|
|
439
|
+
): List<NitroFormDataPart>? {
|
|
440
|
+
if (fields.isEmpty()) return parts.ifEmpty { null }
|
|
441
|
+
// Don't synthesize a multipart body where there wasn't one.
|
|
442
|
+
if (parts.isEmpty()) return null
|
|
443
|
+
val result = parts.toMutableList()
|
|
444
|
+
fields.forEach { (name, value) ->
|
|
445
|
+
val idx = result.indexOfFirst { it.name == name }
|
|
446
|
+
if (idx >= 0) {
|
|
447
|
+
val old = result[idx]
|
|
448
|
+
result[idx] = NitroFormDataPart(
|
|
449
|
+
name = old.name, value = value,
|
|
450
|
+
fileUri = null, fileName = old.fileName, mimeType = old.mimeType
|
|
451
|
+
)
|
|
452
|
+
} else {
|
|
453
|
+
result.add(NitroFormDataPart(name, value, null, null, null))
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return result
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private fun mapToJson(map: Map<String, String>): JSONObject {
|
|
460
|
+
val o = JSONObject()
|
|
461
|
+
map.forEach { (k, v) -> o.put(k, v) }
|
|
462
|
+
return o
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private fun jsonToMap(obj: JSONObject?): Map<String, String> {
|
|
466
|
+
if (obj == null) return emptyMap()
|
|
467
|
+
return obj.keys().asSequence().associateWith { k -> obj.optString(k, "") }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private fun serializeCache(result: TokenRefreshResult): String =
|
|
471
|
+
JSONObject().apply {
|
|
472
|
+
put("headers", mapToJson(result.headers))
|
|
473
|
+
put("bodyFields", mapToJson(result.bodyFields))
|
|
474
|
+
put("formFields", mapToJson(result.formFields))
|
|
475
|
+
}.toString()
|
|
476
|
+
|
|
477
|
+
private fun deserializeCache(raw: String?): TokenRefreshResult {
|
|
478
|
+
if (raw.isNullOrEmpty()) return TokenRefreshResult.EMPTY
|
|
479
|
+
val o = try { JSONObject(raw) } catch (_: Throwable) { return TokenRefreshResult.EMPTY }
|
|
480
|
+
if (!o.has("headers") && !o.has("bodyFields") && !o.has("formFields")) {
|
|
481
|
+
return TokenRefreshResult(jsonToMap(o), emptyMap(), emptyMap())
|
|
482
|
+
}
|
|
483
|
+
return TokenRefreshResult(
|
|
484
|
+
headers = jsonToMap(o.optJSONObject("headers")),
|
|
485
|
+
bodyFields = jsonToMap(o.optJSONObject("bodyFields")),
|
|
486
|
+
formFields = jsonToMap(o.optJSONObject("formFields")),
|
|
487
|
+
)
|
|
488
|
+
}
|
|
396
489
|
}
|
|
@@ -111,19 +111,14 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
if initialized {
|
|
114
|
-
// Late path — apply cached
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
!cacheRaw.isEmpty,
|
|
118
|
-
let cacheData = cacheRaw.data(using: .utf8),
|
|
119
|
-
let cacheObj = try? JSONSerialization.jsonObject(with: cacheData) as? [String: String] {
|
|
120
|
-
tokenHeaders = cacheObj
|
|
121
|
-
}
|
|
114
|
+
// Late path — apply cached tokens + kick immediate prefetch
|
|
115
|
+
let tokens = deserializeCache(
|
|
116
|
+
NitroFetchSecureAtRest.decryptedString(forKey: tokenCacheKey, defaults: userDefaults))
|
|
122
117
|
var merged: [String: String] = headers
|
|
123
|
-
for (k, v) in
|
|
118
|
+
for (k, v) in tokens.headers { merged[k] = v }
|
|
124
119
|
var hdrs: [NitroHeader] = merged.map { NitroHeader(key: $0.key, value: $0.value) }
|
|
125
120
|
hdrs.append(NitroHeader(key: "prefetchKey", value: prefetchKey))
|
|
126
|
-
let req = buildNitroRequest(from: entry, mergedHeaders: hdrs)
|
|
121
|
+
let req = buildNitroRequest(from: entry, mergedHeaders: hdrs, tokens: tokens)
|
|
127
122
|
Task {
|
|
128
123
|
do { try await NitroFetchClient.prefetchStatic(req) } catch { /* best-effort */ }
|
|
129
124
|
}
|
|
@@ -143,8 +138,8 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
143
138
|
let refreshRaw = NitroFetchSecureAtRest.decryptedString(forKey: tokenRefreshKey, defaults: userDefaults)
|
|
144
139
|
|
|
145
140
|
Task {
|
|
146
|
-
// Resolve
|
|
147
|
-
let
|
|
141
|
+
// Resolve tokens (may require a network call)
|
|
142
|
+
let tokens: TokenRefreshResult
|
|
148
143
|
if let refreshRaw = refreshRaw,
|
|
149
144
|
!refreshRaw.isEmpty,
|
|
150
145
|
let refreshData = refreshRaw.data(using: .utf8),
|
|
@@ -154,35 +149,29 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
154
149
|
print("[NitroFetch][TokenRefresh] Calling refresh endpoint: \(refreshURL)")
|
|
155
150
|
let refreshed = try? await callTokenRefresh(config: refreshObj)
|
|
156
151
|
if let refreshed = refreshed {
|
|
157
|
-
print("[NitroFetch][TokenRefresh] ✅ Success — got \(refreshed.count) header(s)")
|
|
158
|
-
for (k, v) in refreshed { print("[NitroFetch][TokenRefresh] \(k): \(v)") }
|
|
159
|
-
// Cache fresh
|
|
160
|
-
if let
|
|
161
|
-
let cacheStr = String(data: cacheData, encoding: .utf8) {
|
|
152
|
+
print("[NitroFetch][TokenRefresh] ✅ Success — got \(refreshed.headers.count) header(s)")
|
|
153
|
+
for (k, v) in refreshed.headers { print("[NitroFetch][TokenRefresh] \(k): \(v)") }
|
|
154
|
+
// Cache fresh tokens for useStoredHeaders fallback on next cold start
|
|
155
|
+
if let cacheStr = serializeCache(refreshed) {
|
|
162
156
|
try? NitroFetchSecureAtRest.setEncrypted(cacheStr, forKey: tokenCacheKey, defaults: userDefaults)
|
|
163
157
|
}
|
|
164
|
-
|
|
158
|
+
tokens = refreshed
|
|
165
159
|
} else {
|
|
166
160
|
print("[NitroFetch][TokenRefresh] ❌ Refresh failed — onFailure: \(onFailure)")
|
|
167
161
|
if onFailure == "skip" {
|
|
168
162
|
print("[NitroFetch][TokenRefresh] Skipping all prefetches")
|
|
169
163
|
return
|
|
170
164
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
let cacheObj = try? JSONSerialization.jsonObject(with: cacheData) as? [String: String] {
|
|
176
|
-
cached = cacheObj
|
|
177
|
-
}
|
|
178
|
-
print("[NitroFetch][TokenRefresh] Using cached headers (\(cached.count) header(s))")
|
|
179
|
-
tokenHeaders = cached
|
|
165
|
+
let cached = deserializeCache(
|
|
166
|
+
NitroFetchSecureAtRest.decryptedString(forKey: tokenCacheKey, defaults: userDefaults))
|
|
167
|
+
print("[NitroFetch][TokenRefresh] Using cached headers (\(cached.headers.count) header(s))")
|
|
168
|
+
tokens = cached
|
|
180
169
|
}
|
|
181
170
|
} else {
|
|
182
|
-
|
|
171
|
+
tokens = .empty
|
|
183
172
|
}
|
|
184
173
|
|
|
185
|
-
// Launch a prefetch task per entry with merged headers
|
|
174
|
+
// Launch a prefetch task per entry with merged headers + body/form injection
|
|
186
175
|
print("[NitroFetch][TokenRefresh] Injecting token headers into \(arr.count) prefetch URL(s)")
|
|
187
176
|
for item in arr {
|
|
188
177
|
guard let obj = item as? [String: Any] else { continue }
|
|
@@ -193,7 +182,7 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
193
182
|
// Merge: static headers first, token headers override
|
|
194
183
|
var merged: [String: String] = [:]
|
|
195
184
|
for (k, v) in headersDict { merged[k] = String(describing: v) }
|
|
196
|
-
for (k, v) in
|
|
185
|
+
for (k, v) in tokens.headers { merged[k] = v }
|
|
197
186
|
|
|
198
187
|
var headers: [NitroHeader] = merged.map { NitroHeader(key: $0.key, value: $0.value) }
|
|
199
188
|
headers.append(NitroHeader(key: "prefetchKey", value: prefetchKey))
|
|
@@ -201,7 +190,7 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
201
190
|
print("[NitroFetch][TokenRefresh] Prefetching \(url) with \(merged.count) header(s)")
|
|
202
191
|
for (k, v) in merged { print("[NitroFetch][TokenRefresh] \(k): \(v)") }
|
|
203
192
|
|
|
204
|
-
let req = buildNitroRequest(from: obj, mergedHeaders: headers)
|
|
193
|
+
let req = buildNitroRequest(from: obj, mergedHeaders: headers, tokens: tokens)
|
|
205
194
|
Task {
|
|
206
195
|
do { try await NitroFetchClient.prefetchStatic(req) } catch { /* ignore – best effort */ }
|
|
207
196
|
}
|
|
@@ -248,18 +237,19 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
248
237
|
|
|
249
238
|
private static func buildNitroRequest(
|
|
250
239
|
from entry: [String: Any],
|
|
251
|
-
mergedHeaders: [NitroHeader]
|
|
240
|
+
mergedHeaders: [NitroHeader],
|
|
241
|
+
tokens: TokenRefreshResult = .empty
|
|
252
242
|
) -> NitroRequest {
|
|
253
243
|
let url = (entry["url"] as? String) ?? ""
|
|
254
244
|
let methodStr = entry["method"] as? String
|
|
255
245
|
let method: NitroRequestMethod? = methodStr.flatMap { NitroRequestMethod(fromString: $0) }
|
|
256
|
-
let bodyString = entry["bodyString"] as? String
|
|
246
|
+
let bodyString = injectBodyFields(entry["bodyString"] as? String, fields: tokens.bodyFields)
|
|
257
247
|
let bodyBytes = entry["bodyBytes"] as? String
|
|
258
248
|
let timeoutMs = (entry["timeoutMs"] as? NSNumber)?.doubleValue
|
|
259
249
|
let followRedirects = (entry["followRedirects"] as? Bool) ?? true
|
|
260
250
|
let prefetchCacheTtlMs = (entry["prefetchCacheTtlMs"] as? NSNumber)?.doubleValue
|
|
261
251
|
|
|
262
|
-
let
|
|
252
|
+
let baseParts: [NitroFormDataPart] = (entry["bodyFormData"] as? [[String: Any]])?.map { p in
|
|
263
253
|
NitroFormDataPart(
|
|
264
254
|
name: (p["name"] as? String) ?? "",
|
|
265
255
|
value: p["value"] as? String,
|
|
@@ -267,7 +257,8 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
267
257
|
fileName: p["fileName"] as? String,
|
|
268
258
|
mimeType: p["mimeType"] as? String
|
|
269
259
|
)
|
|
270
|
-
}
|
|
260
|
+
} ?? []
|
|
261
|
+
let formData: [NitroFormDataPart]? = injectFormFields(baseParts, fields: tokens.formFields)
|
|
271
262
|
|
|
272
263
|
return NitroRequest(
|
|
273
264
|
url: url,
|
|
@@ -285,7 +276,14 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
285
276
|
|
|
286
277
|
// MARK: - Token refresh
|
|
287
278
|
|
|
288
|
-
|
|
279
|
+
struct TokenRefreshResult {
|
|
280
|
+
var headers: [String: String]
|
|
281
|
+
var bodyFields: [String: String]
|
|
282
|
+
var formFields: [String: String]
|
|
283
|
+
static let empty = TokenRefreshResult(headers: [:], bodyFields: [:], formFields: [:])
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private static func callTokenRefresh(config: [String: Any]) async throws -> TokenRefreshResult {
|
|
289
287
|
guard let urlStr = config["url"] as? String,
|
|
290
288
|
let url = URL(string: urlStr) else {
|
|
291
289
|
throw NSError(domain: "NitroAutoPrefetcher", code: -1,
|
|
@@ -315,18 +313,22 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
315
313
|
private static func parseTokenResponse(
|
|
316
314
|
data: Data,
|
|
317
315
|
config: [String: Any]
|
|
318
|
-
) throws ->
|
|
316
|
+
) throws -> TokenRefreshResult {
|
|
319
317
|
let responseType = config["responseType"] as? String ?? "json"
|
|
320
|
-
var
|
|
318
|
+
var headers: [String: String] = [:]
|
|
319
|
+
var bodyFields: [String: String] = [:]
|
|
320
|
+
var formFields: [String: String] = [:]
|
|
321
321
|
|
|
322
322
|
if responseType == "text" {
|
|
323
323
|
let text = String(data: data, encoding: .utf8) ?? ""
|
|
324
324
|
if let textHeader = config["textHeader"] as? String {
|
|
325
|
-
|
|
325
|
+
headers[textHeader] = (config["textTemplate"] as? String)
|
|
326
326
|
.map { $0.replacingOccurrences(of: "{{value}}", with: text) }
|
|
327
327
|
?? text
|
|
328
328
|
}
|
|
329
|
-
|
|
329
|
+
if let bodyTextPath = config["bodyTextPath"] as? String { bodyFields[bodyTextPath] = text }
|
|
330
|
+
if let formDataTextField = config["formDataTextField"] as? String { formFields[formDataTextField] = text }
|
|
331
|
+
return TokenRefreshResult(headers: headers, bodyFields: bodyFields, formFields: formFields)
|
|
330
332
|
}
|
|
331
333
|
|
|
332
334
|
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
@@ -334,17 +336,9 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
334
336
|
userInfo: [NSLocalizedDescriptionKey: "Token refresh: invalid JSON response"])
|
|
335
337
|
}
|
|
336
338
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
let header = m["header"] as? String else { continue }
|
|
341
|
-
if let value = getNestedField(json, dotPath: jsonPath) {
|
|
342
|
-
result[header] = (m["valueTemplate"] as? String)
|
|
343
|
-
.map { $0.replacingOccurrences(of: "{{value}}", with: value) }
|
|
344
|
-
?? value
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
339
|
+
collectMappings(json, config["mappings"] as? [[String: Any]], destKey: "header", into: &headers)
|
|
340
|
+
collectMappings(json, config["bodyMappings"] as? [[String: Any]], destKey: "bodyPath", into: &bodyFields)
|
|
341
|
+
collectMappings(json, config["formDataMappings"] as? [[String: Any]], destKey: "field", into: &formFields)
|
|
348
342
|
|
|
349
343
|
if let compositeHeaders = config["compositeHeaders"] as? [[String: Any]] {
|
|
350
344
|
for comp in compositeHeaders {
|
|
@@ -356,11 +350,29 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
356
350
|
let val = getNestedField(json, dotPath: jsonPath) ?? ""
|
|
357
351
|
built = built.replacingOccurrences(of: "{{\(ph)}}", with: val)
|
|
358
352
|
}
|
|
359
|
-
|
|
353
|
+
headers[header] = built
|
|
360
354
|
}
|
|
361
355
|
}
|
|
362
356
|
|
|
363
|
-
return
|
|
357
|
+
return TokenRefreshResult(headers: headers, bodyFields: bodyFields, formFields: formFields)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// jsonPath -> value (optionally templated), keyed by each mapping's `destKey` field.
|
|
361
|
+
private static func collectMappings(
|
|
362
|
+
_ json: [String: Any],
|
|
363
|
+
_ arr: [[String: Any]]?,
|
|
364
|
+
destKey: String,
|
|
365
|
+
into: inout [String: String]
|
|
366
|
+
) {
|
|
367
|
+
guard let arr = arr else { return }
|
|
368
|
+
for m in arr {
|
|
369
|
+
guard let jsonPath = m["jsonPath"] as? String,
|
|
370
|
+
let dest = m[destKey] as? String,
|
|
371
|
+
let value = getNestedField(json, dotPath: jsonPath) else { continue }
|
|
372
|
+
into[dest] = (m["valueTemplate"] as? String)
|
|
373
|
+
.map { $0.replacingOccurrences(of: "{{value}}", with: value) }
|
|
374
|
+
?? value
|
|
375
|
+
}
|
|
364
376
|
}
|
|
365
377
|
|
|
366
378
|
private static func getNestedField(_ obj: [String: Any], dotPath: String) -> String? {
|
|
@@ -374,6 +386,90 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
374
386
|
if let s = current as? String { return s }
|
|
375
387
|
return String(describing: current)
|
|
376
388
|
}
|
|
389
|
+
|
|
390
|
+
private static func setNestedField(_ root: inout [String: Any], dotPath: String, value: String) {
|
|
391
|
+
let parts = dotPath.split(separator: ".").map(String.init)
|
|
392
|
+
guard !parts.isEmpty else { return }
|
|
393
|
+
if parts.count == 1 {
|
|
394
|
+
root[parts[0]] = value
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
var child = (root[parts[0]] as? [String: Any]) ?? [:]
|
|
398
|
+
setNestedField(&child, dotPath: parts.dropFirst().joined(separator: "."), value: value)
|
|
399
|
+
root[parts[0]] = child
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// MARK: - Body / form-data injection
|
|
403
|
+
|
|
404
|
+
private static func injectBodyFields(_ rawBody: String?, fields: [String: String]) -> String? {
|
|
405
|
+
if fields.isEmpty { return rawBody }
|
|
406
|
+
// Don't synthesize a JSON body where there wasn't one (e.g. a GET or a
|
|
407
|
+
// form-data request) — only rewrite an existing JSON body.
|
|
408
|
+
guard let rawBody = rawBody, !rawBody.isEmpty else { return rawBody }
|
|
409
|
+
guard let data = rawBody.data(using: .utf8),
|
|
410
|
+
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
411
|
+
return rawBody
|
|
412
|
+
}
|
|
413
|
+
var root = obj
|
|
414
|
+
for (path, value) in fields {
|
|
415
|
+
setNestedField(&root, dotPath: path, value: value)
|
|
416
|
+
}
|
|
417
|
+
guard let out = try? JSONSerialization.data(withJSONObject: root),
|
|
418
|
+
let str = String(data: out, encoding: .utf8) else { return rawBody }
|
|
419
|
+
return str
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private static func injectFormFields(
|
|
423
|
+
_ parts: [NitroFormDataPart],
|
|
424
|
+
fields: [String: String]
|
|
425
|
+
) -> [NitroFormDataPart]? {
|
|
426
|
+
if fields.isEmpty { return parts.isEmpty ? nil : parts }
|
|
427
|
+
// Don't synthesize a multipart body where there wasn't one.
|
|
428
|
+
if parts.isEmpty { return nil }
|
|
429
|
+
var result = parts
|
|
430
|
+
for (name, value) in fields {
|
|
431
|
+
if let idx = result.firstIndex(where: { $0.name == name }) {
|
|
432
|
+
let old = result[idx]
|
|
433
|
+
result[idx] = NitroFormDataPart(
|
|
434
|
+
name: old.name, value: value,
|
|
435
|
+
fileUri: nil, fileName: old.fileName, mimeType: old.mimeType
|
|
436
|
+
)
|
|
437
|
+
} else {
|
|
438
|
+
result.append(NitroFormDataPart(name: name, value: value, fileUri: nil, fileName: nil, mimeType: nil))
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return result
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// MARK: - Structured token cache (back-compatible with old flat-header maps)
|
|
445
|
+
|
|
446
|
+
private static func serializeCache(_ result: TokenRefreshResult) -> String? {
|
|
447
|
+
let obj: [String: Any] = [
|
|
448
|
+
"headers": result.headers,
|
|
449
|
+
"bodyFields": result.bodyFields,
|
|
450
|
+
"formFields": result.formFields,
|
|
451
|
+
]
|
|
452
|
+
guard let data = try? JSONSerialization.data(withJSONObject: obj) else { return nil }
|
|
453
|
+
return String(data: data, encoding: .utf8)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private static func deserializeCache(_ raw: String?) -> TokenRefreshResult {
|
|
457
|
+
guard let raw = raw, !raw.isEmpty,
|
|
458
|
+
let data = raw.data(using: .utf8),
|
|
459
|
+
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
460
|
+
return .empty
|
|
461
|
+
}
|
|
462
|
+
if obj["headers"] == nil && obj["bodyFields"] == nil && obj["formFields"] == nil {
|
|
463
|
+
// Old flat-header-map cache.
|
|
464
|
+
let headers = (obj as? [String: String]) ?? [:]
|
|
465
|
+
return TokenRefreshResult(headers: headers, bodyFields: [:], formFields: [:])
|
|
466
|
+
}
|
|
467
|
+
return TokenRefreshResult(
|
|
468
|
+
headers: (obj["headers"] as? [String: String]) ?? [:],
|
|
469
|
+
bodyFields: (obj["bodyFields"] as? [String: String]) ?? [:],
|
|
470
|
+
formFields: (obj["formFields"] as? [String: String]) ?? [:]
|
|
471
|
+
)
|
|
472
|
+
}
|
|
377
473
|
}
|
|
378
474
|
|
|
379
475
|
// Expose a C-ABI symbol the ObjC++ file can call
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
{"version":3,"names":["shellEscape","str","test","replace","generateCurl","options","parts","method","push","headers","h","key","toLowerCase","value","body","maxLen","truncated","length","slice","verbose","compressed","url","join"],"sourceRoot":"../../src","sources":["CurlGenerator.js"],"mappings":";;AAAA,SAASA,WAAWA,CAACC,GAAG,EAAE;
|
|
1
|
+
{"version":3,"names":["shellEscape","str","test","replace","generateCurl","options","parts","method","push","headers","h","key","toLowerCase","value","body","maxLen","truncated","length","slice","verbose","compressed","url","join"],"sourceRoot":"../../src","sources":["CurlGenerator.js"],"mappings":";;AAAA,SAASA,WAAWA,CAACC,GAAG,EAAE;EACxB,IAAI,0BAA0B,CAACC,IAAI,CAACD,GAAG,CAAC,EAAE,OAAOA,GAAG;EACpD,OAAO,GAAG,GAAGA,GAAG,CAACE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,GAAG;AAC/C;AACA,OAAO,SAASC,YAAYA,CAACC,OAAO,EAAE;EACpC,MAAMC,KAAK,GAAG,CAAC,MAAM,CAAC;EACtB,IAAID,OAAO,CAACE,MAAM,IAAIF,OAAO,CAACE,MAAM,KAAK,KAAK,EAAE;IAC9CD,KAAK,CAACE,IAAI,CAAC,IAAI,EAAER,WAAW,CAACK,OAAO,CAACE,MAAM,CAAC,CAAC;EAC/C;EACA,IAAIF,OAAO,CAACI,OAAO,EAAE;IACnB,KAAK,MAAMC,CAAC,IAAIL,OAAO,CAACI,OAAO,EAAE;MAC/B,IAAIC,CAAC,CAACC,GAAG,CAACC,WAAW,CAAC,CAAC,KAAK,aAAa,EAAE;MAC3CN,KAAK,CAACE,IAAI,CAAC,IAAI,EAAER,WAAW,CAAC,GAAGU,CAAC,CAACC,GAAG,KAAKD,CAAC,CAACG,KAAK,EAAE,CAAC,CAAC;IACvD;EACF;EACA,IAAIR,OAAO,CAACS,IAAI,EAAE;IAChB,MAAMC,MAAM,GAAG,MAAM;IACrB,MAAMC,SAAS,GACbX,OAAO,CAACS,IAAI,CAACG,MAAM,GAAGF,MAAM,GACxBV,OAAO,CAACS,IAAI,CAACI,KAAK,CAAC,CAAC,EAAEH,MAAM,CAAC,GAAG,gBAAgB,GAChDV,OAAO,CAACS,IAAI;IAClBR,KAAK,CAACE,IAAI,CAAC,IAAI,EAAER,WAAW,CAACgB,SAAS,CAAC,CAAC;EAC1C;EACA,IAAIX,OAAO,CAACc,OAAO,EAAEb,KAAK,CAACE,IAAI,CAAC,IAAI,CAAC;EACrC,IAAIH,OAAO,CAACe,UAAU,EAAEd,KAAK,CAACE,IAAI,CAAC,cAAc,CAAC;EAClDF,KAAK,CAACE,IAAI,CAACR,WAAW,CAACK,OAAO,CAACgB,GAAG,CAAC,CAAC;EACpC,OAAOf,KAAK,CAACgB,IAAI,CAAC,GAAG,CAAC;AACxB","ignoreList":[]}
|
|
2
|
+
|