Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,14 @@ target/

# Ralph backup directories (created by migration)
.ralph_backup_*

# Coverage reports
ui/coverage/
api/.coverage

# TypeScript build info
*.tsbuildinfo

# Environment variables (secrets)
.env
!.env.template
52 changes: 52 additions & 0 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,63 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Share secrets securely. End-to-end encrypted, zero-knowledge, one-time viewing. Your data never touches our servers unencrypted." />
<meta name="theme-color" content="#0a0e1a" />
<link rel="canonical" href="https://ooshare.io/" />

<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://ooshare.io/" />
<meta property="og:title" content="Only Once Share — Secure One-Time Secret Sharing" />
<meta property="og:description" content="Share passwords, API keys, and sensitive data with end-to-end encryption. Zero-knowledge, single-use links that self-destruct after viewing." />
<meta property="og:image" content="https://ooshare.io/og-image.png" />
<meta property="og:site_name" content="Only Once Share" />
Comment on lines +8 to +16
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The canonical/OG/Twitter URLs are hard-coded to https://ooshare.io/. If this UI is deployed in other environments (staging, self-hosted, preview domains), these tags will point to the wrong origin and produce incorrect share previews/canonicalization. Consider using a build-time env placeholder (Vite supports %VITE_*% in index.html) or otherwise parameterizing the site base URL per deployment.

Copilot uses AI. Check for mistakes.
<meta property="og:locale" content="en_US" />

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Only Once Share — Secure One-Time Secret Sharing" />
<meta name="twitter:description" content="Share passwords, API keys, and sensitive data with end-to-end encryption. Zero-knowledge, single-use links that self-destruct after viewing." />
<meta name="twitter:image" content="https://ooshare.io/og-image.png" />

<!-- Additional SEO -->
<meta name="robots" content="index, follow" />
<meta name="keywords" content="secret sharing, one-time secret, encrypted sharing, zero-knowledge, password sharing, secure link, self-destructing message" />
<meta name="author" content="DHD Tech" />

<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<title>Only Once Share — Secure One-Time Secret Sharing</title>

<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Only Once Share",
"url": "https://ooshare.io",
"description": "Share secrets securely with end-to-end encryption. Zero-knowledge, single-use links that self-destruct after viewing.",
"applicationCategory": "SecurityApplication",
"operatingSystem": "Any",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"creator": {
"@type": "Organization",
"name": "DHD Tech",
"url": "https://dhdtech.com"
},
"featureList": [
"End-to-end AES-256-GCM encryption",
"Zero-knowledge architecture",
"Single-use self-destructing links",
"No account required",
"Open source"
]
}
</script>
</head>
<body>
<div id="root"></div>
Expand Down
16 changes: 16 additions & 0 deletions ui/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ server {
root /usr/share/nginx/html;
index index.html;

# Static assets — long cache
location ~* \.(js|css|png|jpg|jpeg|svg|ico|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
Comment on lines +6 to +10
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This long-lived immutable cache applies to all .svg/.ico files, including un-hashed public assets like /favicon.svg (and potentially other stable URLs). If those change, clients can be stuck with the old version for 30 days. Consider narrowing the rule to fingerprinted build assets (e.g., /assets/) or excluding known non-fingerprinted files from the immutable policy.

Copilot uses AI. Check for mistakes.

# robots.txt and sitemap — short cache
location = /robots.txt {
expires 7d;
add_header Cache-Control "public";
}
location = /sitemap.xml {
expires 7d;
add_header Cache-Control "public";
Comment on lines +8 to +19
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expires directive already sets Cache-Control (max-age) and Expires. Adding a separate add_header Cache-Control ... here can result in multiple Cache-Control headers or losing the max-age/immutable combination depending on client behavior. Consider emitting a single Cache-Control value (including max-age + immutable) and dropping expires, or otherwise ensuring only one Cache-Control header is produced.

Suggested change
expires 30d;
add_header Cache-Control "public, immutable";
}
# robots.txt and sitemap — short cache
location = /robots.txt {
expires 7d;
add_header Cache-Control "public";
}
location = /sitemap.xml {
expires 7d;
add_header Cache-Control "public";
add_header Cache-Control "public, max-age=2592000, immutable";
}
# robots.txt and sitemap — short cache
location = /robots.txt {
add_header Cache-Control "public, max-age=604800";
}
location = /sitemap.xml {
add_header Cache-Control "public, max-age=604800";

Copilot uses AI. Check for mistakes.
}

location / {
try_files $uri $uri/ /index.html;
}
Expand Down
Binary file added ui/public/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions ui/public/og-image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions ui/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Disallow: /s/

Sitemap: https://ooshare.io/sitemap.xml
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sitemap: is hard-coded to https://ooshare.io/sitemap.xml. If this app is deployed to a different domain (staging/self-hosted), crawlers will be directed to the wrong sitemap location. Consider using a relative sitemap URL or generating robots.txt per environment/domain during deployment.

Suggested change
Sitemap: https://ooshare.io/sitemap.xml
Sitemap: /sitemap.xml

Copilot uses AI. Check for mistakes.
8 changes: 8 additions & 0 deletions ui/public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://ooshare.io/</loc>
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sitemap <loc> is hard-coded to https://ooshare.io/. If the UI can be served from other domains/environments, this will produce incorrect sitemap entries. Consider generating sitemap.xml per environment (or templating the base URL at build/deploy time).

Suggested change
<loc>https://ooshare.io/</loc>
<loc>__BASE_URL__/</loc>

Copilot uses AI. Check for mistakes.
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
11 changes: 10 additions & 1 deletion ui/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Shield, Lock, Eye, Trash2 } from "lucide-react";
import SecurityModal from "./SecurityModal";
import LanguageSelector from "./LanguageSelector";

export default function Layout({ children }: { children: React.ReactNode }) {
const { t } = useTranslation();
const { t, i18n } = useTranslation();

useEffect(() => {
document.documentElement.lang = i18n.language;
document.title = t("meta.title");
document
.querySelector('meta[name="description"]')
?.setAttribute("content", t("meta.description"));
}, [i18n.language, t]);
Comment on lines +10 to +16
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<html lang> is updated, but the document direction isn’t. When switching to Arabic (ar), the UI will still render with dir="ltr", which can break layout/reading order and assistive tech expectations. Consider also setting document.documentElement.dir based on the selected language (e.g., via i18n.dir(i18n.language) or a simple rtl language set).

Copilot uses AI. Check for mistakes.

return (
<div className="layout">
Expand Down
4 changes: 4 additions & 0 deletions ui/src/i18n/locales/ar.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"meta": {
"title": "Only Once Share — مشاركة الأسرار الآمنة لمرة واحدة",
"description": "شارك كلمات المرور ومفاتيح API والبيانات الحساسة بتشفير من طرف إلى طرف. معرفة صفرية، روابط تُستخدم مرة واحدة وتتلف ذاتياً بعد المشاهدة."
},
"header": {
"title": "Only Once Share"
},
Expand Down
4 changes: 4 additions & 0 deletions ui/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"meta": {
"title": "Only Once Share — Secure One-Time Secret Sharing",
"description": "Share passwords, API keys, and sensitive data with end-to-end encryption. Zero-knowledge, single-use links that self-destruct after viewing."
},
"header": {
"title": "Only Once Share"
},
Expand Down
4 changes: 4 additions & 0 deletions ui/src/i18n/locales/es.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"meta": {
"title": "Only Once Share — Compartir secretos de forma segura",
"description": "Comparte contraseñas, claves API y datos sensibles con cifrado de extremo a extremo. Conocimiento cero, enlaces de un solo uso que se autodestruyen después de verlos."
},
"header": {
"title": "Only Once Share"
},
Expand Down
4 changes: 4 additions & 0 deletions ui/src/i18n/locales/hi.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"meta": {
"title": "Only Once Share — सुरक्षित एक बार का गोपनीय साझाकरण",
"description": "एंड-टू-एंड एन्क्रिप्शन के साथ पासवर्ड, API कुंजियाँ और संवेदनशील डेटा साझा करें। शून्य ज्ञान, एक बार उपयोग लिंक जो देखने के बाद स्वतः नष्ट हो जाते हैं।"
},
"header": {
"title": "Only Once Share"
},
Expand Down
56 changes: 30 additions & 26 deletions ui/src/i18n/locales/pt.json
Original file line number Diff line number Diff line change
@@ -1,63 +1,67 @@
{
"meta": {
"title": "Only Once Share — Compartilhamento seguro de segredos",
"description": "Compartilhe senhas, chaves de API e dados sensíveis com criptografia ponta a ponta. Conhecimento zero, links de uso único que se autodestroem após a visualização."
},
"header": {
"title": "Only Once Share"
},
"hero": {
"title": "Compartilhe segredos com seguranca",
"subtitle": "Criptografia ponta a ponta no seu navegador. O servidor nunca ve seus dados. Links se autodestroem apos uma visualizacao."
"title": "Compartilhe segredos com segurança",
"subtitle": "Criptografia ponta a ponta no seu navegador. O servidor nunca seus dados. Links se autodestroem após uma visualização."
},
"create": {
"label": "Conteudo secreto",
"label": "Conteúdo secreto",
"placeholder": "Cole sua senha, chave de API ou mensagem privada...",
"charCount": "{{count}} / 50,000",
"expiresIn": "Expira em",
"encrypting": "Criptografando...",
"submit": "Criar link secreto",
"linkCreated": "Link secreto criado",
"linkInfo": "Compartilhe este link com seu destinatario. Ele so pode ser aberto uma vez, depois e permanentemente destruido.",
"linkInfo": "Compartilhe este link com seu destinatário. Ele pode ser aberto uma vez, depois é permanentemente destruído.",
"shareVia": "Compartilhar via",
"copy": "Copiar",
"copied": "Copiado",
"whatsapp": "WhatsApp",
"email": "E-mail",
"createAnother": "Criar outro",
"whatsappMsg": "Estou compartilhando um segredo com voce. Abra este link para ve-lo (apenas uma vez):\n\n{{link}}",
"emailSubject": "Aqui esta um segredo para voce",
"emailBody": "Estou compartilhando um segredo com voce. Abra este link para ve-lo — so pode ser aberto uma vez:\n\n{{link}}"
"whatsappMsg": "Estou compartilhando um segredo com você. Abra este link para -lo (apenas uma vez):\n\n{{link}}",
"emailSubject": "Aqui está um segredo para você",
"emailBody": "Estou compartilhando um segredo com você. Abra este link para -lo — pode ser aberto uma vez:\n\n{{link}}"
},
"view": {
"loading": "Recuperando e descriptografando segredo...",
"destroyed": "Este segredo foi permanentemente destruido. Nao pode ser visualizado novamente.",
"destroyed": "Este segredo foi permanentemente destruído. Não pode ser visualizado novamente.",
"copySecret": "Copiar segredo",
"copiedClipboard": "Copiado para a area de transferencia",
"notFoundTitle": "Segredo nao disponivel",
"notFoundMsg": "Este segredo ja foi visualizado ou expirou. Segredos so podem ser acessados uma vez.",
"copiedClipboard": "Copiado para a área de transferência",
"notFoundTitle": "Segredo não disponível",
"notFoundMsg": "Este segredo foi visualizado ou expirou. Segredos podem ser acessados uma vez.",
"errorTitle": "Algo deu errado",
"errorMsg": "Nao foi possivel descriptografar o segredo. O link pode ser invalido.",
"invalidLink": "Link invalido — chave de descriptografia ausente",
"errorMsg": "Não foi possível descriptografar o segredo. O link pode ser inválido.",
"invalidLink": "Link inválido — chave de descriptografia ausente",
"newSecret": "Compartilhar novo segredo",
"backHome": "Voltar ao inicio"
"backHome": "Voltar ao início"
},
"footer": {
"encryption": "AES-256-GCM",
"zeroKnowledge": "Conhecimento zero",
"autoDelete": "Auto-exclusao"
"autoDelete": "Auto-exclusão"
},
"security": {
"title": "Como funciona",
"e2eTitle": "Criptografia ponta a ponta",
"e2eDesc": "Seu segredo e criptografado no seu navegador usando AES-256-GCM com um IV aleatorio de 96 bits antes de sair do seu dispositivo. A chave de criptografia nunca e enviada ao nosso servidor.",
"hkdfTitle": "Derivacao de chave HKDF",
"hkdfDesc": "Uma chave de criptografia unica e derivada para cada segredo usando HKDF-SHA-256 com o ID do segredo como contexto. Mesmo com a chave mestra, cada segredo tem sua propria chave criptograficamente independente.",
"aadTitle": "Vinculacao de dados autenticados",
"aadDesc": "O ID do segredo e vinculado como Dados Autenticados Adicionais (AAD) durante a criptografia. Se alguem adulterar o ID ou trocar o texto cifrado entre segredos, a descriptografia falhara.",
"e2eDesc": "Seu segredo é criptografado no seu navegador usando AES-256-GCM com um IV aleatório de 96 bits antes de sair do seu dispositivo. A chave de criptografia nunca é enviada ao nosso servidor.",
"hkdfTitle": "Derivação de chave HKDF",
"hkdfDesc": "Uma chave de criptografia única é derivada para cada segredo usando HKDF-SHA-256 com o ID do segredo como contexto. Mesmo com a chave mestra, cada segredo tem sua própria chave criptograficamente independente.",
"aadTitle": "Vinculação de dados autenticados",
"aadDesc": "O ID do segredo é vinculado como Dados Autenticados Adicionais (AAD) durante a criptografia. Se alguém adulterar o ID ou trocar o texto cifrado entre segredos, a descriptografia falhará.",
"zkTitle": "Conhecimento zero",
"zkDesc": "O servidor armazena apenas dados criptografados. Nao podemos ler, descriptografar ou acessar seus segredos de nenhuma forma.",
"zkDesc": "O servidor armazena apenas dados criptografados. Não podemos ler, descriptografar ou acessar seus segredos de nenhuma forma.",
"keyTitle": "A chave nunca sai do navegador",
"keyDesc": "A chave de descriptografia e colocada apos o # na URL. Fragmentos de URL do navegador nunca sao enviados aos servidores.",
"oneTimeTitle": "Visualizacao unica",
"oneTimeDesc": "Quando um segredo e recuperado, ele e atomicamente excluido do armazenamento na mesma operacao.",
"expiryTitle": "Expiracao automatica",
"expiryDesc": "Segredos expiram automaticamente apos o TTL escolhido (1-72 horas), mesmo se nunca forem visualizados."
"keyDesc": "A chave de descriptografia é colocada após o # na URL. Fragmentos de URL do navegador nunca são enviados aos servidores.",
"oneTimeTitle": "Visualização única",
"oneTimeDesc": "Quando um segredo é recuperado, ele é atomicamente excluído do armazenamento na mesma operação.",
"expiryTitle": "Expiração automática",
"expiryDesc": "Segredos expiram automaticamente após o TTL escolhido (1-72 horas), mesmo se nunca forem visualizados."
}
}
4 changes: 4 additions & 0 deletions ui/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"meta": {
"title": "Only Once Share — 安全一次性秘密分享",
"description": "通过端到端加密分享密码、API密钥和敏感数据。零知识架构,一次性自毁链接。"
},
"header": {
"title": "Only Once Share"
},
Expand Down
Loading