{"id":19555113,"url":"https://github.com/lekovr/showonce","last_synced_at":"2026-06-04T23:31:17.531Z","repository":{"id":149993147,"uuid":"622683584","full_name":"LeKovr/showonce","owner":"LeKovr","description":"Write text and show it once","archived":false,"fork":false,"pushed_at":"2025-08-04T14:31:16.000Z","size":349,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-11-19T23:22:24.772Z","etag":null,"topics":["inmemory","onetimesecret","password","self-hosted"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LeKovr.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-04-02T20:24:44.000Z","updated_at":"2025-08-04T14:31:20.000Z","dependencies_parsed_at":null,"dependency_job_id":"5f379d02-0da9-48d2-b1d0-7143d419ee2b","html_url":"https://github.com/LeKovr/showonce","commit_stats":null,"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/LeKovr/showonce","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LeKovr%2Fshowonce","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LeKovr%2Fshowonce/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LeKovr%2Fshowonce/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LeKovr%2Fshowonce/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LeKovr","download_url":"https://codeload.github.com/LeKovr/showonce/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LeKovr%2Fshowonce/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33924832,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["inmemory","onetimesecret","password","self-hosted"],"created_at":"2024-11-11T04:32:01.266Z","updated_at":"2026-06-04T23:31:17.513Z","avatar_url":"https://github.com/LeKovr.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n[![Go Reference][ref1]][ref2]\n [![GitHub Release][gr1]][gr2]\n [![Test Coverage][cct1]][cct2]\n [![Maintainability][ccm1]][ccm2]\n [![GoCard][gc1]][gc2]\n [![Build Status][bs1]][bs2]\n [![GitHub license][gl1]][gl2]\n\n[cct1]: https://api.codeclimate.com/v1/badges/b8061e3ed9faa6819584/test_coverage\n[cct2]: https://codeclimate.com/github/LeKovr/showonce/test_coverage\n[ccm1]: https://api.codeclimate.com/v1/badges/b8061e3ed9faa6819584/maintainability\n[ccm2]: https://codeclimate.com/github/LeKovr/showonce/maintainability\n[ref1]: https://pkg.go.dev/badge/github.com/LeKovr/showonce.svg\n[ref2]: https://pkg.go.dev/github.com/LeKovr/showonce\n[gc1]: https://goreportcard.com/badge/github.com/LeKovr/showonce\n[gc2]: https://goreportcard.com/report/github.com/LeKovr/showonce\n[bs1]: https://github.com/LeKovr/showonce/actions/workflows/docker-publish.yml/badge.svg\n[bs2]: http://github.com/LeKovr/showonce/actions/workflows/docker-publish.yml\n[gr1]: https://img.shields.io/github/release/LeKovr/showonce.svg\n[gr2]: https://github.com/LeKovr/showonce/releases\n[gl1]: https://img.shields.io/github/license/LeKovr/showonce.svg\n[gl2]: https://github.com/LeKovr/showonce/blob/master/LICENSE\n\n# Шованс (Show once)\n\nСервис обмена текстами, которые доступны для чтения только один раз.\n\n## Назначение\n\n* Предоставить **Отправителю** возможность сохранить на сервере некий текстовый **секрет** (например, пароль) с некоторым случайным **идентификатором** и периодом актуальности\n* Предоставить **Получателю** возможность однократно прочитать этот секрет при выполнении условий:\n  * Передан идентификатор\n  * Период актуальности еще не закончился\n  * Запрос на доступ предоставляется впервые\n\nВ первой версии считается допустимым сценарий, при котором **Получатель** может получить фальшивый URL, пройти по ссылке и увидеть похожий сайт, который запросит информацию с легального, покажет ее пользователю и продублирует у себя в интересах третьих лиц.\n\n## Диаграмма потока запросов\n\n```mermaid\n%%{init: {\"theme\": \"neutral\",\"flowchart\": {\"curve\": \"linear\"}} }%%\n\nflowchart TD\n  subgraph web[Internet]\n    subgraph br[Browser]\n      page[Static page]\n      js[/Javascript API / Swagger/]\n    end\n    grpcc[/GRPC-client/]\n  end\n  subgraph srv[Server]\n    subgraph tr[Traefik+LE]\n      br-- https --\u003ehttpf[HTTPS Frontend]\n      grpcc-- GRPC --\u003egrpcf[GRPC Frontend]\n    end\n    subgraph so[Showonce]\n      httpf-- http --\u003ehttp[HTTP Service]\n      grpcf-- h2c --\u003epub\u003eGRPC Public service]\n      http-- public_page_req --\u003estatic[Static FS]\n      http-- public_api_req --\u003egw_pub[/Public API GRPC Gateway/]\n      http-- private_req --\u003eoauth[[Oauth2]]\n      oauth-- page_req --\u003estatic\n      oauth-- api_req --\u003egw_priv[/Private API GRPC Gateway/]\n      gw_pub-- GRPC --\u003epub\n      gw_priv-- GRPC --\u003epriv\u003eGRPC Private service]\n      pub ==\u003e st[(Storage)]\n      priv ==\u003e st\n    end\n  end\n  classDef out stroke:#f00\n  classDef our stroke:#0f0\n  classDef ext stroke:#00f\n  class web out;\n  class tr ext;\n  class so our;\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eОбозначения\u003c/summary\u003e\n\n```mermaid\nflowchart LR\n  a[/Generated from .proto/]\n  b\u003eShowonce API]\n  c[(Cache)]\n  d[[External service]]\n```\n\n\u003c/details\u003e\n\n### API\n\nДля публичной части поддерживается интерфейс GRPC. См [Описание .proto](proto/)\n\nТакже доступен [JSON RPC](static/js/service.swagger.json)\n\n\n## Аргументы сервиса\n\nСм. [config.md](config.md)\n\n## Алгоритм\n\n**Отправитель**\n\n* авторизуется\n* открывает страницу \"Создать\"\n* вводит атрибуты секрета\n* нажимает \"Сохранить\"\n* получает ссылку на доступ к секрету\n\n**Получатель**\n\n* открывает полученную ссылку\n* видит название секрета, срок жизни и, если срок не истек и текст не был прочитан, ссылку \"Показать\"\n* после нажатия \"Показать\" - видит сам текст\n* при отправке текста адресату он удаляется на сервере и (в след версиях) формируется уведомление для **Отправителя**\n\n**Отправитель**\n\n* авторизуется\n* на открывшейся после авторизации странице видит список своих текстов с атрибутами\n  * ссылка\n  * название\n  * факт показа (в след версиях - IP адресата)\n  * время показа (до показа - время удаления текста)\n* (в след версиях) список может быть отфильтрован по\n  * факту показа\n  * значению поля \"Группа\"\n\n## Карта сайта\n\nСервис представляет собой сайт со следующими страницами\n\n* главная (/), содержит\n  * описание сервиса\n  * ссылку на авторизацию отправителя\n  * поле ввода идентификатора текста\n* метаданные секрета (/?id=XXX), содержит\n  * все атрибуты текста (кроме контента)\n  * ссылку \"Показать\" (если не было показа, иначе - время показа)\n  * ссылку на показ контента (запрос POST /?id=XXX), которая возвращает\n  * при первом запросе существующего непрочитанного контента - текст, иначе - 404\n* кабинет отправителя (/my), доступен после авторизации и содержит\n  * ссылку \"создать\"\n  * статистику по созданным текстам\n  * список созданных текстов (/my/items),содержит\n    * список со ссылками (/?id=XXX) на созданные пользователем тексты\n  * создание текста (/my/new), содержит\n    * форму с атрибутами текста\n    * кнопку \"Создать\"\n\n## Дополнения\n\n### ID\n\nДля генерации идентификатора используется [ULID](https://github.com/oklog/ulid).\nЭто (в след версиях) позволит зашивать дату (создания или протухания)\n\n### Уникальность секрета\n\nОсуществляется хранением его sha1 (в след версиях)\n\n### Хранение\n\nСогласно юзкейса сервиса (передача пароля от админа к пользователю), повторное создание текста (генерация нового пароля) дешевле, чем потенциальный ущерб от компрометации этого текста.\n\nПоэтому сам текст хранится только в памяти приложения и удаляется в случае\n\n* показа получателю\n* истечения срока хранения\n* рестарта сервиса\n\nТ.е. на диск тексты не пишутся, чтобы избежать шифрования, для которого придется где-то хранить ключ, который может утечь. Эта проблема, возможно, решаема с использованием таких техник, как [vault](https://github.com/hashicorp/vault), но для первой версии это принято нецелесообразным.\n\nВместе с тем, жизненный цикл у секрета и его метаданных разный, по истечении срока актуальности удаляется только сам секрет, а его метаданные хранятся до истечения Срока жизни метаданных, (в след версиях) метаданные будут храниться в персистентном хранилище.\n\nТекущее решение: in-memory KV [zcache](https://zgo.at/zcache/v2).\n\n### Авторизация отправителя\n\nЦели:\n\n* группировка текстов по автору\n* исключение нецелевого использования сервиса\n\nПервичное решение:\n\n* gitea. **Отправитель**  должен быть членом заданной в настройках организации gitea, обмен с gitea производится по протоколу OAuth2\n\n### Атрибут \"Группа\"\n\nЗадается при создании секрета. Первоначальное значение - `default`, отправитель может заменить это значение.\n\nНазначение атрибута - (в след версиях) фильтрация списка созданных секретов в кабинете.\n\n## См. также\n\n* [Onetime Secret](https://onetimesecret.com/)\n* [Password Pusher](https://pwpush.com/)\n* [Hemmelig](https://hemmelig.app/)\n\n## TODO\n\n* [ ] js: если после GetMeta вышел срок актуальности, GetData доступна и возвращает `{\"code\":2,\"message\":\"item not found\"}`\n* [ ] Подключить автогенерацию тестов для [генерируемых исходников](zdoc/)\n* [ ] GRPC: добавить авторизацию пользователя по токену\n\n## Состав проекта\n\nLanguage|files|blank|comment|code\n:-------|-------:|-------:|-------:|-------:\nGo|10|114|98|686\nJavaScript|2|25|20|377\nJSON|1|0|0|365\nMarkdown|3|129|0|364\nYAML|11|36|84|334\nHTML|5|1|0|261\nProtocol Buffers|1|18|20|116\nCSS|2|18|2|83\nmake|1|37|33|60\nBourne Shell|1|14|11|55\nDockerfile|1|10|0|22\nText|1|0|0|2\n--------|--------|--------|--------|--------\nSUM:|39|402|268|2725\n\n## История изменений\n\nСм. [CHANGELOG](CHANGELOG.md)\n\n## Лицензия\n\nCopyright 2023 Aleksei Kovrizhkin \u003clekovr+github@gmail.com\u003e\n\nИсходный код проекта лицензирован под Apache License, Version 2.0 (the \"[License](LICENSE)\");\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flekovr%2Fshowonce","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flekovr%2Fshowonce","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flekovr%2Fshowonce/lists"}