feat(notifications): mint JWT pool for native fetcher; log API response
- Mint BACKGROUND_JWT_POOL_SIZE (90 + 10) distinct background JWTs with unique jti; pass jwtTokens from nativeFetcherConfig into configureNativeFetcher. - Android TimeSafariNativeFetcher: overload configure with jwtTokenPool; select bearer via epoch day mod pool size; fall back to primary jwtToken. - Log truncated plansLastUpdatedBetween response at DEBUG for prefetch debugging.
This commit is contained in:
@@ -5,6 +5,7 @@ import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
@@ -42,6 +43,8 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
||||
private static final int CONNECT_TIMEOUT_MS = 10000;
|
||||
private static final int READ_TIMEOUT_MS = 15000;
|
||||
private static final int MAX_RETRIES = 3;
|
||||
/** Max chars of response body logged at DEBUG (avoids huge log lines). */
|
||||
private static final int MAX_RESPONSE_BODY_LOG_CHARS = 4096;
|
||||
private static final int RETRY_DELAY_MS = 1000;
|
||||
|
||||
// Must match plugin's SharedPreferences name and keys (DailyNotificationPlugin / TimeSafariIntegrationManager)
|
||||
@@ -56,6 +59,9 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
||||
private volatile String apiBaseUrl;
|
||||
private volatile String activeDid;
|
||||
private volatile String jwtToken;
|
||||
/** Distinct JWTs from configureNativeFetcher `jwtTokens`; null = use jwtToken only. */
|
||||
@Nullable
|
||||
private List<String> jwtTokenPool;
|
||||
|
||||
public TimeSafariNativeFetcher(Context context) {
|
||||
this.appContext = context.getApplicationContext();
|
||||
@@ -64,11 +70,48 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
||||
|
||||
@Override
|
||||
public void configure(String apiBaseUrl, String activeDid, String jwtToken) {
|
||||
configure(apiBaseUrl, activeDid, jwtToken, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(
|
||||
String apiBaseUrl,
|
||||
String activeDid,
|
||||
String jwtToken,
|
||||
@Nullable List<String> jwtTokenPool) {
|
||||
this.apiBaseUrl = apiBaseUrl;
|
||||
this.activeDid = activeDid;
|
||||
this.jwtToken = jwtToken;
|
||||
this.jwtTokenPool =
|
||||
jwtTokenPool != null && !jwtTokenPool.isEmpty()
|
||||
? new ArrayList<>(jwtTokenPool)
|
||||
: null;
|
||||
int starredCount = getStarredPlanIds().size();
|
||||
Log.i(TAG, "Configured with API: " + apiBaseUrl + ", starredPlanIds count=" + starredCount);
|
||||
Log.i(
|
||||
TAG,
|
||||
"Configured with API: "
|
||||
+ apiBaseUrl
|
||||
+ ", starredPlanIds count="
|
||||
+ starredCount
|
||||
+ (this.jwtTokenPool != null
|
||||
? ", jwtPoolSize=" + this.jwtTokenPool.size()
|
||||
: ""));
|
||||
}
|
||||
|
||||
/** One pool entry per UTC day (epoch day mod pool size); else primary jwtToken. */
|
||||
private String selectBearerTokenForRequest() {
|
||||
List<String> pool = jwtTokenPool;
|
||||
if (pool == null || pool.isEmpty()) {
|
||||
return jwtToken;
|
||||
}
|
||||
long epochDay = System.currentTimeMillis() / (24L * 60 * 60 * 1000);
|
||||
int idx = (int) (epochDay % pool.size());
|
||||
String t = pool.get(idx);
|
||||
if (t == null || t.isEmpty()) {
|
||||
return jwtToken;
|
||||
}
|
||||
Log.i(TAG, "Bearer from JWT pool: index=" + idx + " of " + pool.size());
|
||||
return t;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -92,7 +135,8 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
Log.i(TAG, "fetchContent worker thread=" + Thread.currentThread().getName());
|
||||
if (apiBaseUrl == null || activeDid == null || jwtToken == null) {
|
||||
String bearer = selectBearerTokenForRequest();
|
||||
if (apiBaseUrl == null || activeDid == null || bearer == null || bearer.isEmpty()) {
|
||||
Log.e(TAG, "Not configured. Call configureNativeFetcher() from TypeScript first.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -104,7 +148,7 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
||||
connection.setReadTimeout(READ_TIMEOUT_MS);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("Authorization", "Bearer " + jwtToken);
|
||||
connection.setRequestProperty("Authorization", "Bearer " + bearer);
|
||||
connection.setDoOutput(true);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
@@ -143,6 +187,16 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
||||
}
|
||||
}
|
||||
String responseBody = response.toString();
|
||||
String snippet =
|
||||
responseBody.length() <= MAX_RESPONSE_BODY_LOG_CHARS
|
||||
? responseBody
|
||||
: responseBody.substring(0, MAX_RESPONSE_BODY_LOG_CHARS) + "…";
|
||||
Log.d(
|
||||
TAG,
|
||||
"plansLastUpdatedBetween response len="
|
||||
+ responseBody.length()
|
||||
+ " body="
|
||||
+ snippet);
|
||||
List<NotificationContent> contents = parseApiResponse(responseBody, context);
|
||||
if (!contents.isEmpty()) {
|
||||
updateLastAckedJwtIdFromResponse(responseBody);
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
/**
|
||||
* JWT lifetime for native New Activity background prefetch (`configureNativeFetcher`).
|
||||
* Phase A: single long-lived token minted in TS; see doc/plan-background-jwt-pool-and-expiry.md.
|
||||
* Confirm max `exp` with Endorser before raising.
|
||||
* See doc/plan-background-jwt-pool-and-expiry.md. Confirm max `exp` with Endorser before raising.
|
||||
*/
|
||||
export const BACKGROUND_JWT_EXPIRY_DAYS = 90;
|
||||
|
||||
export const BACKGROUND_JWT_EXPIRY_SECONDS =
|
||||
BACKGROUND_JWT_EXPIRY_DAYS * 24 * 60 * 60;
|
||||
|
||||
/** Headroom for retries / tests; pool size should be ≥ expiryDays + buffer. */
|
||||
export const BACKGROUND_JWT_POOL_BUFFER = 10;
|
||||
|
||||
/** Distinct JWT strings minted per configure (duplicate-JWT / daily slot). */
|
||||
export const BACKGROUND_JWT_POOL_SIZE =
|
||||
BACKGROUND_JWT_EXPIRY_DAYS + BACKGROUND_JWT_POOL_BUFFER;
|
||||
|
||||
@@ -4,7 +4,10 @@ import { entropyToMnemonic } from "ethereum-cryptography/bip39";
|
||||
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
|
||||
import { HDNode } from "@ethersproject/hdnode";
|
||||
|
||||
import { BACKGROUND_JWT_EXPIRY_SECONDS } from "@/constants/backgroundJwt";
|
||||
import {
|
||||
BACKGROUND_JWT_EXPIRY_SECONDS,
|
||||
BACKGROUND_JWT_POOL_SIZE,
|
||||
} from "@/constants/backgroundJwt";
|
||||
import {
|
||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||
createEndorserJwtForDid,
|
||||
@@ -122,6 +125,28 @@ export const accessTokenForBackgroundNotifications = async (
|
||||
return createEndorserJwtForDid(did, tokenPayload);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mint {@link BACKGROUND_JWT_POOL_SIZE} distinct JWTs for native background prefetch
|
||||
* (`configureNativeFetcher` `jwtTokens`). Unique `jti` per slot; same `exp` for all.
|
||||
*/
|
||||
export async function mintBackgroundJwtTokenPool(
|
||||
did: string,
|
||||
): Promise<string[]> {
|
||||
const nowEpoch = Math.floor(Date.now() / 1000);
|
||||
const endEpoch = nowEpoch + BACKGROUND_JWT_EXPIRY_SECONDS;
|
||||
const tokens: string[] = [];
|
||||
for (let i = 0; i < BACKGROUND_JWT_POOL_SIZE; i++) {
|
||||
const tokenPayload = {
|
||||
exp: endEpoch,
|
||||
iat: nowEpoch,
|
||||
iss: did,
|
||||
jti: `${did}#bg#${i}`,
|
||||
};
|
||||
tokens.push(await createEndorserJwtForDid(did, tokenPayload));
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract JWT from various URL formats
|
||||
* @param jwtUrlText The URL containing the JWT
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
||||
import { accessTokenForBackgroundNotifications } from "@/libs/crypto";
|
||||
import { mintBackgroundJwtTokenPool } from "@/libs/crypto";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||
@@ -63,7 +63,8 @@ export async function configureNativeFetcherIfReady(
|
||||
: DEFAULT_ENDORSER_API_SERVER;
|
||||
}
|
||||
|
||||
const jwtToken = await accessTokenForBackgroundNotifications(did);
|
||||
const jwtTokens = await mintBackgroundJwtTokenPool(did);
|
||||
const jwtToken = jwtTokens[0] ?? "";
|
||||
if (!jwtToken) {
|
||||
logger.warn(
|
||||
"[nativeFetcherConfig] No JWT for native fetcher; API-driven notifications may fail",
|
||||
@@ -82,9 +83,12 @@ export async function configureNativeFetcherIfReady(
|
||||
apiBaseUrl?.trim().replace(/\/$/, "") ?? DEFAULT_ENDORSER_API_SERVER,
|
||||
activeDid: did,
|
||||
jwtToken,
|
||||
jwtTokens,
|
||||
});
|
||||
logger.info(
|
||||
"[nativeFetcherConfig] Native fetcher configured for API-driven notifications",
|
||||
"[nativeFetcherConfig] Native fetcher configured (JWT pool size=" +
|
||||
jwtTokens.length +
|
||||
")",
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user