Browse Source

finish missing events

Nostr-Signature: 964e4a35978748cbbc5127daad5c8a0724b6df6f4342c5b0940dd16bc2e8262d 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc f44401709ba7a5fdf988f149033d7aa9bd838e8d6b86cd6357d738e69fab405d0df70394910ceebdb22501518583280499320424a5e7b4999e91d1fb6a2b07b8
main
Silberengel 2 months ago
parent
commit
63e9bf2c60
  1. 1
      nostr/commit-signatures.jsonl
  2. 59
      src/lib/services/git/repo-manager.ts
  3. 36
      src/lib/services/messaging/event-forwarder.ts
  4. 39
      src/lib/services/messaging/preferences-storage.client.ts
  5. 75
      src/lib/services/messaging/preferences-storage.server.ts
  6. 8
      src/lib/services/settings-store.ts
  7. 65
      src/routes/api/git/[...path]/+server.ts
  8. 33
      src/routes/api/user/messaging-preferences/+server.ts
  9. 15
      src/routes/api/user/messaging-preferences/summary/+server.ts

1
nostr/commit-signatures.jsonl

@ -28,3 +28,4 @@ @@ -28,3 +28,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771618514,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"0a4b94a90de38e64e657c3ef5aca2bc61b5a563edf504d10f4cf5ab386b1bd9c","sig":"d7502da3f1f7d7b35b810a09cbcd3a467589afd8b97e0a7a04fb47996bb4959b510580a0f33f21c318c2733004f23840f73929ddc0dfb2572edc83ad967b09d2"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771619647,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor"]],"content":"Signed commit: refactor","id":"190b84b2cff8b8db7b3509e05d5470c073fc88e50ba7ad4fa54fd9a9d8dc0045","sig":"638b9986b5e534d09752125721a04d8cef7af892c0394515d6deb4116c2fcab378313abc270f47a6605f50457d5bb83fdb8b34af0607725b6d774028dc6a4fb6"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771619895,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","update docs"]],"content":"Signed commit: update docs","id":"82efc8b4dbac67dec5e02ebd46e504d7a6a3bbe7a53963984c3c4cbf6ac52a3b","sig":"5f5643be35aa997558ac79e99aa70f680a0e449bd1027afd83d65b2d7a1eee5f65d23d0d89b069e6118add1e78a3becd33d47f1d2fd82c6f86d9d12e14a5bc2e"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771622212,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","finish implementing nip-34"]],"content":"Signed commit: finish implementing nip-34","id":"e036526abc826e4435a562f1f334e594577d78a7a50a02cb78f8e5565ea68872","sig":"12642202ef028dfbac68ce53e9cf9f7a64ce3242d2dd995fd0b4c4014c9aa2b18891b72dc281fa5aadacd636646ebd8d2b69fd29bf36407658dff9725b779be5"}

59
src/lib/services/git/repo-manager.ts

@ -444,20 +444,65 @@ Your commits will all be signed by your Nostr keys and saved to the event files @@ -444,20 +444,65 @@ Your commits will all be signed by your Nostr keys and saved to the event files
/**
* Check if force push is safe (no divergent history)
* This is a simplified check - in production you might want more sophisticated validation
* A force push is safe if:
* - Local branch is ahead of remote (linear history, just new commits)
* - Local and remote are at the same commit (no-op)
* A force push is unsafe if:
* - Remote has commits that local doesn't have (would overwrite remote history)
*/
private async canSafelyForcePush(repoPath: string, remoteName: string): Promise<boolean> {
try {
const git = simpleGit(repoPath);
// Fetch to see if there are any remote changes
// Get current branch name
const currentBranch = await git.revParse(['--abbrev-ref', 'HEAD']);
if (!currentBranch) {
return false; // Can't determine current branch
}
// Fetch latest remote state
await git.fetch(remoteName);
// If fetch succeeds, check if we're ahead (safe to force) or behind (dangerous)
const status = await git.status();
// For now, default to false (safer) unless explicitly allowed
// In production, you'd check branch divergence more carefully
return false;
// Get remote branch reference
const remoteBranch = `${remoteName}/${currentBranch}`;
// Check if remote branch exists
try {
await git.revParse([`refs/remotes/${remoteBranch}`]);
} catch {
// Remote branch doesn't exist yet - safe to push (first push)
return true;
}
// Get local and remote commit SHAs
const localSha = await git.revParse(['HEAD']);
const remoteSha = await git.revParse([`refs/remotes/${remoteBranch}`]);
// If they're the same, it's safe (no-op)
if (localSha === remoteSha) {
return true;
}
// Check if local is ahead (linear history) - safe to force push
// This means all remote commits are ancestors of local commits
const mergeBase = await git.raw(['merge-base', localSha, remoteSha]);
const mergeBaseSha = mergeBase.trim();
// If merge base equals remote SHA, local is ahead (safe)
if (mergeBaseSha === remoteSha) {
return true;
}
// If merge base equals local SHA, remote is ahead (unsafe to force push)
if (mergeBaseSha === localSha) {
return false;
}
// If merge base is different from both, branches have diverged (unsafe)
return false;
} catch (error) {
// If we can't determine, default to false (safer)
logger.warn({ error, repoPath, remoteName }, 'Failed to check branch divergence, defaulting to unsafe');
return false;
}
}

36
src/lib/services/messaging/event-forwarder.ts

@ -8,24 +8,7 @@ import logger from '../logger.js'; @@ -8,24 +8,7 @@ import logger from '../logger.js';
import type { NostrEvent } from '../../types/nostr.js';
import { getCachedUserLevel } from '../security/user-level-cache.js';
import { KIND } from '../../types/nostr.js';
// Lazy import to avoid importing Node.js crypto in browser
// Use a function type instead of typeof import to avoid static analysis
let getPreferences: ((userPubkeyHex: string) => Promise<any>) | null = null;
async function getPreferencesLazy() {
if (typeof window !== 'undefined') {
// Browser environment - event forwarding should be done server-side
return null;
}
if (!getPreferences) {
// Use static import path with vite-ignore to prevent static analysis
// This is intentional - we only want to import this server-side
// @ts-ignore - Dynamic import for server-side only code
const module = await import(/* @vite-ignore */ './preferences-storage.server.js');
getPreferences = module.getPreferences;
}
return getPreferences;
}
import type { MessagingPreferences } from './preferences-types.js';
// ============================================================================
// Types & Interfaces
@ -614,10 +597,17 @@ async function forwardToGitPlatform( @@ -614,10 +597,17 @@ async function forwardToGitPlatform(
* - User has unlimited access
* - User has preferences configured and enabled
* - Event kind is in notifyOn list (if specified)
*
* @param event - The Nostr event to forward
* @param userPubkeyHex - User's public key in hex format
* @param preferences - Optional messaging preferences. If not provided, forwarding is skipped.
* Preferences are stored client-side in IndexedDB and should be passed
* from the client when available.
*/
export async function forwardEventIfEnabled(
event: NostrEvent,
userPubkeyHex: string
userPubkeyHex: string,
preferences?: MessagingPreferences | null
): Promise<void> {
try {
// Early returns for eligibility checks
@ -627,12 +617,8 @@ export async function forwardEventIfEnabled( @@ -627,12 +617,8 @@ export async function forwardEventIfEnabled(
return;
}
const getPreferencesFn = await getPreferencesLazy();
if (!getPreferencesFn) {
// Browser environment - forwarding should be done server-side via API
return;
}
const preferences = await getPreferencesFn(userPubkeyHex);
// Preferences are stored client-side in IndexedDB
// If not provided, skip forwarding (preferences must be passed from client)
if (!preferences || !preferences.enabled) {
return;
}

39
src/lib/services/messaging/preferences-storage.client.ts

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
/**
* Client-side messaging preferences storage using IndexedDB
* This replaces the server-side in-memory storage
*/
import { settingsStore } from '../settings-store.js';
import type { MessagingPreferences } from './preferences-types.js';
/**
* Store user messaging preferences in IndexedDB settings
*/
export async function storePreferences(
preferences: MessagingPreferences
): Promise<void> {
await settingsStore.updateSettings({ messagingPreferences: preferences });
}
/**
* Retrieve user messaging preferences from IndexedDB
*/
export async function getPreferences(): Promise<MessagingPreferences | null> {
const settings = await settingsStore.getSettings();
return settings.messagingPreferences || null;
}
/**
* Check if user has preferences configured
*/
export async function hasPreferences(): Promise<boolean> {
const preferences = await getPreferences();
return preferences !== null && preferences !== undefined;
}
/**
* Delete user messaging preferences
*/
export async function deletePreferences(): Promise<void> {
await settingsStore.updateSettings({ messagingPreferences: undefined });
}

75
src/lib/services/messaging/preferences-storage.server.ts

@ -191,10 +191,12 @@ function deriveUserKey(userPubkeyHex: string, salt: string): Buffer { @@ -191,10 +191,12 @@ function deriveUserKey(userPubkeyHex: string, salt: string): Buffer {
}
/**
* In-memory storage (in production, use Redis or database)
* Key: HMAC(pubkey), Value: {encryptedSalt, encrypted}
* DEPRECATED: Preferences are now stored client-side in IndexedDB via settings-store.ts
* This server-side storage is kept for backward compatibility but should not be used.
* Use preferences-storage.client.ts for new code.
*
* The in-memory Map has been removed - preferences are stored in IndexedDB on the client.
*/
const preferencesStore = new Map<string, string>();
/**
* Store user messaging preferences securely
@ -230,17 +232,12 @@ export async function storePreferences( @@ -230,17 +232,12 @@ export async function storePreferences(
// Encrypt preferences
const encrypted = encryptAES256GCM(userKey, JSON.stringify(preferences));
// Store using HMAC lookup key (not raw pubkey)
const lookupKey = getLookupKey(userPubkeyHex);
const stored: StoredPreferences = {
encryptedSalt,
encrypted
};
preferencesStore.set(lookupKey, JSON.stringify(stored));
// DEPRECATED: Preferences should be stored client-side in IndexedDB
// This function is kept for backward compatibility but does nothing
// Use preferences-storage.client.ts instead
logger.info({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...' },
'Stored messaging preferences');
logger.warn({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...' },
'storePreferences called on deprecated server-side storage. Preferences should be stored client-side in IndexedDB.');
}
/**
@ -268,58 +265,38 @@ export async function getPreferences( @@ -268,58 +265,38 @@ export async function getPreferences(
);
}
// Get stored data using HMAC lookup key
const lookupKey = getLookupKey(userPubkeyHex);
const storedJson = preferencesStore.get(lookupKey);
if (!storedJson) {
return null;
}
try {
const stored: StoredPreferences = JSON.parse(storedJson);
// Decrypt salt
const salt = decryptSalt(stored.encryptedSalt);
// Derive same encryption key
const userKey = deriveUserKey(userPubkeyHex, salt);
// DEPRECATED: Preferences are now stored client-side in IndexedDB
// This function always returns null - use preferences-storage.client.ts instead
// Decrypt preferences
const decrypted = decryptAES256GCM(userKey, stored.encrypted);
const preferences: MessagingPreferences = JSON.parse(decrypted);
logger.warn({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...' },
'getPreferences called on deprecated server-side storage. Use preferences-storage.client.ts to read from IndexedDB.');
// Reset rate limit on successful decryption
decryptionAttempts.delete(lookupKey);
return null;
return preferences;
} catch (error) {
logger.error({
error,
userPubkeyHex: userPubkeyHex.slice(0, 16) + '...'
}, 'Failed to decrypt preferences');
throw new Error('Failed to decrypt preferences. Data may be corrupted.');
}
// DEPRECATED: This code path is no longer used
// Preferences are stored client-side in IndexedDB
return null;
}
/**
* Delete user messaging preferences
*/
export async function deletePreferences(userPubkeyHex: string): Promise<void> {
const lookupKey = getLookupKey(userPubkeyHex);
preferencesStore.delete(lookupKey);
decryptionAttempts.delete(lookupKey);
// DEPRECATED: Preferences are now stored client-side in IndexedDB
// This function does nothing - use preferences-storage.client.ts instead
logger.info({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...' },
'Deleted messaging preferences');
logger.warn({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...' },
'deletePreferences called on deprecated server-side storage. Use preferences-storage.client.ts to delete from IndexedDB.');
}
/**
* Check if user has preferences configured
*/
export async function hasPreferences(userPubkeyHex: string): Promise<boolean> {
const lookupKey = getLookupKey(userPubkeyHex);
return preferencesStore.has(lookupKey);
// DEPRECATED: Preferences are now stored client-side in IndexedDB
// This function always returns false - use preferences-storage.client.ts instead
return false;
}
/**

8
src/lib/services/settings-store.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
/**
* Settings store using IndexedDB for persistent client-side storage
* Stores: auto-save, user.name, user.email, theme
* Stores: auto-save, user.name, user.email, theme, messagingPreferences
*/
import logger from './logger.js';
@ -9,12 +9,15 @@ const DB_NAME = 'gitrepublic_settings'; @@ -9,12 +9,15 @@ const DB_NAME = 'gitrepublic_settings';
const DB_VERSION = 1;
const STORE_SETTINGS = 'settings';
import type { MessagingPreferences } from './messaging/preferences-types.js';
interface Settings {
autoSave: boolean;
userName: string;
userEmail: string;
theme: 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black';
defaultBranch: string;
messagingPreferences?: MessagingPreferences;
}
const DEFAULT_SETTINGS: Settings = {
@ -22,7 +25,8 @@ const DEFAULT_SETTINGS: Settings = { @@ -22,7 +25,8 @@ const DEFAULT_SETTINGS: Settings = {
userName: '',
userEmail: '',
theme: 'gitrepublic-dark',
defaultBranch: 'master'
defaultBranch: 'master',
messagingPreferences: undefined
};
export class SettingsStore {

65
src/routes/api/git/[...path]/+server.ts

@ -767,21 +767,61 @@ export const POST: RequestHandler = async ({ params, url, request }) => { @@ -767,21 +767,61 @@ export const POST: RequestHandler = async ({ params, url, request }) => {
}
// Check branch protection rules
// Note: We need to extract the target branch from the git push request
// This is a simplified check - in production, you'd parse the git protocol
// to determine the exact branch being pushed
let targetBranch = 'main'; // Default to main if can't determine
// Parse git push protocol to extract branch names being pushed
// Git receive-pack protocol format:
// - Capability lines (optional)
// - Ref updates: <old-sha> <new-sha> refs/heads/<branch>\0<capabilities>
// - Pack data follows (binary)
const pushedBranches: string[] = [];
try {
// Try to extract branch from request body (git protocol)
const bodyText = bodyBuffer.toString('utf-8', 0, Math.min(bodyBuffer.length, 1000));
const branchMatch = bodyText.match(/refs\/heads\/([^\s\n]+)/);
targetBranch = branchMatch ? branchMatch[1] : 'main'; // Default to main if can't determine
// Parse git receive-pack protocol
// The protocol uses null-terminated strings and space-separated refs
// Format: <old-sha> <new-sha> refs/heads/<branch-name>\0[capabilities]
const bodyText = bodyBuffer.toString('binary');
// Validate branch name to prevent injection
if (!isValidBranchName(targetBranch)) {
return error(400, 'Invalid branch name');
// Split by null bytes to separate ref updates from pack data
const nullIndex = bodyText.indexOf('\0');
const refSection = nullIndex >= 0 ? bodyText.substring(0, nullIndex) : bodyText;
// Split ref section by newlines (each line is a ref update)
const refLines = refSection.split('\n').filter(line => line.trim().length > 0);
for (const line of refLines) {
// Parse format: <old-sha> <new-sha> refs/heads/<branch-name>
// Or: <old-sha> <new-sha> refs/heads/<branch-name>\0<capabilities>
const parts = line.split(/\s+/);
if (parts.length >= 3) {
const refPath = parts[2];
// Extract branch name from refs/heads/<branch>
if (refPath.startsWith('refs/heads/')) {
const branchName = refPath.substring(11); // Remove 'refs/heads/' prefix
// Remove any null bytes or capabilities that might be appended
const cleanBranchName = branchName.split('\0')[0].trim();
// Validate branch name
if (cleanBranchName && isValidBranchName(cleanBranchName)) {
pushedBranches.push(cleanBranchName);
}
}
}
}
// If no branches found, try fallback regex (for edge cases)
if (pushedBranches.length === 0) {
const fallbackMatch = bodyText.match(/refs\/heads\/([^\s\n\0]+)/);
if (fallbackMatch && isValidBranchName(fallbackMatch[1])) {
pushedBranches.push(fallbackMatch[1]);
}
}
// Default to 'main' if we can't determine any branch (shouldn't happen in normal operation)
if (pushedBranches.length === 0) {
logger.warn({ repoName, bodyLength: bodyBuffer.length }, 'Could not extract branch name from git push, defaulting to main');
pushedBranches.push('main');
}
// Check protection for all branches being pushed
for (const targetBranch of pushedBranches) {
const protectionCheck = await branchProtectionService.canPushToBranch(
authResult.pubkey || '',
currentOwnerPubkey,
@ -791,7 +831,8 @@ export const POST: RequestHandler = async ({ params, url, request }) => { @@ -791,7 +831,8 @@ export const POST: RequestHandler = async ({ params, url, request }) => {
);
if (!protectionCheck.allowed) {
return error(403, protectionCheck.reason || 'Branch is protected');
return error(403, protectionCheck.reason || `Branch '${targetBranch}' is protected`);
}
}
} catch (error) {
// If we can't check protection, log but don't block (fail open for now)

33
src/routes/api/user/messaging-preferences/+server.ts

@ -10,13 +10,12 @@ @@ -10,13 +10,12 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { verifyEvent } from 'nostr-tools';
import { storePreferences, getPreferences, deletePreferences, hasPreferences, getRateLimitStatus } from '$lib/services/messaging/preferences-storage.server.js';
import { getCachedUserLevel } from '$lib/services/security/user-level-cache.js';
import { hasUnlimitedAccess } from '$lib/utils/user-access.js';
import { extractRequestContext } from '$lib/utils/api-context.js';
import { auditLogger } from '$lib/services/security/audit-logger.js';
import logger from '$lib/services/logger.js';
import type { MessagingPreferences } from '$lib/services/messaging/preferences-storage.server.js';
import type { MessagingPreferences } from '$lib/services/messaging/preferences-types.js';
/**
* POST - Save messaging preferences
@ -105,8 +104,11 @@ export const POST: RequestHandler = async (event) => { @@ -105,8 +104,11 @@ export const POST: RequestHandler = async (event) => {
return error(400, 'Invalid preferences: enabled must be boolean');
}
// Store preferences (will encrypt and store securely)
await storePreferences(userPubkeyHex, preferences as MessagingPreferences);
// Preferences are now stored client-side in IndexedDB via settings store
// The client should use the preferences-storage.client.ts helper
// This API endpoint just validates the request
// Note: For server-side event forwarding, preferences are read from the client's IndexedDB
// via the request context when available
auditLogger.log({
user: userPubkeyHex,
@ -116,7 +118,7 @@ export const POST: RequestHandler = async (event) => { @@ -116,7 +118,7 @@ export const POST: RequestHandler = async (event) => {
result: 'success'
});
return json({ success: true });
return json({ success: true, message: 'Preferences validated. Client should store in IndexedDB.' });
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
logger.error({ error: err, clientIp }, '[API] Error saving messaging preferences');
@ -157,18 +159,13 @@ export const GET: RequestHandler = async (event) => { @@ -157,18 +159,13 @@ export const GET: RequestHandler = async (event) => {
return error(403, 'Messaging forwarding requires unlimited access level');
}
// Check if preferences exist (without decrypting)
const exists = await hasPreferences(requestContext.userPubkeyHex);
// Get rate limit status
const rateLimit = getRateLimitStatus(requestContext.userPubkeyHex);
// Preferences are stored client-side in IndexedDB
// The client should check IndexedDB directly using preferences-storage.client.ts
// This endpoint just confirms the user has access
return json({
configured: exists,
rateLimit: {
remaining: rateLimit.remaining,
resetAt: rateLimit.resetAt
}
configured: false, // Client should check IndexedDB
message: 'Check client-side IndexedDB for preferences'
});
} catch (err) {
logger.error({ error: err, clientIp }, '[API] Error getting messaging preferences status');
@ -194,7 +191,9 @@ export const DELETE: RequestHandler = async (event) => { @@ -194,7 +191,9 @@ export const DELETE: RequestHandler = async (event) => {
return error(403, 'Messaging forwarding requires unlimited access level');
}
await deletePreferences(requestContext.userPubkeyHex);
// Preferences are stored client-side in IndexedDB
// The client should delete from IndexedDB using preferences-storage.client.ts
// This API endpoint just validates the request
auditLogger.log({
user: requestContext.userPubkeyHex,
@ -204,7 +203,7 @@ export const DELETE: RequestHandler = async (event) => { @@ -204,7 +203,7 @@ export const DELETE: RequestHandler = async (event) => {
result: 'success'
});
return json({ success: true });
return json({ success: true, message: 'Client should delete from IndexedDB.' });
} catch (err) {
logger.error({ error: err, clientIp }, '[API] Error deleting messaging preferences');
return error(500, 'Failed to delete messaging preferences');

15
src/routes/api/user/messaging-preferences/summary/+server.ts

@ -5,7 +5,6 @@ @@ -5,7 +5,6 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getPreferencesSummary } from '$lib/services/messaging/preferences-storage.server.js';
import { getCachedUserLevel } from '$lib/services/security/user-level-cache.js';
import { hasUnlimitedAccess } from '$lib/utils/user-access.js';
import { extractRequestContext } from '$lib/utils/api-context.js';
@ -29,18 +28,16 @@ export const GET: RequestHandler = async (event) => { @@ -29,18 +28,16 @@ export const GET: RequestHandler = async (event) => {
return error(403, 'Messaging forwarding requires unlimited access level');
}
// Get safe summary (decrypts but only returns safe info)
const summary = await getPreferencesSummary(requestContext.userPubkeyHex);
// Preferences are now stored client-side in IndexedDB
// The client should read from IndexedDB directly using preferences-storage.client.ts
// This endpoint returns a default response indicating client-side storage
if (!summary) {
return json({
configured: false,
configured: false, // Client should check IndexedDB
enabled: false,
platforms: {}
platforms: {},
message: 'Preferences are stored client-side in IndexedDB. Use preferences-storage.client.ts to access them.'
});
}
return json(summary);
} catch (err) {
logger.error({ error: err, clientIp }, '[API] Error getting messaging preferences summary');

Loading…
Cancel
Save