Выпущен 0.1.4: N открытий, «доступно с», шаблоны, /verify и A2HS.
devops-tools/wrapped/wrapped-build/pipeline/head This commit looks good
devops-tools/wrapped/wrapped-deploy/pipeline/head This commit looks good

This commit is contained in:
Sergey Antropoff
2026-07-29 20:27:51 +03:00
parent 4389c0cea4
commit 54749e5e12
23 changed files with 709 additions and 60 deletions
+8
View File
@@ -36,6 +36,14 @@
<span data-i18n="admin.limits.auditRetention">Audit retention (days)</span>
<input name="audit_retention_days" type="number" value="{{ settings.audit_retention_days }}" />
</label>
<label>
<span data-i18n="admin.limits.maxOpens">Max opens per wrap</span>
<input name="max_opens_limit" type="number" min="1" max="10" step="1"
value="{{ settings.max_opens_limit or 3 }}" />
<small class="hint" data-i18n="admin.limits.maxOpensHint">
Ceiling for create UI (110). Default: 3.
</small>
</label>
</div>
</section>
+8
View File
@@ -31,6 +31,11 @@
</span>
</a>
<div class="top-actions">
<button type="button" class="icon-btn hidden" id="install-app" aria-describedby="install-app-tip">
<i class="fa-solid fa-mobile-screen-button" aria-hidden="true"></i>
<span class="sr-only" data-i18n="install.aria">Add to Home Screen</span>
<span class="ui-tooltip" id="install-app-tip" role="tooltip" data-i18n="install.tip">Add to Home Screen</span>
</button>
<button type="button" class="icon-btn" id="lang-toggle" title="Language" aria-label="Language">
<span id="lang-label">RU</span>
</button>
@@ -50,6 +55,7 @@
<p class="footer-copy">
© <span data-i18n="footer.copy.author">Сергей Антропов</span>
· <a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer">devops.org.ru</a>
· <a href="/verify" data-i18n="footer.verify">How to verify</a>
</p>
</footer>
@@ -65,6 +71,7 @@
<p data-i18n="about.p2">Можно отправить заметку или код, а также вложения: документы, архивы, скриншоты (drag-and-drop, выбор с диска или вставка из буфера). И текст, и файлы шифруются в браузере до загрузки — на сервер уходит только ciphertext.</p>
<p data-i18n="about.p3">Сервер никогда не видит plaintext: зашифрованные данные лежат на сервере до тех пор, пока получатель не откроет ссылку с ключом и не расшифрует пакет.</p>
<p data-i18n="about.p4">После успешной расшифровки копия на сервере уничтожается. Ключ шифрования живёт во фрагменте URL (#…) и не уходит на сервер вместе с запросом страницы. При желании wrap можно дополнительно защитить паролем.</p>
<p><a href="/verify" data-i18n="about.verifyLink">How to verify →</a></p>
</div>
<div class="modal-actions">
<button type="button" class="btn primary" data-about-close data-i18n="about.close">Понятно</button>
@@ -77,6 +84,7 @@
<script src="{{ static_url('js/theme.js') }}"></script>
<script src="{{ static_url('js/ui.js') }}"></script>
<script src="{{ static_url('js/about.js') }}"></script>
<script src="{{ static_url('js/install.js') }}"></script>
<script src="{{ static_url('js/highlight-ui.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
+18
View File
@@ -14,6 +14,11 @@
</div>
<label class="sr-only" for="payload-text" data-i18n="create.text">Text</label>
<div class="template-chips" id="template-chips" role="group" aria-label="Templates"></div>
<div class="field">
<label for="text-label" data-i18n="create.itemLabel">Label (optional)</label>
<input id="text-label" type="text" maxlength="120" data-i18n-placeholder="create.itemLabelPlaceholder" placeholder="e.g. Instructions" />
</div>
<div class="code-editor" id="code-editor">
<pre class="code-highlight" aria-hidden="true"><code id="code-highlight" class="hljs"></code></pre>
<textarea id="payload-text" class="code-input" spellcheck="false" data-i18n-placeholder="create.textPlaceholder" placeholder="Paste secrets, configs, notes…"></textarea>
@@ -31,6 +36,17 @@
<label for="ttl-seconds" data-i18n="create.ttl">Time to live</label>
<select id="ttl-seconds"></select>
</div>
<div class="field">
<label for="max-opens" data-i18n="create.maxOpens">Opens</label>
<select id="max-opens"></select>
</div>
</div>
<div class="grid-2">
<div class="field">
<label for="available-from" data-i18n="create.availableFrom">Available from (optional)</label>
<input id="available-from" type="datetime-local" />
<p class="hint" data-i18n="create.availableFromHint">Empty = available immediately</p>
</div>
<div class="field">
<label for="password" data-i18n="create.password">Password (optional)</label>
<div class="input-with-action">
@@ -83,6 +99,8 @@
</p>
</div>
<div id="success-meta-badges" class="success-meta-badges"></div>
<label for="share-link" data-i18n="create.shareLink">Share link</label>
<div class="copy-row">
<input id="share-link" readonly />
+5 -4
View File
@@ -42,15 +42,16 @@
</div>
<div id="result-panel" class="result-panel hidden">
<div class="success-callout" role="status">
<div class="success-callout" role="status" id="result-callout">
<span class="success-callout-icon" aria-hidden="true">
<i class="fa-solid fa-fire"></i>
<i class="fa-solid fa-fire" id="result-callout-fa"></i>
</span>
<span class="success-callout-body">
<strong data-i18n="unwrap.destroyedTitle">Server copy destroyed</strong>
<span data-i18n="unwrap.destroyedHint">Preview lives only in this browser session.</span>
<strong id="result-callout-title" data-i18n="unwrap.destroyedTitle">Server copy destroyed</strong>
<span id="result-callout-hint" data-i18n="unwrap.destroyedHint">Preview lives only in this browser session.</span>
</span>
</div>
<p class="hint success-trust" id="result-opens-hint"></p>
<p class="hint success-trust" data-i18n="unwrap.trustKey">
The key was only in the link #fragment and was never sent to the server.
</p>
+38
View File
@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block title %}{{ title }} · Wrapped{% endblock %}
{% block content %}
<section class="hero-panel verify-panel">
<div class="panel-head">
<p class="eyebrow" data-i18n="verify.eyebrow">Verify</p>
<h1 data-i18n="verify.title">How to verify Wrapped</h1>
<p class="lede" data-i18n="verify.lede">What leaves your browser, what stays on the server, and how the key in #fragment works.</p>
</div>
<div class="verify-sections">
<article class="verify-block">
<h2 data-i18n="verify.s1.title">Encryption in the browser</h2>
<p data-i18n="verify.s1.body">Text and files are packed and encrypted with Web Crypto (AES-GCM) before upload. The server receives only ciphertext plus metadata (TTL, MIME, size, optional password hash).</p>
</article>
<article class="verify-block">
<h2 data-i18n="verify.s2.title">Key in the URL fragment</h2>
<p data-i18n="verify.s2.body">The share link looks like /w/&lt;id&gt;#&lt;key&gt;. The part after # never reaches the server in the page request. Without that fragment (or the full wrapped token), ciphertext cannot be decrypted.</p>
</article>
<article class="verify-block">
<h2 data-i18n="verify.s3.title">Opens and destruction</h2>
<p data-i18n="verify.s3.body">By default a wrap can be opened once; then ciphertext is deleted. If the sender chose 23 opens, the server keeps ciphertext until the last successful unwrap. Expiry and password lockout still destroy the package.</p>
</article>
<article class="verify-block">
<h2 data-i18n="verify.s4.title">Optional password</h2>
<p data-i18n="verify.s4.body">When a password is set, the server checks an Argon2 hash before releasing ciphertext. Wrong guesses are limited; empty password does not burn an attempt.</p>
</article>
<article class="verify-block">
<h2 data-i18n="verify.s5.title">Available from</h2>
<p data-i18n="verify.s5.body">If “available from” is set, unwrap is rejected until that time. After that, normal open/expiry rules apply.</p>
</article>
</div>
<div class="actions actions-center">
<a class="btn primary" href="/" data-i18n="verify.createCta">Create a wrap</a>
</div>
</section>
{% endblock %}