feat(notifications): mint long-lived JWT for native New Activity prefetch (Phase A)

Add BACKGROUND_JWT_EXPIRY_DAYS/SECONDS and accessTokenForBackgroundNotifications
via createEndorserJwtForDid; configureNativeFetcher uses it instead of getHeaders
so WorkManager prefetch is not stuck with a 60s access token. Interactive API
calls unchanged.
This commit is contained in:
Jose Olarte III
2026-03-27 21:33:46 +08:00
parent c9ea2e4120
commit 9f44a53047
4 changed files with 31 additions and 9 deletions

4
package-lock.json generated
View File

@@ -8685,8 +8685,8 @@
}
},
"node_modules/@timesafari/daily-notification-plugin": {
"version": "2.1.5",
"resolved": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#469167a55fbebb91b3e61d6c8b3aec6fc873a13c",
"version": "2.2.0",
"resolved": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#9121b1e0f7e1be50d00eb3e78d52e06816196697",
"license": "MIT",
"workspaces": [
"packages/*"

View File

@@ -0,0 +1,9 @@
/**
* 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.
*/
export const BACKGROUND_JWT_EXPIRY_DAYS = 90;
export const BACKGROUND_JWT_EXPIRY_SECONDS =
BACKGROUND_JWT_EXPIRY_DAYS * 24 * 60 * 60;

View File

@@ -4,6 +4,7 @@ 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 {
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
createEndorserJwtForDid,
@@ -104,6 +105,23 @@ export const accessToken = async (did?: string) => {
}
};
/**
* JWT for native New Activity prefetch (`configureNativeFetcher` / WorkManager).
* Uses a long `exp` (`BACKGROUND_JWT_EXPIRY_SECONDS`); do not use for ordinary
* in-app API calls — use `getHeaders` / `accessToken` instead.
*/
export const accessTokenForBackgroundNotifications = async (
did?: string,
): Promise<string> => {
if (!did) {
return "";
}
const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + BACKGROUND_JWT_EXPIRY_SECONDS;
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
return createEndorserJwtForDid(did, tokenPayload);
};
/**
* Extract JWT from various URL formats
* @param jwtUrlText The URL containing the JWT

View File

@@ -8,7 +8,7 @@
import { Capacitor } from "@capacitor/core";
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
import { getHeaders } from "@/libs/endorserServer";
import { accessTokenForBackgroundNotifications } from "@/libs/crypto";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { logger } from "@/utils/logger";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
@@ -63,12 +63,7 @@ export async function configureNativeFetcherIfReady(
: DEFAULT_ENDORSER_API_SERVER;
}
const headers = await getHeaders(did);
const auth = headers?.Authorization;
const jwtToken =
typeof auth === "string" && auth.startsWith("Bearer ")
? auth.slice(7)
: "";
const jwtToken = await accessTokenForBackgroundNotifications(did);
if (!jwtToken) {
logger.warn(
"[nativeFetcherConfig] No JWT for native fetcher; API-driven notifications may fail",