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 android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonArray;
|
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 CONNECT_TIMEOUT_MS = 10000;
|
||||||
private static final int READ_TIMEOUT_MS = 15000;
|
private static final int READ_TIMEOUT_MS = 15000;
|
||||||
private static final int MAX_RETRIES = 3;
|
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;
|
private static final int RETRY_DELAY_MS = 1000;
|
||||||
|
|
||||||
// Must match plugin's SharedPreferences name and keys (DailyNotificationPlugin / TimeSafariIntegrationManager)
|
// 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 apiBaseUrl;
|
||||||
private volatile String activeDid;
|
private volatile String activeDid;
|
||||||
private volatile String jwtToken;
|
private volatile String jwtToken;
|
||||||
|
/** Distinct JWTs from configureNativeFetcher `jwtTokens`; null = use jwtToken only. */
|
||||||
|
@Nullable
|
||||||
|
private List<String> jwtTokenPool;
|
||||||
|
|
||||||
public TimeSafariNativeFetcher(Context context) {
|
public TimeSafariNativeFetcher(Context context) {
|
||||||
this.appContext = context.getApplicationContext();
|
this.appContext = context.getApplicationContext();
|
||||||
@@ -64,11 +70,48 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure(String apiBaseUrl, String activeDid, String jwtToken) {
|
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.apiBaseUrl = apiBaseUrl;
|
||||||
this.activeDid = activeDid;
|
this.activeDid = activeDid;
|
||||||
this.jwtToken = jwtToken;
|
this.jwtToken = jwtToken;
|
||||||
|
this.jwtTokenPool =
|
||||||
|
jwtTokenPool != null && !jwtTokenPool.isEmpty()
|
||||||
|
? new ArrayList<>(jwtTokenPool)
|
||||||
|
: null;
|
||||||
int starredCount = getStarredPlanIds().size();
|
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
|
@NonNull
|
||||||
@@ -92,7 +135,8 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
|||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try {
|
try {
|
||||||
Log.i(TAG, "fetchContent worker thread=" + Thread.currentThread().getName());
|
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.");
|
Log.e(TAG, "Not configured. Call configureNativeFetcher() from TypeScript first.");
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
@@ -104,7 +148,7 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
|||||||
connection.setReadTimeout(READ_TIMEOUT_MS);
|
connection.setReadTimeout(READ_TIMEOUT_MS);
|
||||||
connection.setRequestMethod("POST");
|
connection.setRequestMethod("POST");
|
||||||
connection.setRequestProperty("Content-Type", "application/json");
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
connection.setRequestProperty("Authorization", "Bearer " + jwtToken);
|
connection.setRequestProperty("Authorization", "Bearer " + bearer);
|
||||||
connection.setDoOutput(true);
|
connection.setDoOutput(true);
|
||||||
|
|
||||||
Map<String, Object> requestBody = new HashMap<>();
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
@@ -143,6 +187,16 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
String responseBody = response.toString();
|
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);
|
List<NotificationContent> contents = parseApiResponse(responseBody, context);
|
||||||
if (!contents.isEmpty()) {
|
if (!contents.isEmpty()) {
|
||||||
updateLastAckedJwtIdFromResponse(responseBody);
|
updateLastAckedJwtIdFromResponse(responseBody);
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* JWT lifetime for native New Activity background prefetch (`configureNativeFetcher`).
|
* 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.
|
* See doc/plan-background-jwt-pool-and-expiry.md. Confirm max `exp` with Endorser before raising.
|
||||||
* Confirm max `exp` with Endorser before raising.
|
|
||||||
*/
|
*/
|
||||||
export const BACKGROUND_JWT_EXPIRY_DAYS = 90;
|
export const BACKGROUND_JWT_EXPIRY_DAYS = 90;
|
||||||
|
|
||||||
export const BACKGROUND_JWT_EXPIRY_SECONDS =
|
export const BACKGROUND_JWT_EXPIRY_SECONDS =
|
||||||
BACKGROUND_JWT_EXPIRY_DAYS * 24 * 60 * 60;
|
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 { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
|
||||||
import { HDNode } from "@ethersproject/hdnode";
|
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 {
|
import {
|
||||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
createEndorserJwtForDid,
|
createEndorserJwtForDid,
|
||||||
@@ -122,6 +125,28 @@ export const accessTokenForBackgroundNotifications = async (
|
|||||||
return createEndorserJwtForDid(did, tokenPayload);
|
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
|
* Extract JWT from various URL formats
|
||||||
* @param jwtUrlText The URL containing the JWT
|
* @param jwtUrlText The URL containing the JWT
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
||||||
import { accessTokenForBackgroundNotifications } from "@/libs/crypto";
|
import { mintBackgroundJwtTokenPool } from "@/libs/crypto";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||||
@@ -63,7 +63,8 @@ export async function configureNativeFetcherIfReady(
|
|||||||
: DEFAULT_ENDORSER_API_SERVER;
|
: DEFAULT_ENDORSER_API_SERVER;
|
||||||
}
|
}
|
||||||
|
|
||||||
const jwtToken = await accessTokenForBackgroundNotifications(did);
|
const jwtTokens = await mintBackgroundJwtTokenPool(did);
|
||||||
|
const jwtToken = jwtTokens[0] ?? "";
|
||||||
if (!jwtToken) {
|
if (!jwtToken) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"[nativeFetcherConfig] No JWT for native fetcher; API-driven notifications may fail",
|
"[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,
|
apiBaseUrl?.trim().replace(/\/$/, "") ?? DEFAULT_ENDORSER_API_SERVER,
|
||||||
activeDid: did,
|
activeDid: did,
|
||||||
jwtToken,
|
jwtToken,
|
||||||
|
jwtTokens,
|
||||||
});
|
});
|
||||||
logger.info(
|
logger.info(
|
||||||
"[nativeFetcherConfig] Native fetcher configured for API-driven notifications",
|
"[nativeFetcherConfig] Native fetcher configured (JWT pool size=" +
|
||||||
|
jwtTokens.length +
|
||||||
|
")",
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user