From 6b6ea8ffe9a9b3d84a3f9c465f890a892803110b Mon Sep 17 00:00:00 2001 From: secure Date: Thu, 1 May 2025 07:10:47 -0500 Subject: [PATCH 1/6] Improve SDK documentation #377 --- packages/docs/src/integrations/npm-package.md | 506 +++++++++++++++++- 1 file changed, 497 insertions(+), 9 deletions(-) diff --git a/packages/docs/src/integrations/npm-package.md b/packages/docs/src/integrations/npm-package.md index 0ce73efd..a61c9780 100644 --- a/packages/docs/src/integrations/npm-package.md +++ b/packages/docs/src/integrations/npm-package.md @@ -1,29 +1,517 @@ # NPM Package -::: warning -This page is a work in progress. -::: - -The Enclosed lib is available as an npm package. You can use it to create and manage Enclosed notes. +The Enclosed library is available as an npm package. It provides a simple and secure way to create, manage, and access encrypted notes with optional password protection, expiration times, and file attachments. ## Installation -You can install the Enclosed Lib using npm, yarn, or pnpm. +You can install the Enclosed library using npm, yarn, or pnpm. ### Using npm ```bash -npm install -g @enclosed/lib +npm install @enclosed/lib ``` ### Using yarn ```bash -yarn global add @enclosed/lib +yarn add @enclosed/lib ``` ### Using pnpm ```bash -pnpm add -g @enclosed/lib +pnpm add @enclosed/lib +``` + +## Basic Usage + +### Creating a Note + +```typescript +import { createNote } from '@enclosed/lib'; + +async function createSimpleNote() { + const { noteUrl } = await createNote({ + content: 'This is a secret note', + }); + + console.log(`Your note is available at: ${noteUrl}`); +} +``` + +### Reading a Note + +```typescript +import { decryptNote } from '@enclosed/lib'; + +async function readNote(noteUrl, password) { + // Parse the URL to extract noteId and encryptionKey + const { noteId, encryptionKey } = parseNoteUrl({ noteUrl }); + + // Fetch the encrypted note from the server + const { payload } = await fetchNote({ noteId }); + + // Decrypt the note + const { note } = await decryptNote({ + encryptedPayload: payload, + encryptionKey, + password, // Optional, only needed if the note is password-protected + }); + + console.log('Note content:', note.content); + + // If the note has file attachments + if (note.assets.length > 0) { + console.log('Note has file attachments:', note.assets.length); + } +} +``` + +## API Reference + +### createNote + +Creates an encrypted note and stores it on the server. + +```typescript +async function createNote({ + content, + password, + ttlInSeconds, + deleteAfterReading = false, + clientBaseUrl = 'https://enclosed.cc', + apiBaseUrl = clientBaseUrl, + storeNote = params => storeNoteImpl({ ...params, apiBaseUrl }), + assets = [], + encryptionAlgorithm = 'aes-256-gcm', + serializationFormat = 'cbor-array', + isPublic = true, + pathPrefix, +}: { + content: string; + password?: string; + ttlInSeconds?: number; + deleteAfterReading?: boolean; + clientBaseUrl?: string; + apiBaseUrl?: string; + assets?: NoteAsset[]; + encryptionAlgorithm?: EncryptionAlgorithm; + serializationFormat?: SerializationFormat; + isPublic?: boolean; + pathPrefix?: string; + storeNote?: (params: { + payload: string; + ttlInSeconds?: number; + deleteAfterReading: boolean; + encryptionAlgorithm: EncryptionAlgorithm; + serializationFormat: SerializationFormat; + isPublic?: boolean; + }) => Promise<{ noteId: string }>; +}): Promise<{ + encryptedPayload: string; + encryptionKey: string; + noteId: string; + noteUrl: string; +}> +``` + +#### Parameters + +- `content` (required): The content of the note as a string. +- `password` (optional): A password to protect the note. If provided, the password will be required to decrypt the note. +- `ttlInSeconds` (optional): Time-to-live in seconds. The note will be automatically deleted after this time. +- `deleteAfterReading` (optional, default: `false`): If `true`, the note will be deleted after it's read once. +- `clientBaseUrl` (optional, default: `'https://enclosed.cc'`): The base URL for the client. +- `apiBaseUrl` (optional, default: `clientBaseUrl`): The base URL for the API. +- `assets` (optional, default: `[]`): An array of file assets to attach to the note. +- `encryptionAlgorithm` (optional, default: `'aes-256-gcm'`): The encryption algorithm to use. +- `serializationFormat` (optional, default: `'cbor-array'`): The serialization format to use. +- `isPublic` (optional, default: `true`): If `true`, the note is publicly accessible with the correct URL. +- `pathPrefix` (optional): A prefix for the note URL path. +- `storeNote` (optional): A custom function to store the note. By default, it uses the built-in `storeNote` function. + +#### Returns + +- `encryptedPayload`: The encrypted note payload. +- `encryptionKey`: The encryption key used to encrypt the note. +- `noteId`: The ID of the created note. +- `noteUrl`: The URL to access the note. + +### decryptNote + +Decrypts an encrypted note. + +```typescript +async function decryptNote({ + encryptedPayload, + password, + encryptionKey, + serializationFormat = 'cbor-array', + encryptionAlgorithm = 'aes-256-gcm', +}: { + encryptedPayload: string; + password?: string; + encryptionKey: string; + serializationFormat?: SerializationFormat; + encryptionAlgorithm?: EncryptionAlgorithm; +}): Promise<{ + note: Note; +}> +``` + +#### Parameters + +- `encryptedPayload` (required): The encrypted note payload. +- `password` (optional): The password used to encrypt the note, if any. +- `encryptionKey` (required): The encryption key used to encrypt the note. +- `serializationFormat` (optional, default: `'cbor-array'`): The serialization format used. +- `encryptionAlgorithm` (optional, default: `'aes-256-gcm'`): The encryption algorithm used. + +#### Returns + +- `note`: The decrypted note object containing the content and any attached assets. + +### fetchNote + +Fetches an encrypted note from the server. + +```typescript +async function fetchNote({ + noteId, + apiBaseUrl, +}: { + noteId: string; + apiBaseUrl?: string; +}): Promise<{ + payload: string; +}> +``` + +#### Parameters + +- `noteId` (required): The ID of the note to fetch. +- `apiBaseUrl` (optional): The base URL for the API. + +#### Returns + +- An object containing the encrypted note payload. + +### URL Handling Functions + +#### createNoteUrl + +Creates a URL for accessing a note. + +```typescript +function createNoteUrl({ + noteId, + encryptionKey, + clientBaseUrl, + isPasswordProtected, + isDeletedAfterReading, + pathPrefix, +}: { + noteId: string; + encryptionKey: string; + clientBaseUrl: string; + isPasswordProtected?: boolean; + isDeletedAfterReading?: boolean; + pathPrefix?: string; +}): { noteUrl: string } +``` + +#### parseNoteUrl + +Parses a note URL to extract the noteId, encryptionKey, and other information. + +```typescript +function parseNoteUrl({ noteUrl }: { noteUrl: string }): { + noteId: string; + encryptionKey: string; + isPasswordProtected: boolean; + isDeletedAfterReading: boolean; +} +``` + +### File Handling Functions + +#### fileToNoteAsset + +Converts a File object to a NoteAsset. + +```typescript +async function fileToNoteAsset({ file }: { file: File }): Promise +``` + +#### filesToNoteAssets + +Converts an array of File objects to an array of NoteAssets. + +```typescript +async function filesToNoteAssets({ files }: { files: File[] }): Promise +``` + +#### noteAssetToFile + +Converts a NoteAsset to a File object. + +```typescript +async function noteAssetToFile({ noteAsset }: { noteAsset: NoteAsset }): Promise +``` + +#### noteAssetsToFiles + +Converts an array of NoteAssets to an array of File objects. + +```typescript +async function noteAssetsToFiles({ noteAssets }: { noteAssets: NoteAsset[] }): Promise +``` + +## TypeScript Types and Interfaces + +The library exports several TypeScript types and interfaces that you can use in your code. + +### Note Types + +```typescript +// Represents a file or other binary asset attached to a note +type NoteAsset = { + metadata: { + type: string; + [key: string]: unknown; + }; + content: Uint8Array; +}; + +// Represents a decrypted note +type Note = { + content: string; + assets: NoteAsset[]; +}; + +// Represents an encrypted note +type EncryptedNote = { + version: number; + payload: string; + encryptionAlgorithm: EncryptionAlgorithm; + serializationFormat: SerializationFormat; + keyDerivationAlgorithm: KeyDerivationAlgorithm; + compressionAlgorithm: CompressionAlgorithm; + ttlInSeconds: number; + deleteAfterReading: boolean; +}; +``` + +### Encryption and Serialization Types + +```typescript +// Available encryption algorithms +type EncryptionAlgorithm = 'aes-256-gcm'; + +// Available key derivation algorithms +type KeyDerivationAlgorithm = 'pbkdf2-base-key-salted'; + +// Available compression algorithms +type CompressionAlgorithm = 'brotli' | 'none'; + +// Available serialization formats +type SerializationFormat = 'cbor-array'; +``` + +## Advanced Usage + +### Creating a Password-Protected Note + +```typescript +import { createNote } from '@enclosed/lib'; + +async function createPasswordProtectedNote() { + const { noteUrl } = await createNote({ + content: 'This is a password-protected note', + password: 'my-secure-password', + }); + + console.log(`Your password-protected note is available at: ${noteUrl}`); +} +``` + +### Creating a Note with Expiration + +```typescript +import { createNote } from '@enclosed/lib'; + +async function createExpiringNote() { + // Create a note that expires after 1 hour (3600 seconds) + const { noteUrl } = await createNote({ + content: 'This note will expire after 1 hour', + ttlInSeconds: 3600, + }); + + console.log(`Your expiring note is available at: ${noteUrl}`); +} +``` + +### Creating a Note that Deletes After Reading + +```typescript +import { createNote } from '@enclosed/lib'; + +async function createSelfDestructingNote() { + const { noteUrl } = await createNote({ + content: 'This note will self-destruct after reading', + deleteAfterReading: true, + }); + + console.log(`Your self-destructing note is available at: ${noteUrl}`); +} +``` + +### Creating a Note with File Attachments + +```typescript +import { createNote, filesToNoteAssets } from '@enclosed/lib'; + +async function createNoteWithAttachments() { + // Assuming you have File objects from a file input or other source + const files = [ + new File(['file content'], 'document.txt', { type: 'text/plain' }), + // Add more files as needed + ]; + + // Convert files to note assets + const assets = await filesToNoteAssets({ files }); + + const { noteUrl } = await createNote({ + content: 'This note has file attachments', + assets, + }); + + console.log(`Your note with attachments is available at: ${noteUrl}`); +} +``` + +### Using a Custom API Endpoint + +```typescript +import { createNote } from '@enclosed/lib'; + +async function createNoteWithCustomEndpoint() { + const { noteUrl } = await createNote({ + content: 'This note uses a custom API endpoint', + apiBaseUrl: 'https://my-custom-enclosed-instance.com', + clientBaseUrl: 'https://my-custom-enclosed-instance.com', + }); + + console.log(`Your note is available at: ${noteUrl}`); +} ``` + +## Error Handling + +The library throws errors in various situations. Here's how to handle them: + +```typescript +import { createNote, isApiClientErrorWithCode, isApiClientErrorWithStatusCode } from '@enclosed/lib'; + +async function createNoteWithErrorHandling() { + try { + const { noteUrl } = await createNote({ + content: 'This is a note with error handling', + }); + + console.log(`Your note is available at: ${noteUrl}`); + } catch (error) { + // Check if it's a rate limit error + if (isApiClientErrorWithStatusCode({ error, statusCode: 429 })) { + console.error('Rate limit exceeded. Try again later.'); + return; + } + + // Check if it's a specific API error + if (isApiClientErrorWithCode({ error, code: 'NOTE_TOO_LARGE' })) { + console.error('The note is too large. Please reduce its size.'); + return; + } + + // Handle other errors + console.error('An error occurred:', error); + } +} +``` + +## Complete Examples + +### Creating and Reading a Note + +```typescript +import { createNote, decryptNote, fetchNote, parseNoteUrl } from '@enclosed/lib'; + +async function createAndReadNote() { + // Create a note + const { noteUrl } = await createNote({ + content: 'This is a complete example note', + }); + + console.log(`Note created at: ${noteUrl}`); + + // Parse the URL to extract noteId and encryptionKey + const { noteId, encryptionKey } = parseNoteUrl({ noteUrl }); + + // Fetch the encrypted note from the server + const note = await fetchNote({ noteId }); + + // Decrypt the note + const { note: decryptedNote } = await decryptNote({ + encryptedPayload: note.payload, + encryptionKey, + }); + + console.log('Decrypted note content:', decryptedNote.content); +} +``` + +### Creating and Reading a Password-Protected Note with File Attachments + +```typescript +import { createNote, decryptNote, fetchNote, parseNoteUrl, filesToNoteAssets, noteAssetsToFiles } from '@enclosed/lib'; + +async function createAndReadComplexNote() { + // Prepare file attachments + const files = [ + new File(['file content'], 'document.txt', { type: 'text/plain' }), + ]; + + const assets = await filesToNoteAssets({ files }); + + // Create a password-protected note with attachments that expires after 1 day + const { noteUrl } = await createNote({ + content: 'This is a complex note example', + password: 'secure-password', + assets, + ttlInSeconds: 86400, // 1 day + }); + + console.log(`Complex note created at: ${noteUrl}`); + + // Parse the URL to extract noteId and encryptionKey + const { noteId, encryptionKey, isPasswordProtected } = parseNoteUrl({ noteUrl }); + + // Fetch the encrypted note from the server + const note = await fetchNote({ noteId }); + + // Decrypt the note with password + const { note: decryptedNote } = await decryptNote({ + encryptedPayload: note.payload, + encryptionKey, + password: isPasswordProtected ? 'secure-password' : undefined, + }); + + console.log('Decrypted note content:', decryptedNote.content); + + // Handle file attachments + if (decryptedNote.assets.length > 0) { + const attachedFiles = await noteAssetsToFiles({ noteAssets: decryptedNote.assets }); + console.log('Attached files:', attachedFiles.map(file => file.name)); + } +} From 27ccb6fc543f561822c94060a9516a82c38d3814 Mon Sep 17 00:00:00 2001 From: secure Date: Thu, 1 May 2025 07:23:58 -0500 Subject: [PATCH 2/6] add legal docs --- packages/docs/src/index.md | 8 +++ packages/docs/src/legal/privacy-policy.md | 60 +++++++++++++++++++++++ packages/docs/src/legal/terms-of-use.md | 0 3 files changed, 68 insertions(+) create mode 100644 packages/docs/src/legal/privacy-policy.md create mode 100644 packages/docs/src/legal/terms-of-use.md diff --git a/packages/docs/src/index.md b/packages/docs/src/index.md index 22c42dec..ae8b195d 100644 --- a/packages/docs/src/index.md +++ b/packages/docs/src/index.md @@ -33,3 +33,11 @@ By leveraging client-side encryption and a zero-knowledge server, Enclosed guara ## Get Started Ready to start using Enclosed? You can [try it out online](https://enclosed.cc) or [self-host](./self-hosting/docker) your instance for maximum control. Dive into our documentation to learn more about how Enclosed works and how you can take full advantage of its features. + +## Disclaimer + +**Enclosed is provided "as is", without warranty of any kind, express or implied.** The creators and contributors of Enclosed are not responsible for the content of any notes created or shared using the service, or for any actions taken by users based on such content. Users are solely responsible for their use of the service and any content they create, share, or access. + +If you choose to self-host an instance of Enclosed, you do so at your own risk. The creators and contributors are not responsible for any issues, security breaches, or other problems that may arise from self-hosting. + +For more detailed information, please review our [Privacy Policy](./legal/privacy-policy.md) and [Terms of Use](./legal/terms-of-use.md). diff --git a/packages/docs/src/legal/privacy-policy.md b/packages/docs/src/legal/privacy-policy.md new file mode 100644 index 00000000..05687ff2 --- /dev/null +++ b/packages/docs/src/legal/privacy-policy.md @@ -0,0 +1,60 @@ +# Privacy Policy + +Last Updated: May 1, 2025 + +## Introduction + +Welcome to Enclosed. We respect your privacy and are committed to protecting your personal data. This Privacy Policy explains how we collect, use, and safeguard your information when you use our service. + +Enclosed is designed with privacy as a core principle. Our application uses end-to-end encryption, meaning that the content of your notes is encrypted on your device before being sent to our servers, and can only be decrypted by someone with the correct link and password (if set). + +## Information We Collect + +### Information You Provide + +- **Note Content**: The content of your notes is encrypted on your device before being sent to our servers. We cannot access the unencrypted content of your notes. +- **File Attachments**: Any files you attach to notes are also encrypted on your device before being sent to our servers. +- **Authentication Information**: If you choose to create an account, we collect your email address and a hashed version of your password. + +### Information Collected Automatically + +- **Usage Data**: We collect information about how you interact with our service, such as access dates and times, pages viewed, and the features you use. +- **Device Information**: We collect information about the device you use to access our service, including IP address, browser type, and operating system. + +## How We Use Your Information + +We use the information we collect to: + +- Provide, maintain, and improve our service +- Detect, prevent, and address technical issues +- Monitor the usage of our service +- Protect against unauthorized access to our servers + +## Data Storage and Security + +- **Note Content and Attachments**: These are stored in an encrypted format on our servers and are automatically deleted after the expiration period you set, or after being read (if you selected that option). +- **Authentication Information**: If you create an account, your email and password hash are stored securely. + +We implement appropriate technical and organizational measures to protect your data against unauthorized access, alteration, disclosure, or destruction. + +## Your Rights + +Depending on your location, you may have certain rights regarding your personal information, such as: + +- The right to access the personal information we hold about you +- The right to request correction or deletion of your personal information +- The right to restrict or object to our processing of your personal information +- The right to data portability + +To exercise these rights, please contact us using the information provided below. + +## Changes to This Privacy Policy + +We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last Updated" date. + +## Contact Us + +If you have any questions about this Privacy Policy, please contact us: + +- By email: [contact@enclosed.cc](mailto:contact@enclosed.cc) +- By visiting our GitHub repository: [https://github.com/CorentinTh/enclosed](https://github.com/CorentinTh/enclosed) \ No newline at end of file diff --git a/packages/docs/src/legal/terms-of-use.md b/packages/docs/src/legal/terms-of-use.md new file mode 100644 index 00000000..e69de29b From 17201dd2c608863cb9c9c21a72d89379709e1d8b Mon Sep 17 00:00:00 2001 From: secure Date: Thu, 1 May 2025 08:27:04 -0500 Subject: [PATCH 3/6] increased timeout from 5s to 30s --- packages/app-server/src/modules/app/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-server/src/modules/app/config/config.ts b/packages/app-server/src/modules/app/config/config.ts index 63da50c3..33276dcd 100644 --- a/packages/app-server/src/modules/app/config/config.ts +++ b/packages/app-server/src/modules/app/config/config.ts @@ -19,7 +19,7 @@ export const configDefinition = { routeTimeoutMs: { doc: 'The maximum time in milliseconds for a route to complete before timing out', schema: z.coerce.number().int().positive(), - default: 5_000, + default: 30_000, // Increased from 5s to 30s to accommodate larger file uploads env: 'SERVER_API_ROUTES_TIMEOUT_MS', }, corsOrigins: { From 8513a9e8f206f762fa0493f96044a3c5091e6119 Mon Sep 17 00:00:00 2001 From: secure Date: Thu, 1 May 2025 10:44:43 -0500 Subject: [PATCH 4/6] added chacha20 encryption --- packages/app-client/src/index.tsx | 24 ++-- packages/app-client/src/locales/en.json | 13 +- .../EncryptionAlgorithmSelector.tsx | 57 ++++++++ .../src/modules/config/config.provider.tsx | 133 ++++++++++++++++-- .../src/modules/config/config.test.ts | 40 ++++++ .../src/modules/config/pages/SettingsPage.tsx | 28 ++++ .../src/modules/ui/layouts/app.layout.tsx | 5 + packages/app-client/src/routes.tsx | 5 + 8 files changed, 281 insertions(+), 24 deletions(-) create mode 100644 packages/app-client/src/modules/config/components/EncryptionAlgorithmSelector.tsx create mode 100644 packages/app-client/src/modules/config/config.test.ts create mode 100644 packages/app-client/src/modules/config/pages/SettingsPage.tsx diff --git a/packages/app-client/src/index.tsx b/packages/app-client/src/index.tsx index 68412ea6..f154426a 100644 --- a/packages/app-client/src/index.tsx +++ b/packages/app-client/src/index.tsx @@ -5,6 +5,7 @@ import { Router } from '@solidjs/router'; import { render, Suspense } from 'solid-js/web'; import { I18nProvider } from './modules/i18n/i18n.provider'; import { NoteContextProvider } from './modules/notes/notes.context'; +import { ConfigProvider } from './modules/config/config.provider'; import { Toaster } from './modules/ui/components/sonner'; import { getRoutes } from './routes'; import '@unocss/reset/tailwind.css'; @@ -23,17 +24,18 @@ render( root={props => ( - - - -
{props.children}
- - -
-
+ + + + +
{props.children}
+ +
+
+
)} diff --git a/packages/app-client/src/locales/en.json b/packages/app-client/src/locales/en.json index de370e20..bf07eaa2 100644 --- a/packages/app-client/src/locales/en.json +++ b/packages/app-client/src/locales/en.json @@ -28,7 +28,18 @@ "support": "Support Enclosed", "report-bug": "Report a bug", "logout": "Logout", - "contribute-to-i18n": "Contribute to i18n" + "contribute-to-i18n": "Contribute to i18n", + "title": "Settings", + "security": "Security Settings" + }, + "config": { + "encryptionAlgorithm": "Encryption Algorithm", + "aes256gcm": "AES-256-GCM", + "chacha20poly1305": "ChaCha20-Poly1305", + "default": "Default", + "httpCompatible": "HTTP Compatible", + "aes256gcmDescription": "Standard encryption algorithm with broad browser support.", + "chacha20poly1305Description": "High-performance encryption that works on HTTP pages and devices without AES hardware acceleration." } }, "footer": { diff --git a/packages/app-client/src/modules/config/components/EncryptionAlgorithmSelector.tsx b/packages/app-client/src/modules/config/components/EncryptionAlgorithmSelector.tsx new file mode 100644 index 00000000..ad1dfdf5 --- /dev/null +++ b/packages/app-client/src/modules/config/components/EncryptionAlgorithmSelector.tsx @@ -0,0 +1,57 @@ +import { createEffect, Show } from 'solid-js'; +import { useI18n } from '@/modules/i18n/i18n.provider'; +import { useConfig, AES_256_GCM, CHACHA20_POLY1305, EncryptionAlgorithm } from '../config.provider'; + +interface EncryptionAlgorithmSelectorProps { + class?: string; +} + +/** + * Component that allows users to select their preferred encryption algorithm + */ +export function EncryptionAlgorithmSelector(props: EncryptionAlgorithmSelectorProps) { + const { t } = useI18n(); + const { getEncryptionAlgorithm, setEncryptionAlgorithm } = useConfig(); + + // Handle algorithm change + const handleChange = (e: Event) => { + const target = e.target as HTMLSelectElement; + const newAlgorithm = target.value as EncryptionAlgorithm; + setEncryptionAlgorithm(newAlgorithm); + }; + + return ( +
+ + + + + + {t('navbar.config.aes256gcmDescription')} +

+ } + > +

+ {t('navbar.config.chacha20poly1305Description')} +

+
+
+ ); +} \ No newline at end of file diff --git a/packages/app-client/src/modules/config/config.provider.tsx b/packages/app-client/src/modules/config/config.provider.tsx index f637b899..577c8b9e 100644 --- a/packages/app-client/src/modules/config/config.provider.tsx +++ b/packages/app-client/src/modules/config/config.provider.tsx @@ -1,18 +1,127 @@ -import type { Config } from './config.types'; -import { get } from 'lodash-es'; -import { buildTimeConfig } from './config.constants'; +import { createContext, createEffect, useContext } from 'solid-js'; +import { makePersisted } from '@solid-primitives/storage'; +import { createSignal, ParentComponent } from 'solid-js'; -export { - getConfig, -}; +// Define encryption algorithm constants +export const AES_256_GCM = 'aes-256-gcm'; +export const CHACHA20_POLY1305 = 'chacha20-poly1305'; + +// Configuration object type +export interface Config { + viewNotePathPrefix: string; + isAuthenticationRequired: boolean; + preferredEncryptionAlgorithm: EncryptionAlgorithm; +} -function getConfig(): Config { - const runtimeConfig: Partial = get(window, '__CONFIG__', {}); +// Default configuration +const defaultConfig: Config = { + viewNotePathPrefix: 'n', + isAuthenticationRequired: false, + preferredEncryptionAlgorithm: AES_256_GCM, +}; - const config: Config = { - ...buildTimeConfig, - ...runtimeConfig, +// Get the application configuration +export function getConfig(): Config { + try { + // Try to get the config from the context if we're in a component + const context = useContext(ConfigContext); + if (context) { + return context.config; + } + } catch (error) { + // If we're not in a component context, continue with the fallback + } + + // Fallback: Try to get the preferred encryption algorithm from localStorage + let preferredEncryptionAlgorithm = AES_256_GCM; + + try { + const storedAlgorithm = localStorage.getItem('enclosed_encryption_algorithm'); + if (storedAlgorithm && (storedAlgorithm === AES_256_GCM || storedAlgorithm === CHACHA20_POLY1305)) { + preferredEncryptionAlgorithm = storedAlgorithm as EncryptionAlgorithm; + } + } catch (error) { + console.error('Failed to read encryption algorithm from localStorage:', error); + } + + return { + ...defaultConfig, + preferredEncryptionAlgorithm: preferredEncryptionAlgorithm as EncryptionAlgorithm, }; +} + +// Define the encryption algorithm type +export type EncryptionAlgorithm = typeof AES_256_GCM | typeof CHACHA20_POLY1305; - return config; +// Define the configuration context type +interface ConfigContextType { + getEncryptionAlgorithm: () => EncryptionAlgorithm; + setEncryptionAlgorithm: (algorithm: EncryptionAlgorithm) => void; + supportedEncryptionAlgorithms: readonly EncryptionAlgorithm[]; + config: Config; } + +// Create the context with a default undefined value +const ConfigContext = createContext(undefined); + +// Hook to use the config context +export function useConfig() { + const context = useContext(ConfigContext); + + if (!context) { + throw new Error('useConfig must be used within a ConfigProvider'); + } + + return context; +} + +// Configuration provider component +export const ConfigProvider: ParentComponent = (props) => { + // Create a persisted signal for the encryption algorithm + const [getEncryptionAlgorithm, setEncryptionAlgorithm] = makePersisted( + createSignal(AES_256_GCM), + { name: 'enclosed_encryption_algorithm', storage: localStorage } + ); + + // List of supported encryption algorithms + const supportedEncryptionAlgorithms = [AES_256_GCM, CHACHA20_POLY1305] as const; + + // Validate the stored algorithm on load + createEffect(() => { + const currentAlgorithm = getEncryptionAlgorithm(); + const isValidAlgorithm = supportedEncryptionAlgorithms.includes(currentAlgorithm as EncryptionAlgorithm); + + if (!isValidAlgorithm) { + setEncryptionAlgorithm(AES_256_GCM); + } + }); + + // Create a reactive config object that updates when the encryption algorithm changes + const [config, setConfig] = createSignal({ + ...defaultConfig, + preferredEncryptionAlgorithm: getEncryptionAlgorithm() + }); + + // Update config when encryption algorithm changes + createEffect(() => { + const algorithm = getEncryptionAlgorithm(); + setConfig(prev => ({ + ...prev, + preferredEncryptionAlgorithm: algorithm + })); + }); + + // Create the context value + const contextValue: ConfigContextType = { + getEncryptionAlgorithm, + setEncryptionAlgorithm, + supportedEncryptionAlgorithms, + config: config() + }; + + return ( + + {props.children} + + ); +}; diff --git a/packages/app-client/src/modules/config/config.test.ts b/packages/app-client/src/modules/config/config.test.ts new file mode 100644 index 00000000..5c237cf5 --- /dev/null +++ b/packages/app-client/src/modules/config/config.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { AES_256_GCM, CHACHA20_POLY1305, getConfig } from './config.provider'; + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {}; + + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { store[key] = value; }, + clear: () => { store = {}; }, + removeItem: (key: string) => { delete store[key]; }, + }; +})(); + +// Mock the window.localStorage +Object.defineProperty(window, 'localStorage', { value: localStorageMock }); + +describe('Config Provider', () => { + beforeEach(() => { + localStorageMock.clear(); + }); + + test('getConfig returns default encryption algorithm when none is set', () => { + const config = getConfig(); + expect(config.preferredEncryptionAlgorithm).toBe(AES_256_GCM); + }); + + test('getConfig returns ChaCha20-Poly1305 when set in localStorage', () => { + localStorageMock.setItem('enclosed_encryption_algorithm', CHACHA20_POLY1305); + const config = getConfig(); + expect(config.preferredEncryptionAlgorithm).toBe(CHACHA20_POLY1305); + }); + + test('getConfig ignores invalid encryption algorithm in localStorage', () => { + localStorageMock.setItem('enclosed_encryption_algorithm', 'invalid-algorithm'); + const config = getConfig(); + expect(config.preferredEncryptionAlgorithm).toBe(AES_256_GCM); + }); +}); \ No newline at end of file diff --git a/packages/app-client/src/modules/config/pages/SettingsPage.tsx b/packages/app-client/src/modules/config/pages/SettingsPage.tsx new file mode 100644 index 00000000..ee19915c --- /dev/null +++ b/packages/app-client/src/modules/config/pages/SettingsPage.tsx @@ -0,0 +1,28 @@ +import { useI18n } from '@/modules/i18n/i18n.provider'; +import { EncryptionAlgorithmSelector } from '../components/EncryptionAlgorithmSelector'; + +/** + * Settings page component that includes configuration options + */ +export function SettingsPage() { + const { t } = useI18n(); + + return ( +
+

{t('navbar.settings.title')}

+ +
+

{t('navbar.settings.security')}

+ +
+ {/* Encryption Algorithm Selector */} + + + {/* Other security settings can be added here */} +
+
+ + {/* Additional settings sections can be added here */} +
+ ); +} \ No newline at end of file diff --git a/packages/app-client/src/modules/ui/layouts/app.layout.tsx b/packages/app-client/src/modules/ui/layouts/app.layout.tsx index f2a8a022..1733efbb 100644 --- a/packages/app-client/src/modules/ui/layouts/app.layout.tsx +++ b/packages/app-client/src/modules/ui/layouts/app.layout.tsx @@ -167,6 +167,11 @@ export const Navbar: Component = () => { {/* Default items */} + +
+ {t('navbar.settings.title')} +
+
{t('navbar.settings.documentation')} diff --git a/packages/app-client/src/routes.tsx b/packages/app-client/src/routes.tsx index 31723d2b..ccda06cc 100644 --- a/packages/app-client/src/routes.tsx +++ b/packages/app-client/src/routes.tsx @@ -1,6 +1,7 @@ import { A, type RouteDefinition } from '@solidjs/router'; import { LoginPage } from './modules/auth/pages/login.page'; import { getConfig } from './modules/config/config.provider'; +import { SettingsPage } from './modules/config/pages/SettingsPage'; import { NOTE_ID_REGEX } from './modules/notes/notes.constants'; import { buildViewNotePagePath } from './modules/notes/notes.models'; import { CreateNotePage } from './modules/notes/pages/create-note.page'; @@ -29,6 +30,10 @@ export function getRoutes(): RouteDefinition[] { noteId: NOTE_ID_REGEX, }, }, + { + path: '/settings', + component: SettingsPage, + }, { path: '*404', component: () => ( From f62d1d6beb7b5be253d7a11de8cfcfd08a6c0ba7 Mon Sep 17 00:00:00 2001 From: secure Date: Thu, 1 May 2025 11:03:41 -0500 Subject: [PATCH 5/6] addressed sonarqube issues --- .../components/EncryptionAlgorithmSelector.tsx | 6 +++--- .../src/modules/config/config.provider.tsx | 17 +++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/app-client/src/modules/config/components/EncryptionAlgorithmSelector.tsx b/packages/app-client/src/modules/config/components/EncryptionAlgorithmSelector.tsx index ad1dfdf5..2a1af01c 100644 --- a/packages/app-client/src/modules/config/components/EncryptionAlgorithmSelector.tsx +++ b/packages/app-client/src/modules/config/components/EncryptionAlgorithmSelector.tsx @@ -1,9 +1,9 @@ -import { createEffect, Show } from 'solid-js'; +import { Show } from 'solid-js'; import { useI18n } from '@/modules/i18n/i18n.provider'; import { useConfig, AES_256_GCM, CHACHA20_POLY1305, EncryptionAlgorithm } from '../config.provider'; interface EncryptionAlgorithmSelectorProps { - class?: string; + readonly class?: string; } /** @@ -21,7 +21,7 @@ export function EncryptionAlgorithmSelector(props: EncryptionAlgorithmSelectorPr }; return ( -
+
diff --git a/packages/app-client/src/modules/config/config.provider.tsx b/packages/app-client/src/modules/config/config.provider.tsx index 577c8b9e..ffe759be 100644 --- a/packages/app-client/src/modules/config/config.provider.tsx +++ b/packages/app-client/src/modules/config/config.provider.tsx @@ -1,6 +1,5 @@ -import { createContext, createEffect, useContext } from 'solid-js'; +import { createContext, createEffect, useContext, createSignal, ParentComponent, createMemo } from 'solid-js'; import { makePersisted } from '@solid-primitives/storage'; -import { createSignal, ParentComponent } from 'solid-js'; // Define encryption algorithm constants export const AES_256_GCM = 'aes-256-gcm'; @@ -30,6 +29,7 @@ export function getConfig(): Config { } } catch (error) { // If we're not in a component context, continue with the fallback + console.debug('Not in a component context, using fallback config'); } // Fallback: Try to get the preferred encryption algorithm from localStorage @@ -41,7 +41,12 @@ export function getConfig(): Config { preferredEncryptionAlgorithm = storedAlgorithm as EncryptionAlgorithm; } } catch (error) { + // This can happen in environments where localStorage is not available or restricted + // (e.g., incognito mode, some browser settings, or server-side rendering) console.error('Failed to read encryption algorithm from localStorage:', error); + + // Fall back to default encryption algorithm + preferredEncryptionAlgorithm = AES_256_GCM; } return { @@ -111,16 +116,16 @@ export const ConfigProvider: ParentComponent = (props) => { })); }); - // Create the context value - const contextValue: ConfigContextType = { + // Create the context value with createMemo to prevent it from changing on every render + const contextValue = createMemo(() => ({ getEncryptionAlgorithm, setEncryptionAlgorithm, supportedEncryptionAlgorithms, config: config() - }; + })); return ( - + {props.children} ); From e7bbfdb5b553ca42e49c6fe061d02d619841ecae Mon Sep 17 00:00:00 2001 From: secure Date: Thu, 1 May 2025 11:10:36 -0500 Subject: [PATCH 6/6] minor config correction --- .../src/modules/config/config.provider.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/app-client/src/modules/config/config.provider.tsx b/packages/app-client/src/modules/config/config.provider.tsx index ffe759be..bd402e60 100644 --- a/packages/app-client/src/modules/config/config.provider.tsx +++ b/packages/app-client/src/modules/config/config.provider.tsx @@ -21,15 +21,11 @@ const defaultConfig: Config = { // Get the application configuration export function getConfig(): Config { - try { - // Try to get the config from the context if we're in a component - const context = useContext(ConfigContext); - if (context) { - return context.config; - } - } catch (error) { - // If we're not in a component context, continue with the fallback - console.debug('Not in a component context, using fallback config'); + // We don't need to catch exceptions here since useContext doesn't throw + // and we're just checking if we're in a component context + const context = useContext(ConfigContext); + if (context) { + return context.config; } // Fallback: Try to get the preferred encryption algorithm from localStorage