npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-sealed-secrets

v0.3.12

Published

Encrypted credential vault for DeepSeek Harness: passphrase- or DPAPI-protected, decrypted in memory, injected into a command's environment, expanded from [[placeholders]], exposed through ctx.credentials, and audited without values.

Readme

dsh-sealed-secrets

Encrypted credential vault for DeepSeek Harness. Пакет называется dsh-sealed-secrets (имя dsh-secrets в npm занято); репозиторий, каталог данных и маршруты остались с прежним именем.

A .env file in the workspace is plaintext that any local process can read and any backup can copy; this plugin replaces it with one AES-256-GCM vault outside the workspace and hands values only to the command that needs them.

secret run -- python deploy.py                     # values arrive as ordinary env vars
deploy --token [[dsh-secrets:HA_TOKEN]]            # or a placeholder the host expands
secret list                                        # key names only

Honest limits: the model can still print a value on purpose (that is what the placeholder masking and the audit log are for), and anything running as your user can read the vault while it is unlocked. This protects against a plaintext file, a copied directory and a stolen disk — not against code running in your session.


Шифрованное хранилище учётных данных для DeepSeek Harness.

Что нового в 0.2

Восемь механизмов, которых не хватало до этого:

| Механизм | Что даёт | |---|---| | Плейсхолдеры | модель пишет [[dsh-secrets:NAME]], значение подставляет хост, а вывод команды маскируется обратно в плейсхолдер | | Фоновая зачистка логов | значения вычищаются из логов сессий по расписанию, а не только вручную | | accessMode + подтверждение | readonly / ask / auto: ask проводит каждый secret run через штатный канал approval | | Автоблокировка | ключ затирается после N секунд бездействия | | Профили инструментов | basic (list/run) или full (+status/audit) | | Провайдер ctx.credentials | хранилище отдаёт секреты штатному шву harness, а не только своему инструменту | | Переход C ↔ B | перенос ключей из .credentials.yaml в сейф с проверкой по диску, DPAPI-печатью, бэкапом профиля и откатом | | Аудит без значений | кто, что и с какими ключами делал; значений в журнале нет по конструкции | | DPAPI-ключ | на Windows ключ хранилища запечатывается под текущего пользователя — старт без ввода пароля |

Что он делает

  • Хранилище: ~/.dsh/secrets/vault.json — AES-256-GCM, ключ выводится из пароля через scrypt (N=2^17). В файле нет ни одного открытого значения.
  • Разблокировка: панель 🔑 в правом нижнем углу Web-интерфейса, либо автоматически из DPAPI-печати (keychain: dpapi). Пароль живёт только в памяти процесса хоста; при блокировке ключ затирается.
  • Доступ для модели: инструмент secret:
    • secret list — только имена ключей и их плейсхолдеры;
    • secret run — запускает команду, подставив значения обычными переменными окружения ($env:HA_TOKEN). Значения не возвращаются в ответ и не попадают в лог сессии; всё, что команда напечатала, маскируется.
  • Панель умеет создавать хранилище, импортировать .env целиком, добавлять и удалять отдельные ключи, блокировать хранилище и включать/выключать DPAPI-печать. Поле имени работает и как поиск: ввод фильтрует список по вхождению, буквы приводятся к верхнему регистру. Переключение режимов убрано под спойлер «Режим хранения», рядом с ним — «?» с кратким разбором рисков.

Конфигурация

- id: dsh-sealed-secrets
  name: dsh-sealed-secrets
  config:
    accessMode: auto            # readonly | ask | auto
    lockTimeoutSeconds: 900     # 0 — не блокировать по бездействию
    tools: basic                # basic | full
    provider: auto              # auto | off — регистрировать ctx.credentials
    keychain: dpapi             # off | dpapi (Windows)
    sweep:
      enabled: true
      intervalMinutes: 30
      skipMinutes: 5

Ошибка в значении (accessMode: sometimes) валит активацию плагина с внятным текстом — молча менять политику безопасности нельзя.

Режимы доступа

  • readonly — модели доступен только secret list; secret run отклоняется.
  • ask — каждый secret run идёт через ctx.approval.request; без сервиса подтверждений вызов закрывается ошибкой, а не проходит молча.
  • auto — по умолчанию; запуск без подтверждений.

Отказы тоже попадают в аудит с outcome: denied.

Плейсхолдеры и маскирование

Токен [[dsh-secrets:NAME]] можно ставить туда, где ожидается значение:

secret run -- curl -H "Authorization: Bearer [[dsh-secrets:HA_TOKEN]]" https://api

Квадратные скобки выбраны намеренно: {{…}} — синтаксис переменных системного промпта DSH, и имя вида dsh-secrets:NAME он отвергает, роняя всю секцию промпта. [[…]] шаблонизатор не трогает, поэтому плейсхолдер можно упоминать в промпте, описании инструмента и командной строке без последствий.

  • токен вне кавычек заменяется на строку в одинарных кавычках (PowerShell-экранирование);
  • внутри "..." — экранируется для двойных кавычек, внутри '...' — удвоение кавычки;
  • если токен склеен с другими символами слова, значение подставляется как есть, а факт отмечается в отчёте (unquoted) — вставлять кавычки в середину слова нельзя;
  • неизвестный токен — ошибка, а не пустой аргумент.

После выполнения значения, похожие на секреты, заменяются обратно на плейсхолдеры в stdout/stderr, поэтому Write-Output $env:TOKEN не утечёт в транскрипт. Отбор — тот же, что у зачистки логов (scrub.js): логины, имена, пути и URL остаются читаемыми, а не превращаются в плейсхолдеры (иначе слово config в имени файла выглядело бы как секрет).

Автоблокировка

lockTimeoutSeconds — срок бездействия, по умолчанию 0 (выключено). Проверка идёт на каждом обращении к хранилищу и дополнительно по таймеру; блокировка пишется в аудит как source: autolock.

В режиме «Единый сейф» включать TTL стоит с пониманием: сейф — единственный источник ключей моделей, поэтому после автоблокировки запросы к модели будут падать, пока вы снова не откроете панель. В режиме «Два хранилища» TTL ни на что больше не влияет.

DPAPI-ключ (Windows)

Ключ хранилища (32 байта + соль) запечатывается ConvertFrom-SecureString и лежит рядом в vault.dpapi. Автоматически он применяется только в режиме «Единый сейф»: в отдельном режиме на старте сейфу нечего делать, поэтому он остаётся запертым, а печать просто ждёт переключения. Так расшифрованные секреты не висят в памяти весь сеанс без пользы.

Честно: DPAPI отвечает «это тот же пользователь», а не «это тот же процесс». Другой процесс под вашей учётной записью сможет расшифровать файл так же, как и сам DSH. Пароль при этом не хранится нигде — ни в открытом виде, ни в конфиге. Выключить: POST /dsh-secrets/seal {enabled:false} (кнопка в панели) — файл удаляется.

Провайдер ctx.credentials

Штатный шов harness: настройки хранят ссылку (DEEPSEEK_API_KEY), провайдер владеет значением. Плагин умеет быть таким провайдером, и это ровно тот компромисс «свой шифрованный файл + регистрация провайдера»:

  • при provider: auto плагин смотрит, зарегистрирован ли уже ctx.credentials (ctx.get('credentials')). Если да — остаётся обычным хранилищем (provider: external), ничего не подменяя;
  • если шов пуст — регистрирует свой провайдер (ctx.provide('credentials', …));
  • имена ссылок читаются из шифрованного payload; ниже vault идут слои env → проектный .env → ~/.dsh/.env;
  • записи второго пространства (<scope>/<id>, api-key/grant) живут там же и поддерживают modifyRecord с read-decide-write.

Чтобы это включилось в бою, из композиции должен уйти штатный @deepseek-ai/dsh-credentials-local (он монтируется в @deepseek-ai/dsh-base). Иначе провайдер остаётся в режиме external — и это ожидаемое поведение, а не поломка. Цена включения: пока хранилище заблокировано, resolve возвращает undefined, то есть ключи LLM не работают до разблокировки. Поэтому включать это стоит вместе с keychain: dpapi. Файл .credentials.yaml при включённом провайдере больше не читается — перенесите ключи в сейф заранее.

Переход: «Два хранилища» ↔ «Единый сейф»

Два состояния установки:

  • Два хранилища (separate, по умолчанию): штатный ~/.dsh/.credentials.yaml и сейф живут раздельно. Ключи моделей — в файле, остальные секреты — в сейфе.
  • Единый сейф (unified): сейф становится провайдером ctx.credentials, и ключи моделей, OAuth-записи аккаунта и записи плагинов читаются из шифрованного файла.

Промежуточные статусы: unified-ready (скопировано и проверено, профиль ещё не тронут), unified-pending (записано, ждёт перезапуска), unified-failed (перезапуск не сделал сейф провайдером).

Переход делается из панели 🔑 и состоит из пяти шагов:

  1. Автозапуск (POST /dsh-secrets/seal). Ключ сейфа запечатывается DPAPI. Это не удобство, а условие: при старте DSH плагин connection пишет служебную запись (client-connection/browser-session) прямо в активации, и если сейф в этот момент заперт, обязательный плагин падает и приложение не запускается. Поэтому prepare без запечатанного ключа отказывает. Сам провайдер при этом ждёт идущую разблокировку, а не отвечает «заперто»: вызовы, начатые во время активации, дожидаются восстановления ключа.
  2. Подготовка (POST /dsh-secrets/mode/prepare). Читает имена ссылок из .credentials.yaml (только имена), значения забирает у смонтированного штатного провайдера через resolve — то есть тем же вызовом, которым пользуются потребители. Копируются только ссылки с источником file (значения из env/.env остаются слоями ниже и не дублируются). Записи (<scope>/<id>) забираются через listRecords/readRecord.
  3. Сверка: сейф переоткрывается с диска тем же ключом и сравнивается с исходником; ключ ещё и распечатывается из DPAPI обратно и сверяется побайтово. Не сошлось — переход не начинается.
  4. Применение (/mode/commit). Пишет в profiles/<имя>/cordis.patch.yml одну строку для строки credentials:
    - id: credentials
      name: '@deepseek-ai/dsh-credentials-local'
      disabled: true
    Перед записью делается бэкап cordis.patch.yml.bak-mode-<дата>, после — перечитывание и проверка, что строка на месте. Маркер перехода (~/.dsh/secrets/mode.json) переводится в pending-restart.
  5. Перезапуск DSH. При следующем старте плагин видит маркер и записывает фактический итог: active, если он действительно стал провайдером, или failed, если нет (тогда панель предложит откат).

Откат (/mode/rollback) восстанавливает файл из бэкапа и удаляет маркер. Штатный .credentials.yaml не изменяется и не удаляется ни на одном шаге, поэтому откат возвращает ровно прежнее состояние.

Важные честности:

  • Автозапуск обязателен. Без запечатанного ключа переход не начинается: плагин connection пишет служебную запись в активации, и запертый сейф валит обязательный плагин, а с ним весь запуск. Проверено на практике: DesktopHostFatalError: startup failed: 1 required plugin did not activate. Если такое случилось — кнопка «Disable third-party plugins» в окне ошибки возвращает композицию и .credentials.yaml остаётся нетронутым;
  • сам провайдер ждёт идущую разблокировку: вызовы, начатые во время активации, дожидаются восстановления ключа, а не получают «заперто»;
  • слои профиля живо перезагружаются: запись патча может примениться сразу, без перезапуска. Если живая пересборка не сложится, DSH оставит последнее рабочее дерево, а статус покажет unified-failed — тогда нужен перезапуск или откат;
  • переключение доступно только из панели (маршруты с проверкой Origin и confirm: true). Из инструмента модели доступно лишь чтение состояния: действие mode в профиле tools: full.

Аудит

~/.dsh/secrets/audit.log — JSONL, ротация при 512 КБ (одна предыдущая копия). Пишутся unlock, lock, autolock, run, list, set, delete, import, sweep, provider. В журнале есть имена ключей, код выхода и длительность, но нет значений: поля собираются по закрытому словарю, лишнее отбрасывается.

Фоновая зачистка

sweep.enabled запускает периодический проход по ~/.dsh/sessions и projection-cache: значения заменяются на [redacted:NAME], каждая перезапись проходит round-trip проверку и оставляет датированный бэкап. Файлы, изменённые меньше skipMinutes назад, пропускаются и обрабатываются в следующий раз. Ручной scrub-cli.mjs остаётся для отчёта и разового прогона.

Профили инструментов

  • basic — secret с действиями list, run;
  • full — плюс status и audit.

Схема и обработчик проверяют профиль дважды: ручной вызов скрытого действия отклоняется, а не просто не показывается модели.

Установка

# из репозитория, зафиксированный тег:
dsh plugin --profile desktop add "git+https://gitverse.ru/GigaVik/dsh-secrets.git#v0.3.6"
# локальная разработка:
dsh plugin --profile desktop add link:<путь>\dsh-secrets
# из npm, когда пакет опубликован:
dsh plugin --profile desktop add dsh-sealed-secrets

Тег фиксирует коммит: правки в main сами не приезжают — нужен новый тег и повторный add. После pnpm add профиль перезагружается, инструмент secret появляется сразу; панель — после обновления страницы.

Терминал (включая DSH Mobile)

unlock.cmd/unlock.mjs — инструменты 0.1.x, в пакет они не входят. Начиная с 0.3.6 в пакете есть cli.mjs (bin dsh-sealed-secrets), который работает с тем же HTTP-фасадом, что и панель 🔑, и сам находит запущенный хост: --url, затем DSH_WEB_URL из окружения оболочки, затем ~/.dsh/secrets/endpoint.json, который плагин пишет при старте.

# из установленного пакета (путь не зависит от рабочей папки):
$cli = "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-sealed-secrets\cli.mjs"
node $cli status
node $cli unlock --plain                   # пароль в cmd; --plain делает ввод видимым (мобильная клавиатура)
node $cli unlock                           # без --plain: ввод без эха, звёздочки
node $cli lock
node $cli list
node $cli set NPM_TOKEN --stdin            # вставить, Enter, Ctrl+Z, Enter
node $cli set HA_TOKEN --from .token --keep
node $cli delete OLD_KEY --yes
node $cli import --from .env --replace

Тот же файл есть в репозитории: default-workspace\dsh-secrets\cli.mjs. Плагин также кладёт рядом с сейфом короткий запускатор, который берёт node из рантайма DSH (шим из .bin требует node в PATH, а на десктопе его обычно нет):

%USERPROFILE%\.dsh\secrets\vault.cmd status
%USERPROFILE%\.dsh\secrets\vault.cmd unlock --plain
%USERPROFILE%\.dsh\secrets\vault.cmd set NPM_TOKEN --plain

Меню и короткая команда

С 0.3.9 у CLI есть интерактивное меню: vault без аргументов печатает пункты 1) статус … 8) сменить пароль, 0) выход, и на телефоне достаточно ввести цифру. Ввод в меню видимый (пароль и значение показываются), это осознанный компромисс ради мобильной клавиатуры — если это неприемлемо, используйте vault unlock без --plain (ввод без эха).

Сменить пароль хранилища (соль и ключ создаются заново, файл перешифровывается на месте):

vault passwd          # спросит новый пароль дважды, ввод без эха
vault passwd --plain  # то же, но символы видны

Чтобы не печатать длинный путь, один раз положите запускатор в рабочую папку:

%USERPROFILE%\.dsh\secrets\vault.cmd shortcut
vault status
vault

vault shortcut создаёт vault.cmd (или vault) в текущем каталоге с абсолютными путями к node и cli.mjs, после чего vault работает как обычная команда в этой папке. Терминал рабочей области стартует в корне workspace, так что этого достаточно.

Экспорт в парольный ZIP

Экспорт доступен только при разблокированном сейфе и заново спрашивает пароль хранилища — чтобы тот, кто оказался за вашим открытым DSH, не смог выгрузить все ключи. Внутри архива — плоский dsh-secrets.env; сам ZIP шифруется AES-256 новым паролем, который задаётся при экспорте и нигде не сохраняется.

  • панель 🔑 → «Экспорт .env (ZIP)»: пароль хранилища, пароль архива (дважды) → скачивается dsh-sealed-secrets-YYYYMMDD.zip;
  • терминал: vault export --to secrets.zip (или пункт 9 в меню);
  • 7-Zip встроен (7z-wasm, ~1.8 МБ): ставить ничего не нужно, работает и на машине без установленного 7-Zip;
  • пароль архива не попадает в командную строку процесса: он передаётся внутрь WASM и живёт только в памяти Node на время сборки архива.

Скачанный ZIP лежит в «Загрузках»; пароль архива не хранится — потеряли, файл не открыть.

Значения не печатаются — только имена ключей и длина; для set/import хранилище должно быть разблокировано в панели 🔑.

webServer опционален: в headless-композиции инструмент и секция промпта работают, панели нет.

Проверка

node test/smoke.mjs        # манифест, крипто, подмена, HTTP, профили, headless
node test/placeholders.mjs # подстановка/экранирование + реальный PowerShell-процесс
node test/provider.mjs     # ctx.credentials: ссылки, записи, locked, отсутствие plaintext
node test/sweep.mjs        # реальные zstd-логи: замена, бэкап, пропуск активной сессии
node test/audit.mjs        # словарь событий, ротация, отсутствие значений
node test/lock.mjs         # TTL: активность продлевает, простой блокирует, autolock в аудите
node test/access.mjs       # readonly / ask (approve, reject, cancel, нет сервиса)
node test/mode.mjs         # редактор патча профиля + prepare/commit/перезапуск/rollback
node test/scrub.mjs        # контейнер Zstandard и точечная замена
node test/dpapi.mjs        # реальный DPAPI round-trip и автозапуск без пароля

node берётся из рантайма DSH: ...\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe.

Тесты не трогают ваш настоящий сейф: каждый работает в своей временной папке и не читает ~/.dsh/secrets.

Формат файла

{
  "v": 1,
  "kdf": { "name": "scrypt", "N": 131072, "r": 8, "p": 1, "salt": "..." },
  "cipher": { "name": "aes-256-gcm", "nonce": "...", "tag": "..." },
  "ct": "...",
  "createdAt": "...", "updatedAt": "..."
}

Открытый payload: { "version": 1, "keys": {…}, "records": {…} }. AAD привязан к параметрам KDF, поэтому подмена заголовка ломает расшифровку. Файл, записанный до появления records, открывается с пустой картой записей.

Чего он не делает

  • Не прячет значения от модели навсегда. Маскирование — по точному совпадению строки; значение, перекодированное (base64, изменённый регистр), оно не поймает.
  • Не защищает от кода под вашей учётной записью. Пока хранилище разблокировано, значения доступны процессу DSH и всему, что он породит. DPAPI-печать не меняет это: она убирает пароль, а не доступ.
  • Не чистит историю мгновенно. Sweep работает по расписанию и пропускает свежие файлы; для «здесь и сейчас» есть scrub-cli.mjs --apply.
  • Пароль восстановить нельзя: забыли — хранилище потеряно (DPAPI-печать спасает, только если она была включена).

Совместимость

  • Формат хранилища v1. Сейф, созданный 0.1.x, открывается текущей версией: keys читаются, records появляется пустой. Обратная совместимость тоже есть — старый код открывает новый файл и видит ключи, — но сохранение из 0.1.x выбрасывает секцию records. Перед откатом на 0.1.x сделайте копию vault.json; проверяется это в node test/compat.mjs (нужен каталог старой версии, по умолчанию ~/.dsh/plugins/dsh-secrets.backup-0.1.1, иначе тест пропускается).
  • Конфиг: прежние конфиги без новых полей работают на значениях по умолчанию; неизвестное значение (accessMode: sometimes) намеренно валит активацию.
  • Панель: client.js и хост — один пакет, версии не расходятся; браузер получает скрипт с no-store.
  • DSH: используются только публичные швы (tools, shell, systemPrompt, webServer, функциональный плагин и ctx.provide), без импортов из @deepseek-ai/*. Проверено на desktop 0.2.1-rc.5; другие версии не проверялись.

Публикация

Имя dsh-secrets в npm занято (0.0.1, «name reserved» у другого автора), поэтому пакет называется dsh-sealed-secrets. Репозиторий и каталог данных остались прежними (GigaVik/dsh-secrets, ~/.dsh/secrets), внутренние маршруты /dsh-secrets/* и префикс плейсхолдера [[dsh-secrets:NAME]] — стабильные идентификаторы, они не переименовываются вместе с пакетом.

Перед публикацией в npm:

  1. заполнить repository, homepage, bugs, author в package.json;
  2. npm run test (и node test/compat.mjs, если есть старая версия);
  3. npm pack и посмотреть содержимое — files уже ограничивает пакет (lib/, патч, оба CLI, README, LICENSE);
  4. помнить, что установка даёт плагину доступ к секретам пользователя: README с границами и разделом «Чего он не делает» — часть поставки, а не приложение.
npm login
npm publish --access public

Лицензии

  • сам плагин — MIT (LICENSE);
  • ZIP создаёт 7-Zip через 7z-wasm (WASM-сборка, ~1.8 МБ). Лицензия 7-Zip: LGPL с ограничением unRAR; текст — в node_modules/7z-wasm/License.txt, ссылка на исходники: https://www.7-zip.org/;
  • readline-sync (зависимость 7z-wasm) — MIT.

7z-wasm — обычная зависимость npm: ставится вместе с плагином и не требует отдельной установки 7-Zip на машине.