Files
webmail/lib/shared-store/import/data.ts
T
Linus Rath 8523289a4b feat: let an admin wipe a lost device, and clear an account the server refuses for good
Until now a lost laptop kept its offline mail for as long as anyone could
open it, and an account whose login the server had ended kept its copy too.

- `lib/devices`, `app/api/devices`: browsers with offline on and desktop
  apps register, in a durable collection with no expiry. A check-in goes
  by a per-device secret, so a revoked device still hears its wipe.
- `app/api/admin/devices` and the org devices page: wipe one device or all
  of a user's, with audit entries for each request and each confirmation.
- `lib/offline/device-wipe.ts`: a start finishes any wipe cut short, then
  waits up to a second for the instance before showing the local copy. A
  wipe takes the store, keys, plugin databases and handoffs, signs out,
  and reports back, again at the next contact if the report failed.
- `lib/jmap/client.ts`, `stores/auth-store.ts`: only `invalid_grant`, the
  session limit, or a 401 lasting past a re-auth count as refused. A 500,
  a proxy page or an outage never do.
- Settings: "Remove everything from this device", with a confirmation.

`lib/shared-store/__tests__/import-data.test.ts` changes its assertion
because it lists every collection the import copies, and `devices` is new.
2026-11-03 13:19:44 +01:00

45 lines
1.9 KiB
TypeScript

import { getStore } from '@/lib/shared-store';
import { localCollection } from '@/lib/shared-store/local';
import type { CollectionOptions } from '@/lib/shared-store/types';
import type { ImportStep } from '@/lib/shared-store/import';
import { SETTINGS_COLLECTION } from '@/lib/settings-sync';
import { VAULT_COLLECTION } from '@/lib/plugin-secrets/vault';
import { ARCHIVE_COLLECTION } from '@/lib/account-archive-storage';
import { TELEMETRY_COLLECTION } from '@/lib/telemetry/state';
import { VERSION_CHECK_COLLECTION } from '@/lib/version-check/state';
import { DEVICES_COLLECTION } from '@/lib/devices/registry';
/**
* Part `data` of the shared-store import (#1066): every document of these
* collections, read from the local files exactly as local mode reads them,
* written to the shared store only where it holds nothing yet. The bytes go
* over as they are: settings and plugin credentials stay encrypted with the
* session secret, which the boot imports first.
*/
function copyCollection(name: string, options: CollectionOptions): ImportStep {
return {
name,
async run() {
const local = localCollection(name, options);
const shared = getStore().collection(name, options);
let written = 0;
for (const id of await local.list()) {
const doc = await local.get(id);
if (!doc) continue;
if ((await shared.put(id, doc.data, { ifVersion: null })).ok) written++;
}
return written;
},
};
}
export const steps: ImportStep[] = [
copyCollection('settings', SETTINGS_COLLECTION),
copyCollection('plugin-secrets', VAULT_COLLECTION),
copyCollection('account-archives', ARCHIVE_COLLECTION),
// state.json, logins.json and the instance id .telemetry-id
copyCollection('telemetry', TELEMETRY_COLLECTION),
copyCollection('version-check', VERSION_CHECK_COLLECTION),
copyCollection('devices', DEVICES_COLLECTION),
];