/** * ─────────────────────────────────────────────────────────────────────────────────────────────────┐ * Scheduled job that (a) keeps the OAuth refresh token alive under the Connected App's Idle Refresh * Token TTL and (b) acts as the ONLY proactive liveness probe for the connection. * * It delegates to TokenService — the single serialized, rotation-safe authority — which handles the row * lock, single-flight, and fail-loud persistence of the rotated pair. This class performs no callout of * its own, no token DML of its own, and NEVER wipes a token. * * ── History, because this was wrong twice and the second fix caused the current outage ────────────── * v1 wiped both tokens on a bare invalid_grant using a stale (unlocked) read. Under mandatory Refresh * Token Rotation that routinely blanked a token another path had just rotated. Correctly removed. * v2 then stopped rotating ENTIRELY, on the stated grounds that "the middleware is the token-rotation * authority ... if this scheduled job ALSO rotated it would issue a new token the middleware doesn't * know about". THAT REASONING IS VOID: this package is 100% Salesforce-native — there is no middleware * and no external server, so there was never a second holder to desynchronise from. (Confirmed * structurally: the only outbound token push in the repo, salesforceAuthenticationController's * '/api/auth/callback', is unreachable — one call site is gated on isSiteSupportedOrg()==false, which * is true for every EE/UE/Developer org, and the other's sole caller is a test.) * * v3 keeps v2's behaviour (no rotation) but for the CORRECT reason, documented in refreshNow below: * rotation cannot be done "just to check", because consuming the token is the only way to test it, and * every consumption risks losing it. The Idle TTL that a keep-alive would protect against cannot * expire on an org that is actually syncing. So not rotating here is right — the previous justification * was simply wrong, and that mattered because a wrong reason invites the wrong fix later. * ────────────────────────────────────────────────────────────────────────────────────────────────── */ global with sharing class RefreshTokenKeepAlive implements Schedulable { public static final String JOB_NAME = 'Hic_ShopifySync_Refresh_Token_Rotation'; // Unchanged (14th and 28th). This job performs no callout, so its frequency does not matter much — // do NOT increase it in the belief that it keeps the token alive. It does not rotate; see refreshNow. public static final String CRON_EXPR = '0 0 23 14,28 * ?'; global void execute(SchedulableContext sc) { refreshNow(); } public static void refreshNow() { try { List cfg = [ SELECT HIC_ShopifySync__Refresh_Token__c FROM HIC_ShopifySync__Configuration__c WHERE Name = 'Default' LIMIT 1 ]; // Not connected at all — nothing to keep alive. Do not attempt a refresh and do not notify. if (cfg.isEmpty() || String.isBlank(cfg[0].HIC_ShopifySync__Refresh_Token__c)) { return; } // DELIBERATELY DOES NOT ROTATE. // // A refresh token cannot be tested without being consumed, so any "probe" IS a rotation — // and under mandatory Refresh Token Rotation every rotation is a chance to lose the token // (see TokenService's lost-response window: if the callout fails after Salesforce has already // rotated, the org can never know which token is live). A scheduled rotation therefore ADDS // risk rather than removing it. // // The only thing it would buy is protection against the Connected App's Idle Refresh Token // TTL — and that clock cannot run out on a working org: ordinary sync traffic exercises the // refresh token every time the access token needs replacing. An org quiet enough to hit a // 30-day idle limit is an org that is not syncing anyway. // // So rotation happens ONLY on demand, only via TokenService, and only when something actually // needs a new access token. This job stays a cheap existence check. } catch (Exception e) { System.debug(LoggingLevel.ERROR, 'RefreshTokenKeepAlive probe failed: ' + e.getMessage()); } } }