# Integration Point Refactor Analysis **Author**: Matthew Raymer **Date**: 2025-10-29 **Status**: 🎯 **ANALYSIS** - Architectural refactoring proposal ## Objective Refactor the Daily Notification Plugin architecture so that **TimeSafari-specific integration logic is implemented by the Capacitor host app** rather than hardcoded in the plugin. This makes the plugin generic and reusable for other applications. ## Current Architecture ### Plugin-Side Integration (Current State) The plugin currently contains TimeSafari-specific code: 1. **`TimeSafariIntegrationManager.java`** - Manages API server URL and DID lifecycle - Coordinates fetching and scheduling - Converts TimeSafari data to `NotificationContent` 2. **`EnhancedDailyNotificationFetcher.java`** - Makes HTTP calls to TimeSafari API endpoints: - `POST /api/v2/offers/person` - `POST /api/v2/offers/plans` - `POST /api/v2/report/plansLastUpdatedBetween` - Handles JWT authentication - Parses TimeSafari response formats 3. **`DailyNotificationJWTManager.java`** - Generates JWT tokens for TimeSafari API authentication - Manages JWT expiration and refresh 4. **TimeSafari Data Models** - Plugin imports `PlanSummary`, `OffersResponse`, `TimeSafariNotificationBundle` - Converts TimeSafari data structures to `NotificationContent[]` ### Current Data Flow ``` Host App (Capacitor) ↓ configure() with activeDidIntegration config Plugin (DailyNotificationPlugin) ↓ initializes TimeSafariIntegrationManager TimeSafariIntegrationManager ↓ creates EnhancedDailyNotificationFetcher EnhancedDailyNotificationFetcher ↓ makes HTTP calls with JWT TimeSafari API Server ↓ returns TimeSafariNotificationBundle EnhancedDailyNotificationFetcher ↓ converts to NotificationContent[] TimeSafariIntegrationManager ↓ saves & schedules Plugin Storage & Scheduler ``` ## Proposed Architecture ### App-Side Integration (Target State) The plugin becomes a **generic notification scheduler** that accepts a **content fetcher callback** from the host app: 1. **Plugin exposes interface** for content fetching 2. **Host app implements** the TimeSafari API integration 3. **Plugin calls app's implementation** when fetching is needed 4. **Plugin remains generic** - no TimeSafari-specific code ### Proposed Data Flow ``` Host App (Capacitor) ↓ implements ContentFetcher interface ↓ provides fetcher callback to plugin Plugin (DailyNotificationPlugin) ↓ calls app's fetcher callback Host App's TimeSafari Integration ↓ makes HTTP calls, handles JWT, parses responses TimeSafari API Server ↓ returns TimeSafariNotificationBundle Host App's Integration ↓ converts to NotificationContent[] ↓ returns to plugin Plugin ↓ saves & schedules (no TimeSafari knowledge) Plugin Storage & Scheduler ``` ## What Needs to Change ### 1. Plugin Interface Changes #### New TypeScript Interface ```typescript /** * Content fetcher callback that host app must implement */ export interface NotificationContentFetcher { /** * Fetch notification content from external source * * This is called by the plugin when: * - Background fetch work is triggered * - Prefetch is scheduled before notification time * - Manual refresh is requested * * @param context Context about why fetch was triggered * @returns Promise with notification content array */ fetchContent(context: FetchContext): Promise; } export interface FetchContext { /** * Why the fetch was triggered */ trigger: 'background_work' | 'prefetch' | 'manual' | 'scheduled'; /** * Scheduled notification time (millis since epoch) */ scheduledTime?: number; /** * When fetch was triggered (millis since epoch) */ fetchTime: number; /** * Any additional context from plugin */ metadata?: Record; } ``` #### New Plugin Method ```typescript export interface DailyNotificationPlugin { // ... existing methods ... /** * Register content fetcher callback from host app * * @param fetcher Implementation provided by host app */ setContentFetcher(fetcher: NotificationContentFetcher): void; } ``` ### 2. Android Plugin Changes #### New Java Interface ```java /** * Content fetcher interface for host app to implement */ public interface NotificationContentFetcher { /** * Fetch notification content from external source * * Called by plugin when background fetch is needed. * Host app implements TimeSafari API calls here. * * @param context Fetch context with trigger info * @return Future with list of notification content */ CompletableFuture> fetchContent( FetchContext context ); } public class FetchContext { public final String trigger; // "background_work", "prefetch", "manual", "scheduled" public final Long scheduledTime; // Optional: when notification is scheduled public final long fetchTime; // When fetch was triggered public final Map metadata; // Additional context // Constructor, getters... } ``` #### Plugin Implementation ```java public class DailyNotificationPlugin extends Plugin { private @Nullable NotificationContentFetcher contentFetcher; @PluginMethod public void setContentFetcher(PluginCall call) { // Receive callback reference from JavaScript // Store for use in background workers // Implementation depends on how to bridge JS -> Java callback } // In DailyNotificationFetchWorker: private NotificationContent fetchContentWithTimeout() { if (contentFetcher == null) { // Fallback: return null, use cached content return null; } FetchContext context = new FetchContext( "background_work", scheduledTime, System.currentTimeMillis(), metadata ); try { List results = contentFetcher.fetchContent(context).get(30, TimeUnit.SECONDS); return results.isEmpty() ? null : results.get(0); } catch (Exception e) { // Handle timeout, errors return null; } } } ``` ### 3. What Gets Removed from Plugin #### Delete These Classes: - `TimeSafariIntegrationManager.java` - Move logic to host app - `EnhancedDailyNotificationFetcher.java` - Move to host app - TimeSafari-specific data model imports #### Keep These (Generic): - `DailyNotificationStorage` - Generic storage - `DailyNotificationScheduler` - Generic scheduling - `DailyNotificationPlugin` - Core plugin (modified) - `DailyNotificationFetchWorker` - Modified to call callback - `NotificationContent` - Generic data model ### 4. Host App Implementation #### TypeScript Implementation ```typescript import { DailyNotification, NotificationContentFetcher, FetchContext } from '@timesafari/daily-notification-plugin'; import { TimeSafariAPI } from './timesafari-api'; // Host app's API client class TimeSafariContentFetcher implements NotificationContentFetcher { constructor( private api: TimeSafariAPI, private activeDid: string ) {} async fetchContent(context: FetchContext): Promise { // 1. Generate JWT for authentication const jwt = await this.api.generateJWT(this.activeDid); // 2. Fetch TimeSafari data const [offersToPerson, offersToProjects, projectUpdates] = await Promise.all([ this.api.fetchOffersToPerson(jwt), this.api.fetchOffersToProjects(jwt), this.api.fetchProjectsLastUpdated(this.starredPlanIds, jwt) ]); // 3. Convert TimeSafari data to NotificationContent[] const contents: NotificationContent[] = []; // Convert offers to person if (offersToPerson?.data) { for (const offer of offersToPerson.data) { contents.push({ id: `offer_${offer.id}`, title: `New Offer: ${offer.title}`, body: offer.description || '', scheduledTime: context.scheduledTime || Date.now() + 3600000, fetchTime: context.fetchTime, mediaUrl: offer.imageUrl }); } } // Convert project updates if (projectUpdates?.data) { for (const update of projectUpdates.data) { contents.push({ id: `project_${update.planSummary.jwtId}`, title: `${update.planSummary.name} Updated`, body: `New updates for ${update.planSummary.name}`, scheduledTime: context.scheduledTime || Date.now() + 3600000, fetchTime: context.fetchTime, mediaUrl: update.planSummary.image }); } } return contents; } } // In host app initialization: const plugin = new DailyNotification(); const fetcher = new TimeSafariContentFetcher(apiClient, activeDid); plugin.setContentFetcher(fetcher); ``` #### Android Implementation (if needed) If you need native Android implementation (for WorkManager background tasks): ```kotlin // In host app's native code class TimeSafariContentFetcher : NotificationContentFetcher { private val apiClient: TimeSafariAPIClient private val activeDid: String override fun fetchContent(context: FetchContext): CompletableFuture> { return CompletableFuture.supplyAsync { // Make TimeSafari API calls // Convert to NotificationContent[] // Return list } } } ``` ## Implementation Challenges ### Challenge 1: JavaScript β†’ Java Callback Bridge **Problem**: Capacitor plugins need to bridge JavaScript callbacks to Java for background workers. **Solutions**: 1. **Bridge via Plugin Method** (Simplest) - Host app calls plugin method with callback ID - Plugin stores callback reference - Plugin calls back via Capacitor bridge when needed - Limitations: Callback must be serializable 2. **Bridge via Event System** (More Flexible) - Plugin emits events when fetch needed - Host app listens and responds - Plugin waits for response - More complex but more flexible 3. **Native Implementation** (Best Performance) - Host app provides native Java/Kotlin implementation - Direct Java interface (no JS bridge needed) - Best for background work - Requires host app to include native code **Recommendation**: Start with Option 1 (Plugin Method), add Option 3 (Native) later if needed. ### Challenge 2: Background Worker Execution **Problem**: Background WorkManager workers need to call host app's fetcher, but WorkManager runs in separate process. **Solutions**: 1. **Store callback reference in SharedPreferences** (Limited) - Serialize callback config, not actual callback - Host app provides API endpoint URL + auth info - Plugin makes HTTP call to host app's endpoint - Host app handles TimeSafari logic server-side 2. **Use Capacitor Background Tasks** (Platform-specific) - iOS: BGTaskScheduler - Android: WorkManager with Capacitor bridge - Bridge JS callback from background task - Complex but works 3. **Native Background Implementation** (Recommended) - Host app provides native Java/Kotlin fetcher - No JS bridge needed in background - Direct Java interface - Best performance **Recommendation**: Option 3 (Native) for background work, Option 1 for foreground. ### Challenge 3: Data Model Conversion **Problem**: Plugin's generic `NotificationContent` vs TimeSafari's specific data structures. **Solution**: - Host app owns conversion logic - Plugin never sees TimeSafari data structures - Plugin only knows about `NotificationContent[]` This is actually **simpler** than current architecture. ### Challenge 4: Configuration Management **Problem**: Plugin currently manages TimeSafari config (apiServer, activeDid, starredPlanIds). **Solution**: - Host app manages all TimeSafari config - Plugin receives only fetcher callback - Host app provides config to fetcher, not plugin - Plugin becomes stateless regarding TimeSafari ## Migration Path ### Phase 1: Add Callback Interface (Non-Breaking) 1. Add `setContentFetcher()` method to plugin 2. Add callback interface definitions 3. Keep existing TimeSafari integration as fallback 4. Allow host app to opt-in to callback approach ### Phase 2: Move TimeSafari Code to Host App 1. Create `TimeSafariContentFetcher` in host app 2. Migrate `EnhancedDailyNotificationFetcher` logic 3. Migrate JWT generation logic 4. Test with callback approach ### Phase 3: Remove Plugin Integration (Breaking) 1. Remove `TimeSafariIntegrationManager` from plugin 2. Remove `EnhancedDailyNotificationFetcher` from plugin 3. Remove TimeSafari data model imports 4. Make callback approach required 5. Update documentation ## Effort Estimation ### Plugin Changes - **Add callback interface**: 4-8 hours - **Modify fetch worker**: 4-8 hours - **Update documentation**: 2-4 hours - **Testing**: 8-16 hours - **Total**: 18-36 hours ### Host App Changes - **Create TimeSafariContentFetcher**: 8-16 hours - **Migrate API client logic**: 8-16 hours - **Migrate JWT logic**: 4-8 hours - **Testing**: 8-16 hours - **Total**: 28-56 hours ### Total Effort **Estimated**: 46-92 hours (6-12 days) for complete migration ## Benefits 1. **Generic Plugin**: Plugin can be used by any app, not just TimeSafari 2. **Clear Separation**: Plugin handles scheduling, app handles fetching 3. **Easier Testing**: Mock fetcher for unit tests 4. **Better Maintainability**: TimeSafari changes don't require plugin updates 5. **Flexibility**: Host app controls all API integration details ## Risks 1. **Breaking Changes**: Requires migration of host app 2. **Complexity**: Callback bridge adds complexity 3. **Performance**: JavaScript bridge may be slower for background work 4. **Testing Burden**: More complex integration testing ## Recommendations ### Short Term (Keep Current Architecture) **Recommendation**: Keep current architecture for now, add callback interface as optional enhancement. **Rationale**: - Current architecture works - Migration is significant effort - Benefits may not justify costs yet - Can add callback interface without removing existing code ### Long Term (Full Refactor) **Recommendation**: Plan for full refactor when: - Plugin needs to support multiple apps - TimeSafari API changes frequently - Plugin team wants to decouple from TimeSafari **Approach**: 1. Phase 1: Add callback interface alongside existing code 2. Migrate TimeSafari app to use callbacks 3. Phase 3: Remove old integration code ## Alternative: Hybrid Approach Keep TimeSafari integration in plugin but make it **optional and pluggable**: ```typescript interface DailyNotificationPlugin { // Current approach (TimeSafari-specific) configure(options: ConfigureOptions): void; // New approach (generic callback) setContentFetcher(fetcher: NotificationContentFetcher): void; // Plugin uses callback if provided, falls back to TimeSafari integration } ``` This allows: - Existing apps continue working (no breaking changes) - New apps can use callback approach - Plugin gradually becomes generic ## Questions to Resolve 1. **Background Work**: How to call JS callback from background WorkManager? - Native implementation required? - Or HTTP callback to host app endpoint? 2. **Error Handling**: How does plugin handle fetcher errors? - Retry logic? - Fallback to cached content? - Event notifications? 3. **Configuration**: Where does TimeSafari config live? - Host app only? - Or plugin still needs some config? 4. **Testing**: How to test plugin without TimeSafari integration? - Mock fetcher interface? - Test fixtures? --- ## Improvement Directive Integration This analysis has been superseded by the **Improvement Directive β€” Daily Notification Plugin "Integration Point" Refactor** (see below). The directive provides concrete, implementable specifications with clear interfaces, test contracts, and migration paths. ### Key Directive Decisions 1. **Dual-Path SPI**: Native Fetcher (required for background) + Optional JS Fetcher (foreground only) 2. **Background Reliability**: Native SPI only; JS fetcher explicitly disabled in background workers 3. **Reversibility**: Legacy TimeSafari code behind feature flag for one minor release 4. **Test Contract**: Single JSON fixture contract used by both native and JS paths See full directive below for complete specifications. --- ## Improvement Directive β€” Daily Notification Plugin "Integration Point" Refactor **Audience:** Plugin & TimeSafari app teams **Goal:** Convert the analysis into an implementable, low-risk refactor with crisp interfaces, background-compatibility, testability, and a reversible migration path. **Source reviewed:** `docs/integration-point-refactor-analysis.md` --- ## A) Decision Record (ADR-001): Architecture Shape **Decision:** Ship a **dual-path SPI** where the plugin exposes: 1. a **Native Fetcher SPI** (Android/Kotlin, iOS/Swift) for background reliability, and 2. an **Optional JS Fetcher** for foreground/manual refresh & prototyping. **Why:** Your analysis correctly targets decoupling, but the **WorkManager/BGTaskScheduler reality** means **native fetchers are the only dependable background path**. The JS callback remains useful for manual refresh, prefetch while the app is foregrounded, or rapid iteration. **Reversibility:** Keep the legacy TimeSafari path behind a feature flag for one minor release; remove in the next major. **Exit Criteria:** Both fetcher paths produce identical `NotificationContent[]` and pass the same test contract. --- ## B) Public Interfaces (Final Form) ### B1. TypeScript (Host App) ```typescript // core types export interface NotificationContent { id: string; title: string; body?: string; scheduledTime?: number; // epoch ms fetchTime: number; // epoch ms mediaUrl?: string; ttlSeconds?: number; // cache validity dedupeKey?: string; // for idempotency priority?: 'min'|'low'|'default'|'high'|'max'; metadata?: Record; } export type FetchTrigger = 'background_work' | 'prefetch' | 'manual' | 'scheduled'; export interface FetchContext { trigger: FetchTrigger; scheduledTime?: number; fetchTime: number; metadata?: Record; } export interface JsNotificationContentFetcher { fetchContent(context: FetchContext): Promise; } ``` **Plugin API Additions** ```typescript export interface DailyNotificationPlugin { setJsContentFetcher(fetcher: JsNotificationContentFetcher): void; // foreground-only enableNativeFetcher(enable: boolean): Promise; // default true setPolicy(policy: SchedulingPolicy): Promise; // see Β§C2 } ``` ### B2. Android SPI (Host App native) ```kotlin interface NativeNotificationContentFetcher { suspend fun fetchContent(context: FetchContext): List } data class FetchContext( val trigger: String, // enum mirror val scheduledTime: Long?, // epoch ms val fetchTime: Long, // epoch ms val metadata: Map = emptyMap() ) ``` **Registration:** Host app injects an implementation via a generated ServiceLoader or explicit setter in `Application.onCreate()`: ```kotlin DailyNotification.setNativeFetcher(TimeSafariNativeFetcher()) ``` > **Note:** JS fetcher remains available but **is not used** by background workers. --- ## C) Scheduling & Policy Guardrails ### C1. Background Reliability * **Android:** WorkManager + `setRequiredNetworkType(CONNECTED)`, exponential backoff, content URI triggers optional, **no JS bridging** in background. * **iOS:** `BGAppRefreshTask` + `BGProcessingTask` with earliestBeginDate windows; keep tasks short (< 30s), aggregate fetches. ### C2. `SchedulingPolicy` ```typescript export interface SchedulingPolicy { prefetchWindowMs?: number; // how early to prefetch before schedule retryBackoff: { minMs: number; maxMs: number; factor: number; jitterPct: number }; maxBatchSize?: number; // upper bound per run dedupeHorizonMs?: number; // window for dedupeKey suppression cacheTtlSeconds?: number; // default if item.ttlSeconds absent exactAlarmsAllowed?: boolean; // guarded by OS version & battery policy } ``` ### C3. Idempotency & De-duplication * The plugin maintains a **recent keys bloom/filter** using `dedupeKey` (fallback `id`) for `dedupeHorizonMs`. * No notification is enqueued if `(dedupeKey, title, body)` unchanged within horizon. --- ## D) Security & Config Partition ### D1. Secrets & Tokens * **JWT generation lives in the host app**, not the plugin. For extra hardening consider **DPoP** or **JWE-wrapped access tokens** if your API supports it. * Rotate keys; keep key refs in host app secure storage; never serialize tokens through JS bridges in background. ### D2. Least-Knowledge Plugin * Plugin stores only generic `NotificationContent`. * **No TimeSafari DTOs** in plugin artifacts. (As your analysis recommends.) --- ## E) Failure Taxonomy & Handling | Class | Examples | Plugin Behavior | | --------------- | ---------------------- | ------------------------------------------------- | | **Retryable** | 5xx, network, timeouts | Backoff per policy; stop on `maxAttempts` | | **Unretryable** | 4xx auth, schema error | Log & surface event; skip batch; keep schedule | | **Partial** | Some items bad | Enqueue valid subset; metric + structured error | | **Over-quota** | 429 | Honor `Retry-After` if present; otherwise backoff | **Cache fallback:** If fetch fails, reuse **unexpired cached contents** (by item `ttlSeconds` or default policy). --- ## F) Metrics, Logs, Events * **Metrics:** `fetch_duration_ms`, `fetch_success`, `fetch_fail_class`, `items_fetched`, `items_enqueued`, `deduped_count`, `cache_hits`. * **Structured logs:** Correlate by `run_id`, include `trigger`, backoff stage, and per-item `dedupeKey`. * **Event bus:** Emit `onFetchStart`, `onFetchEnd`, `onFetchError`, `onEnqueue` (debug UI can subscribe). --- ## G) Test Contract & Fixtures ### G1. Golden Contract A **single JSON fixture contract** used by both native & JS fetchers: ```json { "context": {"trigger":"scheduled","scheduledTime":1730179200000,"fetchTime":1730175600000}, "output": [ {"id":"offer_123","title":"New Offer","body":"...","fetchTime":1730175600000,"dedupeKey":"offer_123","ttlSeconds":86400} ] } ``` * CI runs the same contract through **(1) JS fetcher** (foreground test harness) and **(2) Native fetcher** (instrumented test) and compares normalized outputs. ### G2. Failure Sims * Network chaos (drop, latency), 401β†’refreshβ†’200, 429 with `Retry-After`, malformed item (should be skipped, not crash). --- ## H) Migration Plan (Minor β†’ Major) **Minor (X.Y):** 1. Add `setJsContentFetcher`, `enableNativeFetcher`, `setPolicy`. 2. Implement **Native Fetcher SPI** & plugin side registry. 3. Keep legacy TimeSafari code behind `useLegacyIntegration` flag (default **false**). **Major (X+1.0):** 1. Remove legacy TimeSafari classes (`TimeSafariIntegrationManager`, `EnhancedDailyNotificationFetcher`, etc.). 2. Publish migration guide & codemod mapping old config β†’ SPI registration. --- ## I) Pull-Request Breakdown (7 PRs) 1. **PR1 β€” SPI Shell:** Kotlin/Swift interfaces, TS types, `SchedulingPolicy`, no behavior change. 2. **PR2 β€” Background Workers:** Wire Native SPI in WorkManager/BGTasks; metrics skeleton; cache store. 3. **PR3 β€” JS Fetcher Path:** Foreground bridge, event bus, dev harness screen. 4. **PR4 β€” Dedupe/Idempotency:** Bloom/filter store + tests; `dedupeKey` enforcement. 5. **PR5 β€” Failure Policy:** Retry taxonomy, backoff, cache fallback; structured errors. 6. **PR6 β€” Docs & Samples:** Minimal host app examples (JS + Kotlin), migration notes. 7. **PR7 β€” Feature Flag Legacy:** Guard & regression tests; changelog for major removal. --- ## J) Acceptance Criteria (Definition of Done) * **Background Reliability:** Native fetcher runs with app killed; JS fetcher **never** required for background. * **Parity:** Same inputs β†’ same `NotificationContent[]` via JS & Native paths (contract tests green). * **No TimeSafari in Plugin:** No imports, types, or endpoints leak into plugin modules. * **Safety:** No notifications emitted twice within `dedupeHorizonMs` for identical `dedupeKey`. * **Observability:** Metrics visible; error classes distinguish retryable vs. not; logs correlated by run. * **Docs:** Host app integration guide includes **copy-paste** Kotlin & TS skeletons; migration guide from legacy. * **Battery & Quotas:** Backoff respected; tasks kept <30s; policy toggles documented; exact alarms gated. --- ## K) Open Questions (Resolve Before PR3) 1. **iOS capabilities:** Which BG task identifiers and permitted intervals will Product accept (refresh vs processing)? 2. **Token Hardening:** Do we want DPoP or short-lived JWE now or later? 3. **User Controls:** Should we surface a per-plan "snooze/mute" map inside `metadata` with plugin-side persistence? --- ## L) Example Skeletons ### L1. Android Native Fetcher (Host App) ```kotlin class TimeSafariNativeFetcher( private val api: TimeSafariApi, private val tokenProvider: TokenProvider ) : NativeNotificationContentFetcher { override suspend fun fetchContent(context: FetchContext): List { val jwt = tokenProvider.freshToken() val updates = api.updates(jwt) // domain-specific val offers = api.offers(jwt) return buildList { for (o in offers) add( NotificationContent( id = "offer_${o.id}", title = "New Offer: ${o.title}", body = o.description ?: "", scheduledTime = context.scheduledTime ?: (System.currentTimeMillis() + 3_600_000), fetchTime = context.fetchTime, mediaUrl = o.imageUrl, ttlSeconds = 86_400, dedupeKey = "offer_${o.id}" ) ) // … map updates similarly … } } } ``` ### L2. JS Fetcher Registration (Foreground Only) ```typescript const fetcher: JsNotificationContentFetcher = { async fetchContent(ctx) { const jwt = await api.generateJWT(activeDid); const { offers, updates } = await api.fetchEverything(jwt); return mapToNotificationContent(offers, updates, ctx); } }; DailyNotification.setJsContentFetcher(fetcher); await DailyNotification.setPolicy({ retryBackoff: { minMs: 2000, maxMs: 600000, factor: 2, jitterPct: 20 }, dedupeHorizonMs: 24*60*60*1000, cacheTtlSeconds: 6*60*60 }); ``` --- ## M) Docs to Produce * **INTEGRATION_GUIDE.md** (host app: Native vs JS, when to use which, copy-paste snippets). * **SCHEDULING_POLICY.md** (all knobs with OS caveats). * **MIGRATION_GUIDE_Xβ†’X+1.md** (flag removal plan, oldβ†’new mapping). * **TEST_CONTRACTS.md** (fixtures, CI commands). --- ## N) Risk Register & Mitigations * **Risk:** JS callback used in background β†’ fetch starves. **Mitigation:** Disable in background by design; lint rule + runtime guard. * **Risk:** Duplicate notifications. **Mitigation:** `dedupeKey` + horizon filter; contract tests to assert no dupes. * **Risk:** Token leakage through JS. **Mitigation:** Tokens stay in native path for background; JS path only in foreground; redact logs. * **Risk:** Major removal shocks dependents. **Mitigation:** Two-release migration window + codemod and samples. --- ### Final Note Your analysis nails the **separation of concerns** and enumerates the **callback bridge challenges**. This directive turns it into a concrete, staged plan with **native-first background fetch**, **JS optionality**, **clear policies**, and **verifiable acceptance criteria** while preserving a **rollback path**. --- **Status**: Improvement Directive - Active Implementation Plan **Next Steps**: Begin PR1 (SPI Shell) implementation **Dependencies**: None **Stakeholders**: Plugin developers, TimeSafari app developers