migrateLegacyKeySlots function
Future<List<String> >
migrateLegacyKeySlots(
- Iterable<
KeySlotMigrationSource> migrations, - SecureKeyCache keys
Boot-time migration of key slots stranded by a derivation change
(gh-1570 incident: fa 1.0.556 → 1.0.564 moved a roles entry's endpoint
spelling, and the key stored under the old slot stopped resolving).
For each known endpoint, when the canonical slot is EMPTY and a legacy
cross-spelling slot holds a value, the value is COPIED onto the
canonical slot — non-destructive (the legacy slot stays, so a
rollback keeps working) — and a notice line is returned for the boot
to print. Keychain explicitly in scope: keys.save writes through to
the OS store; an unavailable or failing store degrades to a notice
(resolution still falls back to the legacy slot), never a boot
failure. A canonical slot that already holds a value is never touched.
Implementation
Future<List<String>> migrateLegacyKeySlots(
Iterable<KeySlotMigrationSource> migrations,
SecureKeyCache keys,
) async {
final notices = <String>[];
if (!keys.available) return notices;
for (final (:baseUrl, providerName: name, pinnedKeyName: pinned)
in migrations) {
final canonical =
pinned ??
CustomProviderRegistry.keyNameFor(baseUrl, providerName: name);
if (keys.read(canonical) != null) continue;
for (final legacy in CustomProviderRegistry.keySlotCandidates(
baseUrl,
providerName: name,
)) {
if (legacy == canonical) continue;
final value = keys.read(legacy);
if (value == null) continue;
final copied = await keys.save(canonical, value);
notices.add(
copied
? 'migrated the API key for "$baseUrl" from legacy key slot '
'"$legacy" to "$canonical" (the legacy slot is kept)'
: 'could not migrate the API key for "$baseUrl" from "$legacy" '
'to "$canonical" '
'(${keys.lastSaveError ?? 'secure store refused the write'}) '
'— resolution still falls back to the legacy slot',
);
break; // one stranded source per canonical slot
}
}
return notices;
}