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:
@@ -1,4 +1,4 @@
|
||||
// Encrypted attachment upload / download.
|
||||
// Encrypted attachment upload / download.
|
||||
//
|
||||
// Per-message symmetric key + nonce encrypts the raw blob (XSalsa20-Poly1305
|
||||
// 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
|
||||
// sees the decryption material.
|
||||
|
||||
import { fromBase64, getCryptoBackend, toBase64 } from '../crypto/index.js';
|
||||
import type { AppSupabaseClient } from '../supabase/client.js';
|
||||
import { fromBase64, getCryptoBackend, toBase64 } from '../crypto/index';
|
||||
import type { AppSupabaseClient } from '../supabase/client';
|
||||
|
||||
export const ATTACHMENT_BUCKET = 'chat-attachments';
|
||||
export const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10 MB
|
||||
@@ -22,7 +22,7 @@ export interface AttachmentHandle {
|
||||
sizeBytes: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
// base64-encoded — only readable via per-device envelope decrypt.
|
||||
// base64-encoded — only readable via per-device envelope decrypt.
|
||||
keyB64: string;
|
||||
nonceB64: string;
|
||||
}
|
||||
@@ -166,11 +166,11 @@ function ensureUuid(): string {
|
||||
|
||||
export interface EncryptedAttachmentResult {
|
||||
handle: AttachmentHandle;
|
||||
key: Uint8Array; // raw bytes — caller is responsible for wiping
|
||||
key: Uint8Array; // raw bytes — caller is responsible for wiping
|
||||
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
|
||||
// atomically at the application layer.
|
||||
export async function encryptAndUploadAttachment(params: {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
import {
|
||||
decryptWithConvKey,
|
||||
encryptWithConvKey,
|
||||
generateConvKey,
|
||||
unwrapConvKey,
|
||||
wrapConvKeyForRecipient,
|
||||
} from '../crypto/sessionKeys.js';
|
||||
import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea.js';
|
||||
import type { AppSupabaseClient } from '../supabase/client.js';
|
||||
} from '../crypto/sessionKeys';
|
||||
import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea';
|
||||
import type { AppSupabaseClient } from '../supabase/client';
|
||||
|
||||
// db-types in this monorepo is a static snapshot generated against the older
|
||||
// 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
|
||||
// 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
|
||||
// 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.
|
||||
export async function bootstrapConvKey(
|
||||
client: AppSupabaseClient,
|
||||
@@ -146,7 +146,7 @@ export async function bootstrapConvKey(
|
||||
const convKey = generateConvKey();
|
||||
const recipients = await listDeviceKeys(client, conversationId);
|
||||
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 }> = [];
|
||||
@@ -176,9 +176,9 @@ export async function bootstrapConvKey(
|
||||
|
||||
// Resolves the current conv-key for `conversationId`. Order:
|
||||
// 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
|
||||
// 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(
|
||||
client: AppSupabaseClient,
|
||||
conversationId: string,
|
||||
@@ -202,8 +202,8 @@ export async function getOrCreateConvKey(
|
||||
}
|
||||
|
||||
// No bundle yet for THIS device. Two cases:
|
||||
// - I'm the first ever sender → bootstrap.
|
||||
// - Conversation already has keys but my device wasn't included yet → I
|
||||
// - I'm the first ever sender → bootstrap.
|
||||
// - Conversation already has keys but my device wasn't included yet → I
|
||||
// have to wait until an existing device wraps the key for me.
|
||||
const { count, error: cntErr } = await rawFrom(client, 'conversation_keys')
|
||||
.select('recipient_device_id', { count: 'exact', head: true })
|
||||
@@ -213,7 +213,7 @@ export async function getOrCreateConvKey(
|
||||
|
||||
if ((count ?? 0) > 0) {
|
||||
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);
|
||||
@@ -259,7 +259,7 @@ export async function shareConvKeyToDevice(
|
||||
cache.get(cacheKey(conversationId, version)) ??
|
||||
(await tryGetConvKey(client, conversationId, own.deviceId, own.privateKey, version));
|
||||
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(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ProfileBrief } from '../friends/index.js';
|
||||
import type { AppSupabaseClient } from '../supabase/client.js';
|
||||
import type { ConversationSummary } from './types.js';
|
||||
import type { ProfileBrief } from '../friends/index';
|
||||
import type { AppSupabaseClient } from '../supabase/client';
|
||||
import type { ConversationSummary } from './types';
|
||||
|
||||
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);
|
||||
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).
|
||||
const memberIdsAll = Array.from(new Set((allMembers ?? []).map((m) => m.user_id)));
|
||||
const profileMap = new Map<string, ProfileBrief>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AppSupabaseClient } from '../supabase/client.js';
|
||||
import type { MemberRole } from '../supabase/types.js';
|
||||
import type { AppSupabaseClient } from '../supabase/client';
|
||||
import type { MemberRole } from '../supabase/types';
|
||||
|
||||
async function currentUserId(client: AppSupabaseClient): Promise<string> {
|
||||
const { data, error } = await client.auth.getUser();
|
||||
@@ -14,7 +14,7 @@ export interface CreateGroupParams {
|
||||
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
|
||||
// still usable, admin can retry). On self-insert failure we delete the empty
|
||||
// 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');
|
||||
|
||||
// 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.
|
||||
const conversationId = crypto.randomUUID();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { AppSupabaseClient } from '../supabase/client.js';
|
||||
import type { AppSupabaseClient } from '../supabase/client';
|
||||
|
||||
export * from './attachments.js';
|
||||
export * from './conversations.js';
|
||||
export * from './convKeys.js';
|
||||
export * from './groups.js';
|
||||
export * from './messages.js';
|
||||
export * from './types.js';
|
||||
export * from './attachments';
|
||||
export * from './conversations';
|
||||
export * from './convKeys';
|
||||
export * from './groups';
|
||||
export * from './messages';
|
||||
export * from './types';
|
||||
|
||||
// ----- RPC wrappers ---------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { bytesToUtf8, utf8ToBytes } from '../crypto/index.js';
|
||||
import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea.js';
|
||||
import type { AppSupabaseClient } from '../supabase/client.js';
|
||||
import { bytesToUtf8, utf8ToBytes } from '../crypto/index';
|
||||
import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea';
|
||||
import type { AppSupabaseClient } from '../supabase/client';
|
||||
import {
|
||||
decryptWithConvKey,
|
||||
encryptWithConvKey,
|
||||
getOrCreateConvKey,
|
||||
type OwnDeviceCtx,
|
||||
tryGetConvKey,
|
||||
} from './convKeys.js';
|
||||
import type { ChatMessage, DecryptedMessage } from './types.js';
|
||||
} from './convKeys';
|
||||
import type { ChatMessage, DecryptedMessage } from './types';
|
||||
|
||||
const MESSAGE_COLS =
|
||||
'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;
|
||||
senderPrivateKey: Uint8Array;
|
||||
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
|
||||
// the corresponding message_attachments rows (see insertAttachmentRow) once
|
||||
// the returned message id is known.
|
||||
@@ -246,7 +246,7 @@ export async function listPeerReadsForMessages(
|
||||
}
|
||||
|
||||
// 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.
|
||||
export async function listGroupReadsForMessages(
|
||||
client: AppSupabaseClient,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ProfileBrief } from '../friends/index.js';
|
||||
import type { ConversationType, MemberRole } from '../supabase/types.js';
|
||||
import type { ProfileBrief } from '../friends/index';
|
||||
import type { ConversationType, MemberRole } from '../supabase/types';
|
||||
|
||||
export interface ConversationMember {
|
||||
userId: string;
|
||||
@@ -26,7 +26,7 @@ export interface ConversationSummary {
|
||||
// Caller's per-member preferences.
|
||||
archived: boolean;
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user