5b0aa24a8d
Extend DeviceRecord with revokedAt field, update registerDevice and listOwnDevices selects to include revoked_at, add revokeDevice RPC helper, update db-types to reflect T1 migration schema, and add vitest coverage for all new behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
2.6 KiB
TypeScript
91 lines
2.6 KiB
TypeScript
import { fromBase64, toBase64 } from '../crypto/index';
|
|
import type { AppSupabaseClient } from '../supabase/client';
|
|
import type { DevicePlatform } from '../supabase/types';
|
|
|
|
// After the per-user encryption refactor, devices rows are pure telemetry:
|
|
// they record which physical/browser installs are signed into the account so
|
|
// the user can audit them, but they no longer carry cryptographic identity.
|
|
|
|
export interface RegisterDeviceParams {
|
|
name: string; // user-facing, e.g. "Dennis Laptop"
|
|
platform: DevicePlatform;
|
|
}
|
|
|
|
export interface DeviceRecord {
|
|
id: string;
|
|
name: string;
|
|
platform: DevicePlatform;
|
|
lastSeenAt: string;
|
|
revokedAt: string | null;
|
|
}
|
|
|
|
export async function registerDevice(
|
|
client: AppSupabaseClient,
|
|
params: RegisterDeviceParams,
|
|
): Promise<DeviceRecord> {
|
|
const { data: session } = await client.auth.getUser();
|
|
if (!session.user) throw new Error('not authenticated');
|
|
|
|
const { data, error } = await client
|
|
.from('devices')
|
|
.insert({
|
|
user_id: session.user.id,
|
|
name: params.name,
|
|
platform: params.platform,
|
|
})
|
|
.select('id, name, platform, last_seen_at, revoked_at')
|
|
.single();
|
|
if (error) throw error;
|
|
|
|
return {
|
|
id: data.id,
|
|
name: data.name,
|
|
platform: data.platform,
|
|
lastSeenAt: data.last_seen_at,
|
|
revokedAt: data.revoked_at,
|
|
};
|
|
}
|
|
|
|
export async function listOwnDevices(client: AppSupabaseClient): Promise<DeviceRecord[]> {
|
|
const { data: session } = await client.auth.getUser();
|
|
if (!session.user) throw new Error('not authenticated');
|
|
|
|
const { data, error } = await client
|
|
.from('devices')
|
|
.select('id, name, platform, last_seen_at, revoked_at')
|
|
.eq('user_id', session.user.id)
|
|
.order('last_seen_at', { ascending: false });
|
|
if (error) throw error;
|
|
|
|
return data.map((row) => ({
|
|
id: row.id,
|
|
name: row.name,
|
|
platform: row.platform,
|
|
lastSeenAt: row.last_seen_at,
|
|
revokedAt: row.revoked_at,
|
|
}));
|
|
}
|
|
|
|
export async function touchDeviceLastSeen(
|
|
client: AppSupabaseClient,
|
|
deviceId: string,
|
|
): Promise<void> {
|
|
const { error } = await client
|
|
.from('devices')
|
|
.update({ last_seen_at: new Date().toISOString() })
|
|
.eq('id', deviceId);
|
|
if (error) throw error;
|
|
}
|
|
|
|
export async function revokeDevice(
|
|
client: AppSupabaseClient,
|
|
deviceId: string,
|
|
): Promise<void> {
|
|
const { error } = await client.rpc('revoke_device', { p_device_id: deviceId });
|
|
if (error) throw new Error(error.message);
|
|
}
|
|
|
|
// Intentional re-exports so app layers only need @chat-app/shared/auth.
|
|
export type { SecretStore } from './secure-storage';
|
|
export { toBase64 as base64FromBytes, fromBase64 as bytesFromBase64 };
|