Harden upstream merge (Phase 2): brand-asset baseline, sync-upstream gates, docs

Phase 2 of the merge-hardening effort (Phase 1 added bin/fork-check + the
fork-invariants manifest).

- config/brand_assets.sha256: checksum baseline of the WaboSign 'W' mark assets
  (the 6 logos + both apple-touch icons, which the old hardcoded 6-file restore
  list missed). bin/fork-check now verifies each asset's sha256 (catching a
  silent upstream overwrite that bypasses the text sweep) and warns about
  public/ brand-looking files not in the baseline. Single source of truth.

- bin/sync-upstream: restore brand assets by reading the baseline (not a
  hardcoded list, so new brand files are covered automatically); run BOTH
  rebrand-check and fork-check after the post-merge sweep, aborting with a
  remediation message and leaving the tree in place if either fails; optional
  RUN_TESTS=1 to run rspec; print a status summary + the human-judgment residue.

- .gitattributes: add the two apple-touch icons to the -merge brand list,
  kept in sync with the baseline.

- REBRANDING.md: replace the 21-item manual post-merge checklist (which was not
  run reliably) with an automated tier delegated to fork-check + the manifest,
  plus a slim human-judgment residue; add an agent sync runbook and an
  'Adding a new fork invariant' guide; correct the stale based_on attribution note.

- rebrand-check/rebrand-sync: allowlist/deny the new baseline file (its comment
  legitimately references DocuSeal's logo).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pull/687/head
Wabo 2 weeks ago
parent 6ef4a55bcd
commit 4cf91ff44a

6
.gitattributes vendored

@ -1,9 +1,13 @@
*.html linguist-detectable=false
# WaboSign brand binary files — never merge upstream versions; always keep ours
# WaboSign brand binary files — never merge upstream versions; always keep ours.
# Keep this list in sync with config/brand_assets.sha256 (the source of truth
# that bin/fork-check verifies and bin/sync-upstream restores from).
public/favicon.svg -merge
public/favicon.ico -merge
public/favicon-16x16.png -merge
public/favicon-32x32.png -merge
public/favicon-96x96.png -merge
public/logo.svg -merge
public/apple-touch-icon.png -merge
public/apple-touch-icon-precomposed.png -merge

@ -20,9 +20,9 @@ Scope: ~183 files modified. Decisions were made in a planning conversation befor
## AGPL §7(b) attribution
- `Wabosign::UPSTREAM_NAME = 'DocuSeal'` and `Wabosign::UPSTREAM_URL` constants added
- [app/views/shared/_powered_by.html.erb](app/views/shared/_powered_by.html.erb) and [app/views/shared/_email_attribution.html.erb](app/views/shared/_email_attribution.html.erb) now render a "based on DocuSeal (AGPLv3)" credit alongside the WaboSign brand
- [app/views/shared/_powered_by.html.erb](app/views/shared/_powered_by.html.erb) renders "WaboSign, a fork of DocuSeal" in the footer; [app/views/shared/_email_attribution.html.erb](app/views/shared/_email_attribution.html.erb) states the fork relationship in the email footer
- [app/javascript/submission_form/completed.vue](app/javascript/submission_form/completed.vue) carries the same credit on the post-signing completion screen
- New `based_on` i18n key added to all 14 language sections
- i18n keys `fork_of` and `product_name_is_a_fork_of_upstream_html` carry the credit text (English; other locales fall back via `config.i18n.fallbacks`). `bin/fork-check` asserts these surfaces keep the credit
- [README.md](README.md) and [LICENSE_ADDITIONAL_TERMS](LICENSE_ADDITIONAL_TERMS) rewritten
- New [NOTICE](NOTICE) file added crediting DocuSeal LLC and listing modifications
@ -73,103 +73,81 @@ Upstream lives at `docusealco/docuseal`. Each upstream release is brought in by
### Tooling
- [bin/rebrand-sync](bin/rebrand-sync) — Ruby script that performs the DocuSeal → WaboSign rename sweep across the working tree. Idempotent. Honors a deny-list (see §"Intentionally preserved upstream references" above) and sentinel-protects AGPL §7(b) attribution phrases, SDK custom-element names (`docuseal-form`, `docuseal-builder`), `@docuseal/*` npm packages, and the `github.com/docusealco/{fields-detection,pdfium-binaries,turbo}` binary URLs.
- [bin/rebrand-check](bin/rebrand-check) — fails (exit 1) if any unintended DocuSeal reference survives. Wired into [.github/workflows/ci.yml](.github/workflows/ci.yml) as the `Rebrand check` job.
- [bin/rebrand-check](bin/rebrand-check) — fails (exit 1) if any unintended DocuSeal *text* reference survives. Wired into CI as the `Rebrand check` job.
- [bin/fork-check](bin/fork-check) — fails (exit 1) if any **fork invariant** is broken: re-introduced Pro gate, deleted fork code, overwritten brand asset, lost attribution, dangling partial render, or PRESERVE↔ALLOW_PATTERNS drift. Driven by the declarative manifest [config/fork_invariants.yml](config/fork_invariants.yml). Wired into CI as the `Fork invariants` job. This is the executable form of the old manual post-merge checklist.
- [config/fork_invariants.yml](config/fork_invariants.yml) — the data behind `bin/fork-check`. Extend **this file** (not the script) when upstream adds a new gate; every entry carries a `why:`.
- [config/brand_assets.sha256](config/brand_assets.sha256) — checksum baseline of the WaboSign "W" mark assets. The single source of truth for "what is a brand asset": `bin/fork-check` verifies it, `bin/sync-upstream` restores from it, and [.gitattributes](.gitattributes) `-merge` should mirror it.
- `git config rerere.enabled true && git config rerere.autoupdate true` — once-per-checkout setup; remembers semantic conflict resolutions so the same call is not re-made each release.
- [.gitattributes](.gitattributes) marks `Gemfile.lock` and `yarn.lock` as `-merge` (regenerate after merge rather than diffing).
- [.gitattributes](.gitattributes) marks the brand binary assets as `-merge` (always keep ours; never blend an upstream version during a merge).
### Per-sync steps
### Runbook (for a human or AI agent)
The whole sync — fetch, branch from the tag, sweep, merge, restore brand assets,
re-sweep, and run both gates — is automated:
```sh
git fetch upstream --tags
git checkout -b sync/upstream-<tag> <tag> # e.g. 3.0.0
bin/rebrand-sync
git add -A && git commit -m "Apply WaboSign rebrand sweep to upstream <tag>"
bin/sync-upstream <tag> # e.g. bin/sync-upstream 3.0.2
# RUN_TESTS=1 bin/sync-upstream <tag> # also run rspec before declaring done
```
git checkout master
git merge --no-ff sync/upstream-<tag>
# Resolve conflicts. Rerere caches recurring resolutions.
When it stops, decide based on which gate failed:
1. **Merge conflict** — resolve it. `rerere` caches recurring resolutions.
2. **`rebrand-check` failed** — un-rebranded DocuSeal text survived. If a token
must be preserved, add it to `PRESERVE` (bin/rebrand-sync) **and**
`ALLOW_PATTERNS` (bin/rebrand-check) together (see below).
3. **`fork-check` failed** — a fork invariant broke. Each violation names the
file + the `why:` from the manifest:
- re-introduced gate → remove it;
- brand asset overwritten → `git checkout ORIG_HEAD -- <path>`;
- genuinely new upstream feature/gate → add a scoped invariant to
[config/fork_invariants.yml](config/fork_invariants.yml).
4. Re-run `bin/fork-check` (and `bin/rebrand-check`) until both print `ok`.
5. `bundle install && yarn install`, then **tag + push only when both gates and
`rspec` are green**: `git tag wabosign-synced-with-<tag> && git push origin master --tags`.
The equivalent manual steps (if you are not using the script) are: branch from
the tag, `bin/rebrand-sync`, commit, `git merge --no-ff` into master, restore
the brand assets listed in `config/brand_assets.sha256` from `ORIG_HEAD`,
`bin/rebrand-sync` again, then `bin/rebrand-check && bin/fork-check`.
# Restore WaboSign brand assets that the merge may have overwritten:
git checkout ORIG_HEAD -- public/favicon.svg public/favicon.ico \
public/favicon-16x16.png public/favicon-32x32.png \
public/favicon-96x96.png public/logo.svg
### Adding new preserved tokens
bin/rebrand-sync # catch upstream-only new files
bin/rebrand-check # CI gate
When upstream introduces a new SDK identifier, binary URL, or attribution surface that must survive the sweep, edit `PRESERVE` in [bin/rebrand-sync](bin/rebrand-sync) and `ALLOW_PATTERNS` in [bin/rebrand-check](bin/rebrand-check) together. The two must stay in sync — `rebrand-sync` decides what the sweep ignores, `rebrand-check` decides what CI tolerates. **This pairing is now CI-enforced:** `bin/fork-check` fails if a `PRESERVE` token containing "docuseal" has no matching `ALLOW_PATTERN`.
bundle install
yarn install
### Adding a new fork invariant
# Verify (see "Verification" in the plan), then:
git tag wabosign-synced-with-<tag>
```
When upstream re-introduces a gate, deletes fork code, or adds a brand asset, encode the rule in [config/fork_invariants.yml](config/fork_invariants.yml) — not in `bin/fork-check`. Use a path-scoped `must_not_contain` for re-added gates (never ban a token tree-wide unless it is genuinely unique to the gate — `Wabosign.multitenant?` is legitimate in ~19 views), `must_exist`/`must_not_exist` for files, and add new brand files to `config/brand_assets.sha256` (and the `.gitattributes` `-merge` list). Always include a `why:` — it is the institutional memory the next sync will need.
Or use the automated script:
```sh
bin/sync-upstream <tag>
```
## Post-Merge Verification
### Adding new preserved tokens
Most of what used to be a 21-item manual checklist is now executed by CI. After a
sync, the gates below must be green; only a short residue needs a human eye.
### Automated (CI — must pass)
- **`bin/rebrand-check`** — no unintended DocuSeal *text* survived the sweep.
- **`bin/fork-check`** — every fork invariant holds. The assertions (and the
rationale for each) live in [config/fork_invariants.yml](config/fork_invariants.yml):
attribution surfaces present; renamed identifiers + SDK tokens present; brand
assets match [config/brand_assets.sha256](config/brand_assets.sha256); no
re-introduced Pro gates (`ENTERPRISE_PATHS`, `console_redirect_index_path`, the
reminder `multitenant?` gate, …); placeholders / `console_redirect_controller` /
`lib/docuseal.rb` absent; SMS stack + `lib/ability.rb` present; no dead paywall
i18n keys; no dangling partial renders; PRESERVE↔ALLOW_PATTERNS in sync.
- **`rspec`** — suite passes (also catches Zeitwerk module conflicts at boot).
When upstream changes something the manifest does not yet know about, **extend
the manifest** (see "Adding a new fork invariant" above) rather than re-checking
by hand. That way the next sync inherits the protection.
### Human-judgment residue (not automatable)
When upstream introduces a new SDK identifier, binary URL, or attribution surface that must survive the sweep, edit `PRESERVE` in [bin/rebrand-sync](bin/rebrand-sync) and `ALLOW_PATTERNS` in [bin/rebrand-check](bin/rebrand-check) together. The two must stay in sync — `rebrand-sync` decides what the sweep ignores, `rebrand-check` decides what CI tolerates.
## Post-Merge Verification Checklist
Run through these checks after every upstream merge. The earlier failures are caught by `bin/rebrand-check`; the later ones require manual inspection or `rspec`.
### Automatic (`bin/rebrand-check`)
- Rebrand check passes (no unintended DocuSeal references)
- RSpec suite passes (360+ examples, 0 failures)
### Footer / Attribution
- [ ] `app/views/shared/_powered_by.html.erb` links both WaboSign *and* DocuSeal (upstream AGPL credit)
- [ ] `app/views/shared/_email_attribution.html.erb` uses WaboSign product name, not DocuSeal
- [ ] `app/javascript/submission_form/completed.vue` still has the hardcoded DocuSeal upstream credit
### Logo / Branding
- [ ] `app/views/shared/_logo.html.erb` shows the WaboSign "W" mark (not the DocuSeal abstract shape)
- [ ] `public/favicon.svg`, `public/logo.svg` show the WaboSign "W" mark
- [ ] `app/views/shared/_account_logo.html.erb` renders attached logo or falls back to the W mark
### Console / Plans / Pro / Upgrade
- [ ] `app/controllers/console_redirect_controller.rb` does not exist
- [ ] `config/routes.rb` has no `console_redirect`, `upgrade`, or `manage` routes
- [ ] `app/controllers/sessions_controller.rb` has no `console_redirect_index_path` call
- [ ] `lib/wabosign.rb` has no `CONSOLE_URL`, `CLOUD_URL`, or `CDN_URL` constants
- [ ] `app/views/shared/_settings_nav.html.erb` has no "Plans" link or "Pro" badge
- [ ] `app/views/shared/_navbar.html.erb` has no "Console" link in dropdown
- [ ] `app/views/shared/_navbar_buttons.html.erb` has no "Upgrade" button
- [ ] No view file contains `unlock_with_docuseal_pro`, `activate_with_docuseal_pro`, or `console_redirect_index_path`
### Feature Gates (all freely available)
- [ ] `app/views/sms_settings/index.html.erb` shows provider form (BulkVS/Twilio/VoIP.ms/SignalWire) — not a placeholder
- [ ] `app/views/personalization_settings/_logo_placeholder.html.erb` shows upload form — not a Pro upsell
- [ ] `app/views/notifications_settings/_reminder_placeholder.html.erb` is empty (reminder form renders freely)
- [ ] `app/views/submissions/_bulk_send_placeholder.html.erb` is empty (bulk send freely available)
- [ ] `app/views/submissions/_send_sms_button.html.erb` is a functional button (not Pro-gated tooltip)
- [ ] `app/views/users/_role_select.html.erb` has no disabled options or Pro upsell link
- [ ] `app/views/accounts/show.html.erb` has no console-redirect Pro gates on Decline/Delegate toggles
### Google SSO
- [ ] `app/views/sso_settings/index.html.erb` shows the Google SSO config form (client_id, client_secret, allowed_domains)
- [ ] `app/views/devise/sessions/_omniauthable.html.erb` has the "Sign in with Google" button
- [ ] `app/views/sso_settings/_placeholder.html.erb` does not exist (was replaced by the real form)
- [ ] OmniAuth routes for `auth/google_oauth2` are present in `config/routes.rb`
### E-Signature Settings
- [ ] `app/views/esign_settings/_default_signature_row.html.erb` does not exist
- [ ] `config/locales/i18n.yml` has no `wabosign_trusted_signature` or `sign_documents_with_trusted_certificate_*` keys
### Social / Extras
- [ ] `app/views/shared/_github.html.erb` does not exist (no hardcoded star count)
- [ ] `app/views/shared/_navbar.html.erb` does not render `shared/github` or `shared/github_button`
- [ ] `app/views/shared/_settings_nav.html.erb` has no Discord or AI Assistant links in support channels
- [ ] `config/locales/i18n.yml` has no `discord_community` or `ai_assistant` keys
### SMS (independently developed)
- [ ] `app/views/sms_settings/index.html.erb` is the full provider form (not placeholder)
- [ ] `lib/sms.rb` exists with all 4 providers (BulkVS, Twilio, VoIP.ms, SignalWire)
- [ ] `lib/sms/providers/` directory exists with all 4 provider implementations
- [ ] `app/controllers/sms_settings_controller.rb` handles `test_message` action
- [ ] `app/models/encrypted_config.rb` has `SMS_CONFIGS_KEY = 'sms_configs'` constant
- [ ] `config/routes.rb` has the SMS routes with `test_message` collection route
- [ ] The rendered WaboSign "W" mark *looks* right (checksum proves the file is
unchanged; a human confirms it is the intended asset, e.g. after a
deliberate brand update + baseline regen).
- [ ] Upstream did not introduce a genuinely **new** feature or freemium gate
that needs a fork-policy decision (free it, and add an invariant) — skim
the merge diff for new `multitenant?` / Pro / Console / placeholder code.
- [ ] New upstream **UI strings** read correctly after the sweep (the rename is
mechanical; some phrasings need a human's rebranding nuance).

@ -15,6 +15,7 @@
require 'find'
require 'set'
require 'yaml'
require 'digest'
ROOT = File.expand_path('..', __dir__)
MANIFEST = File.join(ROOT, 'config/fork_invariants.yml')
@ -86,6 +87,7 @@ end
manifest = YAML.safe_load(File.read(MANIFEST)) || {}
violations = []
warnings = []
# 1. Files that must exist (fork code + brand assets upstream tends to delete).
Array(manifest['must_exist']).each do |path|
@ -219,6 +221,49 @@ else
end
end
# 9. Brand-asset checksums: each file in config/brand_assets.sha256 must exist
# and match its recorded hash, or an upstream merge silently overwrote our
# "W" mark. Binary, so it bypasses the text sweep — this is the only guard.
BRAND_BASELINE = 'config/brand_assets.sha256'
baselined = []
if File.exist?(abs(BRAND_BASELINE))
read_lines(abs(BRAND_BASELINE)).each do |line|
next if line.strip.empty? || line.lstrip.start_with?('#')
digest, path = line.strip.split(/\s+/, 2)
next if digest.nil? || path.nil?
baselined << path
unless File.exist?(abs(path))
violations << "brand_asset: missing baselined asset: #{path} (restore: git checkout ORIG_HEAD -- #{path})"
next
end
actual = Digest::SHA256.file(abs(path)).hexdigest
next if actual == digest
violations << "brand_asset: #{path} checksum mismatch — overwritten? " \
"(restore: git checkout ORIG_HEAD -- #{path}, or regenerate the baseline if intentional)"
end
end
# 10. Glob detector (warn-level): a public/ image that looks like a brand asset
# but is not in the baseline — upstream may have added it; decide whether to
# rebrand + baseline it, or leave it. Never fails the build.
brand_globs = %w[public/favicon* public/logo* public/apple-touch-icon*]
brand_globs.each do |g|
Dir.glob(abs(g)).each do |p|
next unless File.file?(p)
rel = p.sub(%r{\A#{Regexp.escape(ROOT)}/}, '')
next if baselined.include?(rel)
warnings << "brand-asset candidate not in #{BRAND_BASELINE}: #{rel} " \
'(add the WaboSign version to the baseline, or confirm it is upstream-neutral)'
end
end
warnings.each { |w| warn "fork-check: warning: #{w}" } unless warnings.empty?
if violations.empty?
puts 'fork-check: ok'
exit 0

@ -57,6 +57,7 @@ ALLOW_FILES = Set.new([
# (e.g. lib/docuseal.rb, docuseal-form) as markers to assert on.
'bin/fork-check',
'config/fork_invariants.yml',
'config/brand_assets.sha256',
'lib/wabosign.rb',
'lib/docuseal.rb',
# Migration that finds rows by the legacy docuseal_aatl name.

@ -55,6 +55,7 @@ DENY_PATHS = Set.new([
# marker), or the guard would silently stop catching that regression.
'bin/fork-check',
'config/fork_invariants.yml',
'config/brand_assets.sha256',
# WaboSign brand logo files — must never be touched by the sweep;
# restored from ORIG_HEAD by bin/sync-upstream after an upstream merge.
'public/favicon.svg',

@ -56,35 +56,67 @@ echo "=== Merging into master ==="
git checkout master
git merge --no-ff "sync/upstream-$TAG" -m "Merge upstream $TAG into master"
echo "=== Restoring WaboSign binary assets overwritten by merge ==="
# Merging an upstream tag may overwrite our brand logo files that rebrand-sync
# cannot protect (they are binary / opaque-image and bypass the text sweep).
# Restore them from pre-merge master (ORIG_HEAD).
LOGO_FILES=(
public/favicon.svg
public/favicon.ico
public/favicon-16x16.png
public/favicon-32x32.png
public/favicon-96x96.png
public/logo.svg
)
for f in "${LOGO_FILES[@]}"; do
if git show ORIG_HEAD:"$f" &>/dev/null 2>&1; then
echo "=== Restoring WaboSign brand assets overwritten by merge ==="
# Merging an upstream tag may overwrite our brand files that rebrand-sync cannot
# protect (binary / opaque-image, they bypass the text sweep). The authoritative
# list lives in config/brand_assets.sha256 — derive it from there so a new brand
# file added to the baseline is automatically restored too. Restore from the
# pre-merge master (ORIG_HEAD).
RESTORED=()
mapfile -t BRAND_FILES < <(awk '!/^#/ && NF {print $2}' config/brand_assets.sha256)
for f in "${BRAND_FILES[@]}"; do
if git show "ORIG_HEAD:$f" &>/dev/null; then
git checkout ORIG_HEAD -- "$f"
RESTORED+=("$f")
echo " restored: $f"
fi
done
echo "=== Catching new upstream files (post-merge sweep) ==="
bin/rebrand-sync
bin/rebrand-check
echo "=== Verifying invariants ==="
# Both checks must pass before this sync can be considered clean. With
# `set -e` a failure already aborts, but wrap them so the operator/agent sees a
# clear remediation path and the half-merged tree is left in place to inspect.
if ! bin/rebrand-check; then
echo "" >&2
echo "!! rebrand-check failed: un-rebranded DocuSeal text survived the sweep." >&2
echo " Inspect the lines above; if a token must be preserved, add it to" >&2
echo " PRESERVE (bin/rebrand-sync) AND ALLOW_PATTERNS (bin/rebrand-check)." >&2
echo " The merge is left in place for you to fix; do NOT push until green." >&2
exit 1
fi
if ! bin/fork-check; then
echo "" >&2
echo "!! fork-check failed: a fork invariant was broken by this merge." >&2
echo " Each violation above names the file + the reason (from" >&2
echo " config/fork_invariants.yml's why:). Typical fixes:" >&2
echo " - re-introduced gate -> remove it" >&2
echo " - brand asset overwritten -> git checkout ORIG_HEAD -- <path>" >&2
echo " - new upstream feature -> add a scoped invariant to the manifest" >&2
echo " The merge is left in place for you to fix; do NOT push until green." >&2
exit 1
fi
if [ "${RUN_TESTS:-0}" = "1" ]; then
echo "=== Running test suite (RUN_TESTS=1) ==="
bundle exec rspec
fi
echo ""
echo "============================================================"
echo "Sync of $TAG complete."
echo "Next steps:"
echo " 1. bundle install && yarn install"
echo " 2. Run tests: bundle exec rspec"
echo " 3. Tag: git tag wabosign-synced-with-$TAG"
echo " 4. Push: git push origin master --tags"
echo "Sync of $TAG complete. Invariants: rebrand-check + fork-check PASS."
echo "Brand assets restored: ${#RESTORED[@]}"
if [ "${RUN_TESTS:-0}" != "1" ]; then
echo "Tests: not run (set RUN_TESTS=1 to run rspec, or rely on CI)."
fi
echo ""
echo "Remaining human-judgment review (see REBRANDING.md \"Human-judgment\"):"
echo " - Does the rendered WaboSign 'W' mark look right?"
echo " - Did upstream add a genuinely new feature/gate needing a fork policy call?"
echo " - Do new upstream UI strings need rebranding nuance the sweep can't infer?"
echo ""
echo "Then: bundle install && yarn install, tag, and push:"
echo " git tag wabosign-synced-with-$TAG && git push origin master --tags"
echo "============================================================"

@ -0,0 +1,23 @@
# WaboSign brand-asset checksum baseline.
#
# bin/fork-check verifies every file listed here exists and matches its sha256,
# failing if one was silently overwritten by an upstream merge (the "W" mark
# replaced by DocuSeal's logo). bin/sync-upstream restores these from ORIG_HEAD
# after a merge. This is the single source of truth for "what is a brand asset":
# .gitattributes (-merge) and the sync-upstream restore list both derive from it.
#
# google_g.svg is intentionally NOT here — it is Google's logo for the SSO
# button, legitimately shared with upstream, not a WaboSign rebrand target.
#
# Regenerate after an intentional brand change (review the diff deliberately):
# sha256sum $(awk '!/^#/{print $2}' config/brand_assets.sha256) > /tmp/b && \
# { grep '^#' config/brand_assets.sha256; cat /tmp/b; } > config/brand_assets.sha256
#
0a269a2b86c2413c08c10e8ab75ce0349d9cc59cc470613696d54480a4688ef0 public/favicon.svg
b78aa6db776b326df6ac48ba75f50541fb3cbcd09c78dc99e14a695ab80b84d6 public/favicon.ico
8fd83aa2a2f56b993e680a1babb5d6984d3ec8b72b40ac470cf4f3acff9f9ed9 public/favicon-16x16.png
e8d9b8fe8346b12dd34eed410a4113992b7708d844441b15f607c49c0574f825 public/favicon-32x32.png
204023b29fdf57cf45ef2352ddb56964ed35ab1be7d58c0dd3ab1b2a45cc6671 public/favicon-96x96.png
0a269a2b86c2413c08c10e8ab75ce0349d9cc59cc470613696d54480a4688ef0 public/logo.svg
9b9507aaefcbe06b58c464724d418940c610df480e75b4cc1a58d1939d5d2142 public/apple-touch-icon.png
9b9507aaefcbe06b58c464724d418940c610df480e75b4cc1a58d1939d5d2142 public/apple-touch-icon-precomposed.png
Loading…
Cancel
Save