├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── app ├── [compositeKey] │ └── page.tsx ├── components │ ├── analytics.tsx │ ├── error.tsx │ ├── stats.tsx │ ├── testimony.tsx │ └── title.tsx ├── deploy │ └── page.tsx ├── globals.css ├── head.tsx ├── header.tsx ├── layout.tsx ├── page.tsx ├── share │ └── page.tsx └── unseal │ └── page.tsx ├── img └── envshare.png ├── jest.config.js ├── next.config.js ├── package.json ├── pages └── api │ └── v1 │ ├── load.ts │ ├── og.tsx │ ├── secret │ ├── [id].ts │ └── index.ts │ └── store.ts ├── pkg ├── constants.ts ├── encoding.test.ts ├── encoding.ts ├── encryption.test.ts ├── encryption.ts └── id.ts ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── fonts │ └── Inter-SemiBold.ttf ├── rome.json ├── tailwind.config.js ├── tsconfig.json └── util └── base58.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env* 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/.pnpm/typescript@4.9.4/node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Andreas Thomas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

EnvShare

3 |
Share Environment Variables Securely
4 |
5 | 6 |
7 | envshare.dev 8 |
9 |
10 | 11 | EnvShare is a simple tool to share environment variables securely. It uses 12 | **AES-GCM** to encrypt your data before sending it to the server. The encryption 13 | key never leaves your browser. 14 | 15 | ## Features 16 | 17 | - **Shareable Links:** Share your environment variables securely by sending a 18 | link 19 | - **End-to-End Encryption:** AES-GCM encryption is used to encrypt your data 20 | before sending it to the server 21 | - **Limit number of reads:** Limit the number of times a link can be read 22 | - **Auto Expire:** Automatically expire links and delete data after a certain 23 | time 24 | 25 |
26 | 27 | ![](img/envshare.png) 28 | 29 | ## Built with 30 | 31 | - [Next.js](https://nextjs.org) 32 | - [tailwindcss](https://tailwindcss.com) 33 | - Deployed on [Vercel](https://vercel.com?utm_source=envshare) 34 | - Data stored on [Upstash](https://upstash.com?utm_source=envshare) 35 | 36 | ## Deploy your own 37 | 38 | Detailed instructions can be found [here](https://envshare.dev/deploy) 39 | 40 | All you need is a Redis database on Upstash and a Vercel account. Click the 41 | button below to clone and deploy: 42 | 43 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=EnvShare&demo-description=Simple%20Next.js%20%2B%20Upstash%20app%20to%20share%20environment%20variables%20securely%20using%20AES-GCM%20encryption.&demo-url=https%3A%2F%2Fenvshare.dev%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F5SaFBHXp5FBFJbsTzVqIJ3%2Ff0f8382369b7642fd8103debb9025c11%2Fenvshare.png&project-name=EnvShare&repository-name=envshare&repository-url=https%3A%2F%2Fgithub.com%2Fchronark%2Fenvshare&from=templates&integration-ids=oac_V3R1GIpkoJorr6fqyiwdhl17) 44 | 45 | 46 | ## Sponsors 47 | 48 | 49 | 50 | 66 | 67 |
51 | 52 | 53 | Upstash 54 | 55 |

Upstash: Serverless Database for Redis

56 | 57 |
    58 |
  • Serverless Redis with global replication and durable storage
  • 59 |
  • Price scales to zero with per request pricing
  • 60 |
  • Built-in REST API designed for serverless and edge functions
  • 61 |
62 | 63 | [Start for free in 30 seconds!](https://upstash.com/?utm_source=envshare) 64 | 65 |
68 | 69 | ## Configuration 70 | 71 | ### Environment Variables 72 | 73 | `ENABLE_VERCEL_ANALYTICS` Any truthy value will enable Vercel Analytics. This is turned off by default 74 | 75 | ## Contributing 76 | 77 | This repository uses `pnpm` to manage dependencies. Install it using 78 | `npm install -g pnpm` 79 | 80 | Please run `pnpm fmt` before committing to format the code. 81 | 82 | ## Docs 83 | 84 | Docs in the README are temporary and will be moved to the website soon. 85 | 86 | ### API 87 | 88 | #### Store a secret 89 | 90 | **PLEASE NEVER EVER UPLOAD UNENCRYPTED SECRETS.** 91 | 92 | This endpoint is only meant to store **already encrypted** secrets. The 93 | encrypted secrets are stored in plain text. 94 | 95 | ```sh-session 96 | $ curl -XPOST -s https://envshare.dev/api/v1/secret -d "already-encrypted-secret" 97 | ``` 98 | 99 | You can add optional headers to configure the ttl and number of reads. 100 | 101 | ```sh-session 102 | $ curl -XPOST -s https://envshare.dev/api/v1/secret -d "already-encrypted-secret" -H "envshare-ttl: 3600" -H "envshare-reads: 10" 103 | ``` 104 | 105 | - Omitting the `envshare-ttl` header will set a default of 30 days. Disable the 106 | ttl by setting it to 0. (`envshare-ttl: 0`) 107 | - Omitting the `envshare-reads` header will simply disable it and allow reading 108 | for an unlimited number of times. 109 | 110 | This endpoint returns a JSON response with the secret id: 111 | 112 | ```json 113 | { 114 | "data": { 115 | "id": "HdPbXgpvUvNk43oxSdK97u", 116 | "ttl": 86400, 117 | "reads": 2, 118 | "expiresAt": "2023-01-19T20:47:28.383Z", 119 | "url": "http://envshare.dev/api/v1/secret/HdPbXgpvUvNk43oxSdK97u" 120 | } 121 | } 122 | ``` 123 | 124 | #### Retrieve a secret 125 | 126 | You need an id to retrieve a secret. The id is returned when you store a secret. 127 | 128 | ```sh-session 129 | $ curl -s https://envshare.dev/api/v1/secret/HdPbXgpvUvNk43oxSdK97u 130 | ``` 131 | 132 | ```json 133 | { 134 | "data": { 135 | "secret": "Hello", 136 | "remainingReads": 1 137 | } 138 | } 139 | ``` 140 | -------------------------------------------------------------------------------- /app/[compositeKey]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | // This page is here for backwards compatibility with old links. 4 | // Old links were of the form /{compositeKey} and now they are of the form /unseal#{compositeKey} 5 | export default function Page(props: { params: { compositeKey: string } }) { 6 | return redirect(`/unseal#${props.params.compositeKey}`); 7 | } 8 | -------------------------------------------------------------------------------- /app/components/analytics.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Analytics as VercelAnalytics } from "@vercel/analytics/react"; 3 | 4 | const track = ["/", "/share", "/deploy", "/unseal"]; 5 | 6 | export function Analytics() { 7 | return ( 8 | { 10 | const url = new URL(event.url); 11 | if (!track.includes(url.pathname)) { 12 | url.pathname = "/__redacted"; 13 | return { 14 | ...event, 15 | url: url.href, 16 | }; 17 | } 18 | return event; 19 | }} 20 | /> 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/components/error.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | message: string; 3 | }; 4 | 5 | export const ErrorMessage: React.FC = ({ message }) => { 6 | return ( 7 |
8 | {message} 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /app/components/stats.tsx: -------------------------------------------------------------------------------- 1 | import { Redis } from "@upstash/redis"; 2 | 3 | const redis = Redis.fromEnv(); 4 | export const revalidate = 60; 5 | 6 | export const Stats = asyncComponent(async () => { 7 | const [reads, writes] = await redis 8 | .pipeline() 9 | .get("envshare:metrics:reads") 10 | .get("envshare:metrics:writes") 11 | .exec<[number, number]>(); 12 | const stars = await fetch("https://api.github.com/repos/chronark/envshare") 13 | .then((res) => res.json()) 14 | .then((json) => json.stargazers_count as number); 15 | 16 | const stats = [ 17 | { 18 | label: "Documents Encrypted", 19 | value: writes, 20 | }, 21 | { 22 | label: "Documents Decrypted", 23 | value: reads, 24 | }, 25 | ] satisfies { label: string; value: number }[]; 26 | 27 | if (stars) { 28 | stats.push({ 29 | label: "GitHub Stars", 30 | value: stars, 31 | }); 32 | } 33 | 34 | return ( 35 |
36 |
    37 | {stats.map(({ label, value }) => ( 38 |
  • 42 |
    43 | {Intl.NumberFormat("en-US", { notation: "compact" }).format(value)} 44 |
    45 |
    {label}
    46 |
  • 47 | ))} 48 |
49 |
50 | ); 51 | }); 52 | 53 | // stupid hack to make "server components" actually work with components 54 | // https://www.youtube.com/watch?v=h_9Vx6kio2s 55 | function asyncComponent(fn: (arg: T) => Promise): (arg: T) => R { 56 | return fn as (arg: T) => R; 57 | } 58 | -------------------------------------------------------------------------------- /app/components/testimony.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import { Props } from "next/script"; 5 | import React, { PropsWithChildren } from "react"; 6 | 7 | const TwitterHandle: React.FC = ({ children }) => { 8 | return {children}; 9 | }; 10 | 11 | const Author: React.FC> = ({ children, href }) => ( 12 | 13 | {children} 14 | 15 | ); 16 | 17 | const Title: React.FC> = ({ children, href }) => ( 18 | 24 | {children} 25 | 26 | ); 27 | 28 | export const Testimonials = () => { 29 | const posts: { 30 | content: React.ReactNode; 31 | link: string; 32 | author: { 33 | name: React.ReactNode; 34 | title?: React.ReactNode; 35 | image: string; 36 | }; 37 | }[] = [ 38 | { 39 | content: ( 40 |
41 |

42 | My cursory audit of @chronark_'s envshare: 43 |

44 |

45 | It is light, extremely functional, and does its symmetric block cipher correctly, unique initialization 46 | vectors, decryption keys derived securely. 47 |

48 |
49 |

Easily modified to remove minimal analytics. Superior to Privnote.

50 |
51 |

Self-hosting is easy. 👏

52 |
53 | ), 54 | link: "https://twitter.com/FrederikMarkor/status/1615299856205250560", 55 | author: { 56 | name: Frederik Markor, 57 | title: CEO @discreet, 58 | image: "https://pbs.twimg.com/profile_images/1438061314010664962/NecuMIGR_400x400.jpg", 59 | }, 60 | }, 61 | { 62 | content: ( 63 |
64 |

I'm particularly chuffed about this launch, for a couple of reasons:

65 |
    66 |
  • 67 | ◆ Built on @nextjs + @upstash, hosted on{" "} 68 | @vercel 69 |
  • 70 |
  • ◆ 100% free to use & open source
  • 71 |
  • ◆ One-click deploy via Vercel + Upstash integration
  • 72 |
73 |

Deploy your own → http://vercel.fyi/envshare

74 |
75 | ), 76 | link: "https://twitter.com/steventey/status/1615035241772482567?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1615035241772482567%7Ctwgr%5E1db44bb10c690189e24c980fcd787299961c34c6%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fpublish.twitter.com%2F%3Fquery%3Dhttps3A2F2Ftwitter.com2Fsteventey2Fstatus2F1615035241772482567widget%3DTweet", 77 | author: { 78 | name: Steven Tey, 79 | title: Senior Developer Advocate at Vercel, 80 | image: "https://pbs.twimg.com/profile_images/1506792347840888834/dS-r50Je_400x400.jpg", 81 | }, 82 | }, 83 | { 84 | content: ( 85 |
86 |

87 | Congratulations on the launch @chronark_👏! This is such a valuable product 88 | for developers. Icing on the cake is that it's open source! ✨ 89 |

90 |
91 | ), 92 | link: "https://twitter.com/DesignSiddharth/status/1615293209164546048", 93 | author: { 94 | name: @DesignSiddharth, 95 | image: "https://pbs.twimg.com/profile_images/1613772710009765888/MbSblJYf_400x400.jpg", 96 | }, 97 | }, 98 | ]; 99 | 100 | return ( 101 |
102 |
    103 | {posts.map((post, i) => ( 104 |
    108 | 109 | {post.content} 110 | 111 |
    112 |
    113 |
    {post.author.name}
    114 |
    {post.author.title}
    115 |
    116 |
    117 | 118 |
    119 |
    120 |
    121 | ))} 122 |
123 |
124 | ); 125 | }; 126 | -------------------------------------------------------------------------------- /app/components/title.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from "react"; 2 | 3 | export const Title: React.FC = ({ children }): JSX.Element => { 4 | return ( 5 |

6 | {children} 7 |

8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /app/deploy/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid"; 3 | import Link from "next/link"; 4 | import { Title } from "@components/title"; 5 | import React from "react"; 6 | const steps: { 7 | name: string; 8 | description: string | React.ReactNode; 9 | cta?: React.ReactNode; 10 | }[] = [ 11 | { 12 | name: "Create a new Redis database on Upstash", 13 | description: ( 14 | <> 15 | Upstash offers a serverless Redis database with a generous free tier of up to 10,000 requests per day. That's 16 | more than enough. 17 |
18 | Click the button below to sign up and create a new Redis database on Upstash. 19 | 20 | ), 21 | cta: ( 22 | 26 | Create Database 27 | 28 | 29 | ), 30 | }, 31 | { 32 | name: "Copy the REST connection credentials", 33 | description: ( 34 |

35 | After creating the database, scroll to the bottom and make a note of UPSTASH_REDIS_REST_URL and{" "} 36 | UPSTASH_REDIS_REST_TOKEN, you need them in the next step 37 |

38 | ), 39 | }, 40 | { 41 | name: "Deploy to Vercel", 42 | description: "Deploy the app to Vercel and paste the connection credentials into the environment variables.", 43 | cta: ( 44 | 48 | Deploy 49 | 50 | 51 | ), 52 | }, 53 | ]; 54 | 55 | export default function Deploy() { 56 | return ( 57 |
58 | Deploy EnvShare for Free 59 |

60 | You can deploy your own hosted version of EnvShare, you just need an Upstash and Vercel account. 61 |

62 |
    63 | {steps.map((step, stepIdx) => ( 64 |
  1. 65 |
  2. 85 | ))} 86 |
87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | input[type="number"]::-webkit-inner-spin-button, 7 | input[type="number"]::-webkit-outer-spin-button { 8 | @apply appearance-none; 9 | } 10 | 11 | 12 | input[type="file"] { 13 | @apply appearance-none; 14 | } 15 | } -------------------------------------------------------------------------------- /app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head({ title, subtitle }: { title: string; subtitle: string }) { 2 | // Fallback tagline 3 | title ??= "Share Environment Variables Securely"; 4 | subtitle ??= "EnvShare"; 5 | 6 | const baseUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"; 7 | 8 | const url = new URL("/api/v1/og", baseUrl); 9 | url.searchParams.set("title", title); 10 | url.searchParams.set("subtitle", subtitle); 11 | 12 | return ( 13 | <> 14 | EnvShare 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {/* Open Graph / Facebook */} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {/* Twitter */} 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | const navigation = [ 7 | { 8 | name: "Share", 9 | href: "/share", 10 | }, 11 | { 12 | name: "Unseal", 13 | href: "/unseal", 14 | }, 15 | 16 | { 17 | name: "Deploy", 18 | href: "/deploy", 19 | }, 20 | { 21 | name: "GitHub", 22 | href: "https://github.com/chronark/envshare", 23 | external: true, 24 | }, 25 | ] satisfies { name: string; href: string; external?: boolean }[]; 26 | 27 | export const Header: React.FC = () => { 28 | const pathname = usePathname(); 29 | return ( 30 |
31 |
32 |
33 | 34 | EnvShare 35 | 36 | {/* Desktop navigation */} 37 | 54 |
55 |
56 | 57 | {/* Fancy fading bottom border */} 58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import { Inter } from "@next/font/google"; 3 | import Link from "next/link"; 4 | import { Header } from "./header"; 5 | 6 | import { Analytics } from "@components/analytics"; 7 | const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); 8 | 9 | export default function RootLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | return ( 15 | 16 | 17 | 18 | { 19 | // Not everyone will want to host envshare on Vercel, so it makes sense to make this opt-in. 20 | process.env.ENABLE_VERCEL_ANALYTICS ? : null 21 | } 22 | 23 |
24 | 25 |
{children}
26 | 27 |
28 |
29 |

30 | Built by{" "} 31 | 32 | @chronark_ 33 | 34 | and{" "} 35 | 39 | many others{" "} 40 | 41 |

42 |

43 | EnvShare is deployed on{" "} 44 | 45 | Vercel 46 | {" "} 47 | and uses{" "} 48 | 49 | Upstash 50 | {" "} 51 | for storing encrypted data. 52 |

53 |
54 |
55 | 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Stats } from "./components/stats"; 3 | import { Testimonials } from "./components/testimony"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |
9 |
10 | 14 | EnvShare is Open Source on{" "} 15 | 16 | GitHub 17 | 18 | 19 |
20 |
21 |

22 | Share Environment Variables Securely 23 |

24 |

25 | Your document is encrypted in your browser before being stored for a limited period of time and read 26 | operations. Unencrypted data never leaves your browser. 27 |

28 |
29 | 33 | Deploy 34 | 35 | 39 | Share 40 | 41 | 42 |
43 |
44 |
45 |

Used and trusted by a growing community

46 | 47 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /app/share/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { toBase58 } from "util/base58"; 3 | import { useState, Fragment } from "react"; 4 | import { Cog6ToothIcon, ClipboardDocumentIcon, ClipboardDocumentCheckIcon } from "@heroicons/react/24/outline"; 5 | import { Title } from "@components/title"; 6 | import { encrypt } from "pkg/encryption"; 7 | import { ErrorMessage } from "@components/error"; 8 | import { encodeCompositeKey } from "pkg/encoding"; 9 | import { LATEST_KEY_VERSION } from "pkg/constants"; 10 | 11 | export default function Home() { 12 | const [text, setText] = useState(""); 13 | const [reads, setReads] = useState(999); 14 | 15 | const [ttl, setTtl] = useState(7); 16 | const [ttlMultiplier, setTtlMultiplier] = useState(60 * 60 * 24); 17 | const [loading, setLoading] = useState(false); 18 | const [error, setError] = useState(""); 19 | const [copied, setCopied] = useState(false); 20 | 21 | const [link, setLink] = useState(""); 22 | 23 | const onSubmit = async () => { 24 | try { 25 | setError(""); 26 | setLink(""); 27 | setLoading(true); 28 | 29 | const { encrypted, iv, key } = await encrypt(text); 30 | 31 | const { id } = (await fetch("/api/v1/store", { 32 | method: "POST", 33 | body: JSON.stringify({ 34 | ttl: ttl * ttlMultiplier, 35 | reads, 36 | encrypted: toBase58(encrypted), 37 | iv: toBase58(iv), 38 | }), 39 | }).then((r) => r.json())) as { id: string }; 40 | 41 | const compositeKey = encodeCompositeKey(LATEST_KEY_VERSION, id, key); 42 | 43 | const url = new URL(window.location.href); 44 | url.pathname = "/unseal"; 45 | url.hash = compositeKey; 46 | setCopied(false); 47 | setLink(url.toString()); 48 | } catch (e) { 49 | console.error(e); 50 | setError((e as Error).message); 51 | } finally { 52 | setLoading(false); 53 | } 54 | }; 55 | 56 | return ( 57 |
58 | {error ? : null} 59 | 60 | {link ? ( 61 |
62 | Share this link with others 63 |
64 |
 65 |               {link}
 66 |             
67 | 82 |
83 |
84 | ) : ( 85 |
{ 88 | e.preventDefault(); 89 | if (text.length <= 0) return; 90 | onSubmit(); 91 | }} 92 | > 93 | Encrypt and Share 94 | 95 |
 96 |             
97 | 107 | 108 |