fix(shared): drop .js extensions from relative imports for Metro

packages/shared/src/index.ts and all sub-modules used .js extensions on
relative imports (e.g. './admin/index.js') pointing at .ts source files.
TypeScript with moduleResolution: "Bundler" doesn't need them, and
Metro's eager exporter (used for preview / production builds) reads
them literally and fails — only the dev-server Metro fell back to .ts.

Workspace typecheck remains 8/8 green; Vite and TS Bundler resolution
already accept both styles, so desktop is unaffected.
This commit is contained in:
byGalax
2026-05-15 01:52:14 +02:00
parent b0967dd2c5
commit b61f929cf7
25 changed files with 108 additions and 108 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
// Admin-only helpers. Every call is RLS-gated server-side via the // Admin-only helpers. Every call is RLS-gated server-side via the
// `current_user_is_admin()` helper + admin-specific policies. Non-admins // `current_user_is_admin()` helper + admin-specific policies. Non-admins
// trying to call these still get clean PostgREST 403/empty-result responses. // trying to call these still get clean PostgREST 403/empty-result responses.
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
// --- Admin settings ------------------------------------------------------- // --- Admin settings -------------------------------------------------------
@@ -189,7 +189,7 @@ export async function setUserFlag(
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Conversations (admin view reads all regardless of membership) // Conversations (admin view — reads all regardless of membership)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface AdminConversationRow { export interface AdminConversationRow {
+9 -9
View File
@@ -1,8 +1,8 @@
import { fromBase64, generateX25519KeyPair, toBase64, wipe } from '../crypto/index.js'; import { fromBase64, generateX25519KeyPair, toBase64, wipe } from '../crypto/index';
import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea.js'; import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea';
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
import type { DevicePlatform } from '../supabase/types.js'; import type { DevicePlatform } from '../supabase/types';
import type { SecretStore } from './secure-storage.js'; import type { SecretStore } from './secure-storage';
export interface RegisterDeviceParams { export interface RegisterDeviceParams {
name: string; // user-facing, e.g. "Dennis Laptop" name: string; // user-facing, e.g. "Dennis Laptop"
@@ -153,7 +153,7 @@ export async function saveDevicePrivateKey(
} }
// Restores a device record from a backup by re-seeding the local private key // Restores a device record from a backup by re-seeding the local private key
// store for an EXISTING server-side device row. Does NOT insert a new row // store for an EXISTING server-side device row. Does NOT insert a new row —
// the original row is kept intact so conversation-key bundles stay valid. // the original row is kept intact so conversation-key bundles stay valid.
// Throws when the server-side device was removed (the backup is then unusable; // Throws when the server-side device was removed (the backup is then unusable;
// user must provision a fresh device and get conv-keys shared from another // user must provision a fresh device and get conv-keys shared from another
@@ -169,7 +169,7 @@ export async function restoreDeviceFromServerRecord(params: {
if (!session.user) throw new Error('not authenticated'); if (!session.user) throw new Error('not authenticated');
if (session.user.id !== params.userId) { if (session.user.id !== params.userId) {
throw new Error( throw new Error(
'Backup is for a different account sign in as the owner before restoring.', 'Backup is for a different account — sign in as the owner before restoring.',
); );
} }
@@ -182,7 +182,7 @@ export async function restoreDeviceFromServerRecord(params: {
if (error) throw error; if (error) throw error;
if (!row) { if (!row) {
throw new Error( throw new Error(
'Device record not found on server it was removed. Backup is no longer valid; register a new device instead.', 'Device record not found on server — it was removed. Backup is no longer valid; register a new device instead.',
); );
} }
@@ -211,5 +211,5 @@ export function deviceIdStorageKey(userId: string): string {
} }
// Intentional re-exports so app layers only need @chat-app/shared/auth. // Intentional re-exports so app layers only need @chat-app/shared/auth.
export type { SecretStore } from './secure-storage.js'; export type { SecretStore } from './secure-storage';
export { toBase64 as base64FromBytes, fromBase64 as bytesFromBase64 }; export { toBase64 as base64FromBytes, fromBase64 as bytesFromBase64 };
+3 -3
View File
@@ -1,3 +1,3 @@
export * from './device.js'; export * from './device';
export * from './magic-link.js'; export * from './magic-link';
export * from './profile.js'; export * from './profile';
+5 -5
View File
@@ -1,5 +1,5 @@
import type { SupportedLocale } from '../i18n/types.js'; import type { SupportedLocale } from '../i18n/types';
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
export interface SignupParams { export interface SignupParams {
email: string; email: string;
@@ -58,7 +58,7 @@ export async function loginWithMagicLink(
} }
// Parse a callback URL produced by the magic-link email and establish a session. // Parse a callback URL produced by the magic-link email and establish a session.
// Works for both PKCE (`?code=`) and legacy token-hash (`#access_token=`). // Works for both PKCE (`?code=…`) and legacy token-hash (`#access_token=…`).
export async function completeSessionFromUrl( export async function completeSessionFromUrl(
client: AppSupabaseClient, client: AppSupabaseClient,
url: string, url: string,
@@ -91,14 +91,14 @@ export async function completeSessionFromUrl(
export async function signOut(client: AppSupabaseClient): Promise<void> { export async function signOut(client: AppSupabaseClient): Promise<void> {
// `scope: 'local'` only ends the session in THIS client. Without it Supabase // `scope: 'local'` only ends the session in THIS client. Without it Supabase
// defaults to 'global', which invalidates the user's refresh tokens // defaults to 'global', which invalidates the user's refresh tokens
// everywhere meaning a logout in the browser would also kick the desktop // everywhere — meaning a logout in the browser would also kick the desktop
// app (and vice versa) the next time it tries to refresh its token. // app (and vice versa) the next time it tries to refresh its token.
const { error } = await client.auth.signOut({ scope: 'local' }); const { error } = await client.auth.signOut({ scope: 'local' });
if (error) throw error; if (error) throw error;
} }
// Verify a 6-digit OTP that arrived via magic-link email. Establishes a // Verify a 6-digit OTP that arrived via magic-link email. Establishes a
// session in the CURRENT webview no browser redirect involved. Used by the // session in the CURRENT webview — no browser redirect involved. Used by the
// desktop/mobile apps where cross-origin redirects don't carry the session // desktop/mobile apps where cross-origin redirects don't carry the session
// back to the native webview. // back to the native webview.
export async function verifyMagicLinkOtp( export async function verifyMagicLinkOtp(
+5 -5
View File
@@ -1,6 +1,6 @@
import type { SupportedLocale } from '../i18n/types.js'; import type { SupportedLocale } from '../i18n/types';
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
import type { Database, PresenceState } from '../supabase/types.js'; import type { Database, PresenceState } from '../supabase/types';
type ProfileUpdate = Database['public']['Tables']['profiles']['Update']; type ProfileUpdate = Database['public']['Tables']['profiles']['Update'];
@@ -75,7 +75,7 @@ export async function getProfileByUsername(
const { data, error } = await client const { data, error } = await client
.from('profiles') .from('profiles')
.select(PROFILE_COLS) .select(PROFILE_COLS)
// citext column compares CI server-side send raw input, don't force case. // citext column compares CI server-side — send raw input, don't force case.
.eq('username', username.trim()) .eq('username', username.trim())
.maybeSingle(); .maybeSingle();
if (error) throw error; if (error) throw error;
@@ -89,7 +89,7 @@ export async function isUsernameAvailable(
const { count, error } = await client const { count, error } = await client
.from('profiles') .from('profiles')
.select('user_id', { count: 'exact', head: true }) .select('user_id', { count: 'exact', head: true })
// citext compares CI no manual normalization needed. // citext compares CI — no manual normalization needed.
.eq('username', username.trim()); .eq('username', username.trim());
if (error) throw error; if (error) throw error;
return (count ?? 0) === 0; return (count ?? 0) === 0;
+6 -6
View File
@@ -1,4 +1,4 @@
// Encrypted attachment upload / download. // Encrypted attachment upload / download.
// //
// Per-message symmetric key + nonce encrypts the raw blob (XSalsa20-Poly1305 // Per-message symmetric key + nonce encrypts the raw blob (XSalsa20-Poly1305
// via secretbox). The encrypted blob is uploaded to Supabase Storage under // via secretbox). The encrypted blob is uploaded to Supabase Storage under
@@ -6,8 +6,8 @@
// travel inside the per-device message envelope as JSON, so the server never // travel inside the per-device message envelope as JSON, so the server never
// sees the decryption material. // sees the decryption material.
import { fromBase64, getCryptoBackend, toBase64 } from '../crypto/index.js'; import { fromBase64, getCryptoBackend, toBase64 } from '../crypto/index';
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
export const ATTACHMENT_BUCKET = 'chat-attachments'; export const ATTACHMENT_BUCKET = 'chat-attachments';
export const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10 MB export const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10 MB
@@ -22,7 +22,7 @@ export interface AttachmentHandle {
sizeBytes: number; sizeBytes: number;
width?: number; width?: number;
height?: number; height?: number;
// base64-encoded only readable via per-device envelope decrypt. // base64-encoded — only readable via per-device envelope decrypt.
keyB64: string; keyB64: string;
nonceB64: string; nonceB64: string;
} }
@@ -166,11 +166,11 @@ function ensureUuid(): string {
export interface EncryptedAttachmentResult { export interface EncryptedAttachmentResult {
handle: AttachmentHandle; handle: AttachmentHandle;
key: Uint8Array; // raw bytes caller is responsible for wiping key: Uint8Array; // raw bytes — caller is responsible for wiping
nonce: Uint8Array; nonce: Uint8Array;
} }
// Encrypt + upload a blob. Does NOT insert the message_attachments row // Encrypt + upload a blob. Does NOT insert the message_attachments row —
// the caller combines this with a message insert so everything commits // the caller combines this with a message insert so everything commits
// atomically at the application layer. // atomically at the application layer.
export async function encryptAndUploadAttachment(params: { export async function encryptAndUploadAttachment(params: {
+12 -12
View File
@@ -1,12 +1,12 @@
import { import {
decryptWithConvKey, decryptWithConvKey,
encryptWithConvKey, encryptWithConvKey,
generateConvKey, generateConvKey,
unwrapConvKey, unwrapConvKey,
wrapConvKeyForRecipient, wrapConvKeyForRecipient,
} from '../crypto/sessionKeys.js'; } from '../crypto/sessionKeys';
import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea.js'; import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea';
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
// db-types in this monorepo is a static snapshot generated against the older // db-types in this monorepo is a static snapshot generated against the older
// schema. The new `conversation_keys` table + `active_key_version` column on // schema. The new `conversation_keys` table + `active_key_version` column on
@@ -135,7 +135,7 @@ function hexNoPrefix(bytes: Uint8Array): string {
// Bootstraps a brand-new conv-key, wrapping it for every member device that // Bootstraps a brand-new conv-key, wrapping it for every member device that
// currently exists (including the caller's own devices). Used the first time // currently exists (including the caller's own devices). Used the first time
// a conversation needs a key, or when rotation is requested. All inserts go // a conversation needs a key, or when rotation is requested. All inserts go
// through `share_conv_keys` (SECURITY DEFINER) silently skips invalid // through `share_conv_keys` (SECURITY DEFINER) — silently skips invalid
// recipients, no per-row 403 console spam. // recipients, no per-row 403 console spam.
export async function bootstrapConvKey( export async function bootstrapConvKey(
client: AppSupabaseClient, client: AppSupabaseClient,
@@ -146,7 +146,7 @@ export async function bootstrapConvKey(
const convKey = generateConvKey(); const convKey = generateConvKey();
const recipients = await listDeviceKeys(client, conversationId); const recipients = await listDeviceKeys(client, conversationId);
if (recipients.length === 0) { if (recipients.length === 0) {
throw new Error('cannot bootstrap conv key no recipient devices'); throw new Error('cannot bootstrap conv key — no recipient devices');
} }
const bundles: Array<{ recipient_device_id: string; encrypted_key: string; nonce: string }> = []; const bundles: Array<{ recipient_device_id: string; encrypted_key: string; nonce: string }> = [];
@@ -176,9 +176,9 @@ export async function bootstrapConvKey(
// Resolves the current conv-key for `conversationId`. Order: // Resolves the current conv-key for `conversationId`. Order:
// 1) cache hit // 1) cache hit
// 2) DB row for own device unwrap // 2) DB row for own device → unwrap
// 3) bootstrap a brand-new key (only valid path if NO existing keys exist // 3) bootstrap a brand-new key (only valid path if NO existing keys exist
// for any device i.e. this is the conversation's very first message) // for any device — i.e. this is the conversation's very first message)
export async function getOrCreateConvKey( export async function getOrCreateConvKey(
client: AppSupabaseClient, client: AppSupabaseClient,
conversationId: string, conversationId: string,
@@ -202,8 +202,8 @@ export async function getOrCreateConvKey(
} }
// No bundle yet for THIS device. Two cases: // No bundle yet for THIS device. Two cases:
// - I'm the first ever sender bootstrap. // - I'm the first ever sender → bootstrap.
// - Conversation already has keys but my device wasn't included yet I // - Conversation already has keys but my device wasn't included yet → I
// have to wait until an existing device wraps the key for me. // have to wait until an existing device wraps the key for me.
const { count, error: cntErr } = await rawFrom(client, 'conversation_keys') const { count, error: cntErr } = await rawFrom(client, 'conversation_keys')
.select('recipient_device_id', { count: 'exact', head: true }) .select('recipient_device_id', { count: 'exact', head: true })
@@ -213,7 +213,7 @@ export async function getOrCreateConvKey(
if ((count ?? 0) > 0) { if ((count ?? 0) > 0) {
throw new Error( throw new Error(
'Awaiting conversation key another device must share it with this device.', 'Awaiting conversation key — another device must share it with this device.',
); );
} }
return bootstrapConvKey(client, conversationId, own, version); return bootstrapConvKey(client, conversationId, own, version);
@@ -259,7 +259,7 @@ export async function shareConvKeyToDevice(
cache.get(cacheKey(conversationId, version)) ?? cache.get(cacheKey(conversationId, version)) ??
(await tryGetConvKey(client, conversationId, own.deviceId, own.privateKey, version)); (await tryGetConvKey(client, conversationId, own.deviceId, own.privateKey, version));
if (!handle) { if (!handle) {
throw new Error('cannot share conv key own device does not have it yet'); throw new Error('cannot share conv key — own device does not have it yet');
} }
const wrapped = await wrapConvKeyForRecipient( const wrapped = await wrapConvKeyForRecipient(
+4 -4
View File
@@ -1,6 +1,6 @@
import type { ProfileBrief } from '../friends/index.js'; import type { ProfileBrief } from '../friends/index';
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
import type { ConversationSummary } from './types.js'; import type { ConversationSummary } from './types';
const PROFILE_BRIEF_COLS = 'user_id, username, display_name, avatar_url, banner_url'; const PROFILE_BRIEF_COLS = 'user_id, username, display_name, avatar_url, banner_url';
@@ -63,7 +63,7 @@ export async function listConversations(client: AppSupabaseClient): Promise<Conv
.in('conversation_id', convIds); .in('conversation_id', convIds);
if (aErr) throw aErr; if (aErr) throw aErr;
// 4. Profiles for ALL distinct member user ids (including self the // 4. Profiles for ALL distinct member user ids (including self — the
// member list in GroupInfoPanel needs our own display name too). // member list in GroupInfoPanel needs our own display name too).
const memberIdsAll = Array.from(new Set((allMembers ?? []).map((m) => m.user_id))); const memberIdsAll = Array.from(new Set((allMembers ?? []).map((m) => m.user_id)));
const profileMap = new Map<string, ProfileBrief>(); const profileMap = new Map<string, ProfileBrief>();
+4 -4
View File
@@ -1,5 +1,5 @@
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
import type { MemberRole } from '../supabase/types.js'; import type { MemberRole } from '../supabase/types';
async function currentUserId(client: AppSupabaseClient): Promise<string> { async function currentUserId(client: AppSupabaseClient): Promise<string> {
const { data, error } = await client.auth.getUser(); const { data, error } = await client.auth.getUser();
@@ -14,7 +14,7 @@ export interface CreateGroupParams {
memberUserIds: string[]; memberUserIds: string[];
} }
// Client-side 3-step create: conversation self as admin peers as members. // Client-side 3-step create: conversation → self as admin → peers as members.
// On any peer-insert failure the conversation still survives (partial group is // On any peer-insert failure the conversation still survives (partial group is
// still usable, admin can retry). On self-insert failure we delete the empty // still usable, admin can retry). On self-insert failure we delete the empty
// conversation to avoid orphans. // conversation to avoid orphans.
@@ -24,7 +24,7 @@ export async function createGroup(params: CreateGroupParams): Promise<string> {
if (trimmed.length === 0) throw new Error('group name required'); if (trimmed.length === 0) throw new Error('group name required');
// Generate the uuid client-side so we don't need a post-insert SELECT on // Generate the uuid client-side so we don't need a post-insert SELECT on
// conversations the SELECT policy requires membership, which only exists // conversations — the SELECT policy requires membership, which only exists
// AFTER we insert the creator's member row in step 2. // AFTER we insert the creator's member row in step 2.
const conversationId = crypto.randomUUID(); const conversationId = crypto.randomUUID();
+7 -7
View File
@@ -1,11 +1,11 @@
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
export * from './attachments.js'; export * from './attachments';
export * from './conversations.js'; export * from './conversations';
export * from './convKeys.js'; export * from './convKeys';
export * from './groups.js'; export * from './groups';
export * from './messages.js'; export * from './messages';
export * from './types.js'; export * from './types';
// ----- RPC wrappers --------------------------------------------------------- // ----- RPC wrappers ---------------------------------------------------------
+7 -7
View File
@@ -1,14 +1,14 @@
import { bytesToUtf8, utf8ToBytes } from '../crypto/index.js'; import { bytesToUtf8, utf8ToBytes } from '../crypto/index';
import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea.js'; import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea';
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
import { import {
decryptWithConvKey, decryptWithConvKey,
encryptWithConvKey, encryptWithConvKey,
getOrCreateConvKey, getOrCreateConvKey,
type OwnDeviceCtx, type OwnDeviceCtx,
tryGetConvKey, tryGetConvKey,
} from './convKeys.js'; } from './convKeys';
import type { ChatMessage, DecryptedMessage } from './types.js'; import type { ChatMessage, DecryptedMessage } from './types';
const MESSAGE_COLS = const MESSAGE_COLS =
'id, conversation_id, sender_id, sender_device_id, reply_to_id, edited_at, deleted_at, created_at, ciphertext, nonce, key_version'; 'id, conversation_id, sender_id, sender_device_id, reply_to_id, edited_at, deleted_at, created_at, ciphertext, nonce, key_version';
@@ -92,7 +92,7 @@ export interface SendMessageParams {
senderDeviceId: string; senderDeviceId: string;
senderPrivateKey: Uint8Array; senderPrivateKey: Uint8Array;
replyToId?: string; replyToId?: string;
// Optional encrypted attachments their handles are already materialised // Optional encrypted attachments — their handles are already materialised
// via `encryptAndUploadAttachment`. The caller is responsible for creating // via `encryptAndUploadAttachment`. The caller is responsible for creating
// the corresponding message_attachments rows (see insertAttachmentRow) once // the corresponding message_attachments rows (see insertAttachmentRow) once
// the returned message id is known. // the returned message id is known.
@@ -246,7 +246,7 @@ export async function listPeerReadsForMessages(
} }
// Group variant: returns reads from ALL users (other than `excludeUserId`, // Group variant: returns reads from ALL users (other than `excludeUserId`,
// typically caller themselves) keyed by message id user id timestamp. // typically caller themselves) keyed by message id → user id → timestamp.
// RLS already filters out users whose receipts are off. // RLS already filters out users whose receipts are off.
export async function listGroupReadsForMessages( export async function listGroupReadsForMessages(
client: AppSupabaseClient, client: AppSupabaseClient,
+3 -3
View File
@@ -1,5 +1,5 @@
import type { ProfileBrief } from '../friends/index.js'; import type { ProfileBrief } from '../friends/index';
import type { ConversationType, MemberRole } from '../supabase/types.js'; import type { ConversationType, MemberRole } from '../supabase/types';
export interface ConversationMember { export interface ConversationMember {
userId: string; userId: string;
@@ -26,7 +26,7 @@ export interface ConversationSummary {
// Caller's per-member preferences. // Caller's per-member preferences.
archived: boolean; archived: boolean;
// ISO timestamp. null = not muted. Past timestamp = expired mute (treat as // ISO timestamp. null = not muted. Past timestamp = expired mute (treat as
// not muted the server row is kept for history until the next toggle). // not muted — the server row is kept for history until the next toggle).
mutedUntil: string | null; mutedUntil: string | null;
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCryptoBackend } from './backend.js'; import { getCryptoBackend } from './backend';
// Authenticated encryption using XSalsa20-Poly1305 + X25519 (Curve25519). // Authenticated encryption using XSalsa20-Poly1305 + X25519 (Curve25519).
// Delegates to the active CryptoBackend (libsodium on desktop, libsodium-rn // Delegates to the active CryptoBackend (libsodium on desktop, libsodium-rn
+4 -4
View File
@@ -1,4 +1,4 @@
export * from './backend.js'; export * from './backend';
export * from './box.js'; export * from './box';
export * from './keys.js'; export * from './keys';
export * from './sessionKeys.js'; export * from './sessionKeys';
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCryptoBackend } from './backend.js'; import { getCryptoBackend } from './backend';
export interface X25519KeyPair { export interface X25519KeyPair {
publicKey: Uint8Array; // 32 bytes publicKey: Uint8Array; // 32 bytes
+3 -3
View File
@@ -1,7 +1,7 @@
import { getCryptoBackend } from './backend.js'; import { getCryptoBackend } from './backend';
import { decryptFrom, encryptFor, type EncryptedEnvelope } from './box.js'; import { decryptFrom, encryptFor, type EncryptedEnvelope } from './box';
// Sender-Key (Signal-style) helpers one symmetric XSalsa20-Poly1305 key per // Sender-Key (Signal-style) helpers — one symmetric XSalsa20-Poly1305 key per
// conversation, wrapped with `crypto_box` for each recipient device's pubkey. // conversation, wrapped with `crypto_box` for each recipient device's pubkey.
// //
// Flow: // Flow:
+3 -3
View File
@@ -1,5 +1,5 @@
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
import type { FriendshipStatus } from '../supabase/types.js'; import type { FriendshipStatus } from '../supabase/types';
export interface ProfileBrief { export interface ProfileBrief {
userId: string; userId: string;
@@ -147,7 +147,7 @@ export async function acceptFriendRequest(
if (error) throw error; if (error) throw error;
} }
// Used for both "decline incoming" and "cancel outgoing" semantically a // Used for both "decline incoming" and "cancel outgoing" — semantically a
// bilateral break. // bilateral break.
export async function removeFriendship( export async function removeFriendship(
client: AppSupabaseClient, client: AppSupabaseClient,
+1 -1
View File
@@ -1,4 +1,4 @@
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, type SupportedLocale } from './types.js'; import { DEFAULT_LOCALE, SUPPORTED_LOCALES, type SupportedLocale } from './types';
export function isSupportedLocale(value: string | null | undefined): value is SupportedLocale { export function isSupportedLocale(value: string | null | undefined): value is SupportedLocale {
return ( return (
+7 -7
View File
@@ -1,12 +1,12 @@
import i18next, { type i18n as I18nInstance, type Resource } from 'i18next'; import i18next, { type i18n as I18nInstance, type Resource } from 'i18next';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import { resources } from './resources.js'; import { resources } from './resources';
import { DEFAULT_LOCALE, type SupportedLocale } from './types.js'; import { DEFAULT_LOCALE, type SupportedLocale } from './types';
export * from './detect.js'; export * from './detect';
export * from './error-map.js'; export * from './error-map';
export * from './types.js'; export * from './types';
export interface InitI18nOptions { export interface InitI18nOptions {
initialLocale: SupportedLocale; initialLocale: SupportedLocale;
@@ -14,7 +14,7 @@ export interface InitI18nOptions {
onLanguageChanged?: (locale: SupportedLocale) => void; onLanguageChanged?: (locale: SupportedLocale) => void;
} }
// Idempotent init safe to call from multiple entry points. // Idempotent init — safe to call from multiple entry points.
// Returns the configured i18next instance. // Returns the configured i18next instance.
export function initI18n(options: InitI18nOptions): I18nInstance { export function initI18n(options: InitI18nOptions): I18nInstance {
if (!i18next.isInitialized) { if (!i18next.isInitialized) {
+2 -2
View File
@@ -1,4 +1,4 @@
import deApp from './locales/de/app.json'; import deApp from './locales/de/app.json';
import deAuth from './locales/de/auth.json'; import deAuth from './locales/de/auth.json';
import deCommon from './locales/de/common.json'; import deCommon from './locales/de/common.json';
import deErrors from './locales/de/errors.json'; import deErrors from './locales/de/errors.json';
@@ -6,7 +6,7 @@ import enApp from './locales/en/app.json';
import enAuth from './locales/en/auth.json'; import enAuth from './locales/en/auth.json';
import enCommon from './locales/en/common.json'; import enCommon from './locales/en/common.json';
import enErrors from './locales/en/errors.json'; import enErrors from './locales/en/errors.json';
import type { Resources, SupportedLocale } from './types.js'; import type { Resources, SupportedLocale } from './types';
export const resources: Record<SupportedLocale, Resources> = { export const resources: Record<SupportedLocale, Resources> = {
en: { common: enCommon, auth: enAuth, errors: enErrors, app: enApp }, en: { common: enCommon, auth: enAuth, errors: enErrors, app: enApp },
+8 -8
View File
@@ -1,8 +1,8 @@
export * as admin from './admin/index.js'; export * as admin from './admin/index';
export * as auth from './auth/index.js'; export * as auth from './auth/index';
export * as chat from './chat/index.js'; export * as chat from './chat/index';
export * as crypto from './crypto/index.js'; export * as crypto from './crypto/index';
export * as friends from './friends/index.js'; export * as friends from './friends/index';
export * as i18n from './i18n/index.js'; export * as i18n from './i18n/index';
export * as rtc from './rtc/index.js'; export * as rtc from './rtc/index';
export * as supabase from './supabase/index.js'; export * as supabase from './supabase/index';
+2 -2
View File
@@ -1,2 +1,2 @@
export * from './token.js'; export * from './token';
export * from './types.js'; export * from './types';
+2 -2
View File
@@ -1,5 +1,5 @@
import type { AppSupabaseClient } from '../supabase/client.js'; import type { AppSupabaseClient } from '../supabase/client';
import type { LivekitToken } from './types.js'; import type { LivekitToken } from './types';
// Fetch a short-lived LiveKit access token via the mint-livekit-token // Fetch a short-lived LiveKit access token via the mint-livekit-token
// edge function. The server-side RLS check lives inside that function. // edge function. The server-side RLS check lives inside that function.
+3 -3
View File
@@ -1,11 +1,11 @@
import { createClient as createSupabaseClient, type SupabaseClient } from '@supabase/supabase-js'; import { createClient as createSupabaseClient, type SupabaseClient } from '@supabase/supabase-js';
import type { Database, SupabaseConfig } from './types.js'; import type { Database, SupabaseConfig } from './types';
// Typed client alias used throughout the app. // Typed client alias used throughout the app.
export type AppSupabaseClient = SupabaseClient<Database>; export type AppSupabaseClient = SupabaseClient<Database>;
// Inline serial lock replaces Supabase's default `navigator.locks` based // Inline serial lock — replaces Supabase's default `navigator.locks` based
// lock that occasionally throws "Lock was stolen by another request" when // lock that occasionally throws "Lock was stolen by another request" when
// the same origin opens multiple tabs / Tauri windows / HMR-reloaded // the same origin opens multiple tabs / Tauri windows / HMR-reloaded
// modules. We only have one client instance per process so a simple promise // modules. We only have one client instance per process so a simple promise
+3 -3
View File
@@ -1,3 +1,3 @@
export * from './bytea.js'; export * from './bytea';
export * from './client.js'; export * from './client';
export * from './types.js'; export * from './types';