Commit Graph
226 Commits
Author SHA1 Message Date
devsyncwrldandClaude Fable 5.1 ef9c73501c feat(profile): a full identity system — name styles, decorations, effects, nameplates, tag, links, board
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The owner asked for profile customisation that is "completo e FODA", pointing
at Discord's profile editor. This is the whole thing, end to end, with a live
preview.

What a user can now set, and where it shows:

  - Display name style: 10 fonts × 9 effects (solid, gradient, neon, toon,
    pop, gummy, prism, glitch, fire) × two colours. Popout and full profile.
  - Avatar decoration: 10 (ring, neon, flames, stars, halo, vines, pixel,
    hearts, frost, crown). Inline SVG — no image assets, scales to any size.
  - Profile effect over the banner: 9 (sparkles, rain, snow, matrix,
    confetti, aurora, embers, bubbles, hearts). CSS particles with seeded
    positions so re-renders do not change the weather.
  - Nameplate behind the member-list row: 10 presets + custom two-colour.
  - Theme: two colours washed behind the card, and the banner fallback.
  - Pronouns, and a clan-style tag (emoji + 2–4 letters) beside the name,
    also on the member row.
  - Links, up to 5, with the brand icon picked from the host.
  - Board: favourite game, up to 5 in rotation, up to 10 wanted, a quote.
    Games come from Steam search through a small server proxy (cached,
    10 min) or are typed by hand; covers only load from Steam's CDN.
  - Badges, derived rather than stored: instance admin, server owner,
    founder (joined in the space's first month), veteran (a year), OG (six
    months), stylist. Nothing to grant, revoke or migrate.

Storage is one JSON column, users.profile_style (migration 0019), and one
normaliser in shared/profile.ts that both the server (on write and on read)
and the client (on render) run. It never throws: unknown ids fall back,
strings are cut to their limits, colours must be #rrggbb, link URLs must be
https, cover URLs must be a Steam CDN host. Kept out of the federation
durable-field set on purpose — peers on older builds do not know the field,
and it is cosmetic.

The editor is a new Settings > Profile tab. Its preview is the same
ProfileCard component the popout renders, with the draft style swapped in —
what you see while editing is, by construction, what others see.

Fonts are OFL builds self-hosted from public/fonts alongside DM Sans, latin
subsets only, ~120 KB in total.

Verified: web build, 517 web tests (11 new: normaliser bounds and fallbacks,
link icon detection, badge derivation), server typecheck, 804 server tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:36:23 -03:00
devsyncwrldandClaude Fable 5.1 872066e5be feat: rebrand to Resenhacord with a new logo, and post a notice when a message is pinned
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Rebrand. The product name is now Resenhacord everywhere a person reads it:
window title, tray, notifications, About panel, PWA manifest, page title,
recovery and instance-picker screens, and the four i18n strings that named
the app. Code identifiers (BackspaceElectronAPI, @backspace/* workspace
names, the federation protocol) are unchanged — they are not a brand, and
renaming them buys nothing but churn. AGPL notices and upstream attribution
stay as they were.

Two rename details that would each have hurt real users:

  - electron-builder's productName changed but appId did not. NSIS keys the
    installation on appId, so the updater replaces the Backspace install in
    place instead of leaving a second app beside it.
  - userData is keyed on app name, so the rename would have moved every
    install to an empty profile — logged out, instance URL gone. main.ts now
    walks a migration chain ("@backspace/desktop" → "Backspace" →
    "Resenhacord"), each step a no-op once its source is gone, and one whose
    target already holds data leaves it alone. Autostart lookups accept the
    old registry entry name too.

Logo. Two speech bubbles in conversation, violet behind, rose in front with
three dots — the front one is talking. Sources are hand-written SVG in
assets/brand (square viewBox; the gap between bubbles is a mask, not paint,
so the alpha-only macOS template keeps the silhouette). PNG sources are
rendered from the icon SVG and the whole set — .icns, .ico, Linux PNGs, tray
variants, favicons, PWA, maskable — comes from the existing gen-icons
pipeline unchanged.

Pin notice. Pinning a message now posts "X pinned a message to this channel.
See all pinned messages." into the channel, the way Discord does, with two
links: the message itself (jumps to it) and the pins panel. Space messages
had no `type` column — only DM messages carried system events — so this adds
one (migration 0018, default 'user', verified against a copy of the
production database: 88 rows, all 'user'). The route inserts a type='system'
row alongside the pin and broadcasts it as message_created, so the client
renders it with the SystemMessage renderer it already has. Unpinning posts
nothing; it is housekeeping, not news. System notices are excluded from the
notification toast and the message sound — they are not from anyone.

The pins panel and jump target are local state in MainContent; a notice deep
in the timeline reaches them through two small uiStore requests (a counter
and an id), neither persisted.

Verified: web build, 506 web tests (2 new), server typecheck, 804 server
tests, desktop typecheck.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:15:14 -03:00
devsyncwrldandClaude Opus 5 a41a944742 feat(chat): @everyone mention
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Rides the existing `<@id>` token: `everyone` already matches the parser's
`[a-zA-Z0-9_-]+`, so the markdown pre-processor, the inline renderer and the
badge pick it up with no second syntax. User ids are snowflakes, so the
literal `everyone` cannot collide with one.

Gated on a new MENTION_EVERYONE permission rather than being open to all —
otherwise a role can never take it away. Granted to @everyone by default: on
a small instance the ping is a convenience, not a weapon, and an owner who
disagrees can deny it and grant it to a role instead.

Two things that would each have made the gate a no-op:

  - DEFAULT_EVERYONE_PERMISSIONS only applies at space creation, so spaces
    that predate the bit would have had every @everyone mention silently
    stripped. A boot-time backfill ORs the bit into @everyone roles only
    (`id = space_id`), leaving other roles alone so a later denial is not
    overruled on the next restart. Verified against a copy of the production
    database: three roles moved by exactly 2^15, non-@everyone roles
    untouched, second pass a no-op.
  - Enforcement covers edit as well as create; without that, posting a plain
    message and editing it is the way around the permission.

A message that lacks the permission is not rejected — losing what someone
wrote over one token is worse than posting it. The token is demoted to the
literal `@everyone` text they typed: the message reads the same and rings
nobody.

Code spans and fences never ping. People paste payloads and snippets into
chat, and a paste should not ring everyone's client. The same rule decides
the row highlight and the notification sound, so the three cannot disagree.

Verified: web build, server typecheck, 504 web tests, 804 server tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:10:56 -03:00
devsyncwrldandClaude Opus 5 699855849d feat(i18n): translate the five systems users actually see
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Ordered by what the group hits, not by string count.

  - Chat system messages ("X added Y to the group") and their sidebar
    preview twin. These are content, not chrome, so they sat in English in
    the middle of conversations.
  - Login, register and the invite landing page — the first screen anyone
    invited to the instance sees.
  - Voice — controls, connection info, the mobile join sheet, settings tab.
  - Mobile screens.
  - Settings navigation across space, user and instance, plus Discovery and
    join requests.

Three call sites build strings outside a component and so read the locale
directly rather than through the hook: the sidebar preview formatter, the
connection-info track labels, and MobileShell's screen map — which holds
plain functions that are *called*, not rendered, where a hook would run
under whichever component invoked the map. Each says so at the definition;
all three re-render often enough that a language switch shows through
promptly.

Adds a test for a mistake made three times in this series: a file with an
exported component plus a private sub-component, where only the exported one
got the hook. tsc catches it, but only after a full build and without naming
the component. The test names it, and also flags the inverse — a hook placed
below an early return, which tsc accepts and React breaks at runtime (this
project shipped that once, as error #310). Verified by mutation: removing a
hook makes it fail and print the component.

Dictionary is at 856 keys, en and pt-BR at exact parity.

Verified: web build (tsc + vite), 492 web tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:13:54 -03:00
devsyncwrldandClaude Opus 5 94f5da4013 feat(i18n): translate five more systems, and unify the permission labels
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
- Space settings > Overview — identity, icon, banner, transfer, delete
  - Space settings > Members and Bans
  - Space settings > Roles, including the whole permission table
  - Category settings — rename, privacy, overrides, delete
  - Instance settings > General — name, discovery, GIF search

The permission labels were duplicated: the roles panel carried one English
list and the channel/category override editors carried another. Both now
read a single `perm.*` set, so renaming a permission lands in all three at
once instead of one and a half. This removes the `channel.perm.*` keys added
two commits ago rather than adding a third copy beside them.

Same pattern as before for anything chosen outside the render: the roles
panel's permission groups and the category's override definitions hold keys
and resolve them where they render. The category delete dialog emphasises
the category name, so its sentence is split around the placeholder rather
than interpolated.

Dictionary is at 723 keys, en and pt-BR at exact parity.

Verified: web build (tsc + vite), 490 web tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:49:25 -03:00
devsyncwrldandClaude Opus 5 4fd6ac890c feat(i18n): translate five more systems
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
- Settings > Voice: video — preview, permission states, camera picker.
    This closes the Voice & Video tab; input, output and video are all done.
  - Create channel / category / space
  - Group DMs — new DM, add friends, group settings, leave
  - Join a space — discovery, invite code, and both federated auth phases
  - Delete account, transfer ownership, and the top-level error screen

Three patterns worth noting. Strings that are chosen in a helper or held in
state (the camera's hard-blocked copy, its preview errors, the space
visibility options, the invite failure reasons) now travel as translation
keys and are resolved where they render, so switching language re-labels
what is already on screen. Sentences that wrap an emphasised literal — the
host name when joining, the space and member names when transferring — are
split around their placeholder instead of interpolated, leaving word order
to the translation. And `CreateChannel` mapped over `(t) =>` for the channel
type, which shadowed the translator; the loop variable is now `channelType`.

The error boundary in main.tsx is a class component, so it reads the locale
once per render via `translate` rather than the hook — correct here anyway,
since an error screen has no reason to re-render on a language switch.

Two GroupDmSettings tests located the name input by its aria-label, which
this change translates. They now use the `data-group-dm-name-input`
attribute the input already carries, matching how the rest of that file
locates elements — and no longer break when copy changes.

Dictionary is at 595 keys, en and pt-BR at exact parity.

Verified: web build (tsc + vite), 490 web tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:38:54 -03:00
devsyncwrldandClaude Opus 5 115c81cba3 feat(i18n): translate five more systems, and check for updates more often
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Update cadence: the desktop app checked once ten seconds after launch and
then every four hours. A release published while the app sits open went
unnoticed until the next poll — which is exactly what happened with 1.4.0.
It now polls every 30 minutes and also checks when the window regains focus,
throttled to one check per five minutes so alt-tabbing does not hammer the
feed. `lastCheckAt` is stamped from the `checking-for-update` event rather
than by the callers, so a manual check from the settings panel counts too.
The focus listener is on `app`, not on mainWindow, because the window is
destroyed and recreated when closed to tray.

Translations, five whole systems:
  - Settings > Desktop (autostart, updates, instance)
  - Settings > Voice: output device, which completes that tab
  - Settings > Keybinds, including the action names and conflict prompt
  - Settings > Account, the largest — profile, password, detached-instance
    notice, danger zone
  - Explore spaces

Two sentences wrap a literal that must not be translated (the `input` group
name, the `user@instance` example). Those are split around a placeholder
rather than interpolated as text, so a translation keeps control of the word
order on both sides of it. Keybind action labels gained a `labelKey`
alongside the English `label`, which stays for non-React callers.

Verified: web build (tsc + vite), desktop typecheck, 490 web tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 13:38:28 -03:00
devsyncwrldandClaude Opus 5 860f02b2c1 feat(channels): rename channels, and quieter defaults for sound and streams
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Four owner requests in one pass.

Rename: the PATCH /api/channels/:id route already accepted a name and a
topic, and the API client already exposed it — the only thing missing was a
way to reach it. The channel settings Overview tab now edits both, gated on
MANAGE_CHANNELS like the server is, with Save/Discard appearing only when
the form is dirty. The server's lowercase-and-hyphenate rule moved to
@backspace/shared so the field can show what a name will actually become
instead of letting it change shape on save; one copy of the rule, used by
both sides.

Stream volume: someone else's screen share now starts at 50 instead of 100
(DEFAULT_STREAM_VOLUME). The publisher boosts system audio before sending,
so the old default arrived far too loud for a viewer who never touched the
slider. Only the default moves — the slider still covers the full range and
a value the user set is still persisted.

Sound effects: SFX_BASE_VOLUME 0.8 -> 0.4, halving every cue. The .ogg files
were left alone; they already peak below full scale, so the harshness was
level, not recorded distortion. Every SFX playback path goes through
getSfxVolume(), so this is the single knob.

Translations: channel settings (including the override editor's permission
labels), the friends page end to end — tabs, lists, requests, the add tab
and discovery cards — the invite modal including its per-invite failure
reasons, and the group-DM member actions. Failure reasons are carried as
translation keys rather than finished text, so results already on screen
re-label when the language changes.

Verified: web build (tsc + vite), server typecheck, 490 web tests, 804
server tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:59:25 -03:00
devsyncwrldandClaude Opus 5 7f08384372 feat(expressions): pickers and upload for emojis and stickers
Completes the feature: the tables existed but nothing could be put in them.

Space settings gain an Emojis & Stickers panel behind MANAGE_SPACE, with a
512KB ceiling — both are fetched on every message that uses them, so weight
matters more than fidelity. The suggested name is pre-normalised so the common
case needs no typing, and a name collision reports itself distinctly from an
upload failure: the corrective action is different.

Custom emojis join the emoji picker as their own category. They have no native
character, so selecting one inserts :name: — the same text the renderer
resolves back to an image, which also means copying a message yields something
that still reads.

Stickers get a picker tab that only appears inside a space, since that is where
they exist, and send immediately on click: a sticker is the whole message, so
parking it in the composer to await Enter would make no sense.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:25:10 -03:00
devsyncwrldandClaude Opus 5 50a8f12c77 feat(search): inline filters, and translate the search UI
The filters themselves already existed end to end — the server takes q, from,
has, before and after, the API client passes them, and the popover has fields
for each. What it lacked was discovery: the panel sits behind a button, so the
capability was invisible.

Typing 'de:fulano' or 'has:image' straight into the search box now applies the
same filters. Keys are accepted in both languages, since the app is bilingual,
and an unrecognised token falls back to being search text — otherwise a message
containing a URL or 'algo:coisa' would become unfindable.

Inline filters win over the panel's: whoever just typed one is expressing the
more recent intent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 17:49:05 -03:00
devsyncwrldandClaude Opus 5 c4158ee245 docs: retire the fixes that shipped and were confirmed in use
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 17:13:30 -03:00
devsyncwrld 61c68761df i18n: translate the channel and member sidebars
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Clears the two remaining rounds owed under the fork's per-update translation
rule.

Covers channels, direct messages, the context menus, the inline voice settings
and the member list's loading and grouping labels.

The member grouping needed care: one function returns either a fixed label or
a role's own name, and role names are user data that must never be translated.
It now marks only the fixed cases and the component resolves them at render,
leaving role names untouched. The function is not a component, so it cannot
call the hook itself.
2026-09-01 17:03:48 -03:00
devsyncwrld c04b8b47eb i18n: translate the chat composer and messages
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Catching up on the fork's rule that every update also translates one existing
system — three updates went by without one.

The composer's placeholder takes the channel as a parameter rather than being
concatenated, since Portuguese puts the channel elsewhere in the sentence than
English does.

The translation hook is bound to `tr` in these two files, not `t`: both already
use `t` for transfer records in inner scopes, which shadowed the function and
made it uncallable exactly where it was needed.
2026-09-01 16:59:50 -03:00
devsyncwrld d208b0c277 build(desktop): publish releases to syncwrld/resenhacord
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-09-01 13:14:43 -03:00
devsyncwrld 2f836cd366 build(desktop): correct the fork's GitHub owner to syncwrld
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-09-01 13:11:49 -03:00
devsyncwrld f7f2cf75ab build(desktop): point releases at the fork and rebuild the new native module
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
release.yml already builds and publishes desktop installers for every platform
on a v* tag, so no new workflow was needed — it needed adapting to the fork.

electron-builder published to upstream's repository, so releases (and with them
the electron-updater feed, which had no source at all) went nowhere useful.

More consequential: postinstall ran electron-rebuild against uiohook-napi only.
electron-native-screenshare would have been compiled for Node's ABI rather than
Electron's and failed to load at runtime — and since the loader degrades
quietly by design, the symptom would have been screen sharing with no sound and
no error, which is the exact bug this module exists to fix.

Documented in docs/build-desktop.md, including why the build has to run on
GitHub (Gitea Actions provides no hosted runners, and the module needs MSVC)
and why the Windows runner stays pinned to windows-2022.
2026-09-01 13:10:10 -03:00
devsyncwrld d525bbb8c5 feat(desktop): capture system audio with process isolation
Electron's audio: 'loopback' captures the whole output mix, this app's own
playback included — so everyone else's voices went back out inside the share
and each listener heard themselves. Not acoustic echo but a digital copy of
the output, which is why headphones never helped, and why shareAudio already
defaulted to off in the desktop app.

Electron offers no way to exclude our own audio: the docs allow only
'loopback' or 'loopbackWithMute', and the handler discards the renderer's
constraints (restrictOwnAudio never arrives). electron-native-screenshare does
it at the OS level — WASAPI process loopback on Windows — capturing only the
shared window when its pid resolves, and otherwise everything except us.

The module hands raw PCM to the main process, so it crosses IPC and is
scheduled onto a running cursor in Web Audio to become a MediaStreamTrack,
published as ScreenShareAudio. Loading is optional and failure degrades to a
silent share rather than blocking the app or the screen share.

The browser path is untouched: Chrome honours restrictOwnAudio and has no echo.

Verified by typecheck (web and Electron main) and the web suite. The audio path
itself cannot be exercised here — no Windows, no Electron, no audio device.
2026-09-01 00:56:34 -03:00
devsyncwrld 310e9b86be docs: diagnose the screen-share audio echo
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 22:58:59 -03:00
devsyncwrld e291b5e411 docs: drop delivered rows from the pending sections
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 22:40:44 -03:00
devsyncwrld c899253e52 fix: soundboard upload in Electron, and Spotify sync/disappearing/progress
Soundboard: naming a clip used window.prompt, which Electron does not
implement — it returned nothing, the flow aborted in silence, and adding a
sound worked in the browser while doing nothing at all in the desktop app.
Replaced with a two-step field inside the popover, identical in both.

Spotify, three separate defects behind the two symptoms reported:

Out of sync — a 20s poll stacked on the activity store's 5s debounce left
everyone else on the previous track for up to 25s. The next poll is now
scheduled just past the current track's end instead of on a fixed interval,
and a track change bypasses the debounce (it happens once every few minutes;
the debounce exists for chatty producers).

Vanishing — a paused track, and the silent gap Spotify reports between two
songs, both cleared the activity outright. Pausing is now carried as state
rather than absence, and an empty answer is tolerated for 25s before the
block comes down.

Progress bar — timestamps are computed with the server's clock and were drawn
against the viewer's, so any drift displaced the bar; and it kept advancing
after a pause until the next poll. The ready payload now carries server time
so each client can correct its own offset, and the bar freezes when paused.

Tray, native notifications and system audio in screen share were all found
already implemented and wired end to end; recorded in the roadmap rather than
built again.
2026-08-31 22:39:28 -03:00
devsyncwrld ff55d9d486 docs: plan desktop features and diagnose the Spotify sync bugs
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 14:02:21 -03:00
devsyncwrld e58021408c docs: record the Electron window.prompt soundboard bug
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 14:00:41 -03:00
devsyncwrld 2afe3453f0 docs: record four approved features not yet started
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 13:57:01 -03:00
devsyncwrld ef5545465d docs: mark GIF favourites, audit log and statistics delivered
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 13:30:26 -03:00
devsyncwrld 0fc6abeb6e docs: mark Spotify delivered, drop resolved items
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 13:04:35 -03:00
devsyncwrld c022f2795f docs: record the i18n foundation and translation queue
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 12:25:03 -03:00
devsyncwrld 89e13441c8 docs: record the pt-BR language rule and new system ideas
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 12:18:33 -03:00
devsyncwrld b92a0d837e docs: update roadmap after mic test and activity work
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 12:05:18 -03:00
devsyncwrld 37407a5ecd feat(voice): open the profile card from voice participants
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The profile popout already existed and was reachable from eleven places —
messages, mentions, avatars, member list, DMs, activity panel — but no voice
surface opened it, so clicking someone during a call did nothing.

Wire it into the voice user rows (VoiceChannel's sidebar list) and the name
label on grid tiles, whose avatar was already a ProfileAvatar; the name beside
it not reacting read as the click failing.

Left mobile alone deliberately: MobileSpacesScreen already opens the profile
from its row wrapper, and MobileVoiceJoinSheet would layer a history-pushed
full-screen profile inside a bottom sheet, which cannot be verified here.
2026-08-31 11:49:57 -03:00
devsyncwrld cad3867027 docs: add fork roadmap
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
2026-08-31 11:45:02 -03:00
cnrd 8456b8f976 fix(web): stop the profile card re-anchoring to its own avatar (#39)
Avatar opened the profile popout whenever it received a user prop. Since user is how every avatar gets its gradient, colour and status dot, all 22 call sites became profile triggers by accident — including the picture inside the profile card itself, which re-anchored the card to that picture on every click and walked it across the screen (120px right, 36px down, until it pinned at the viewport clamp).

Avatar is now presentational. A new ProfileAvatar carries the open-the-profile behaviour at the five call sites that actually want it. The card's own picture escalates to the full profile modal instead of reopening the card.

The card also places itself off its measured size via the shared computeFloatingPosition engine, replacing six call sites that each hand-computed coordinates against a guessed 460px card height.

Closes #37
2026-08-25 15:51:04 +02:00
Jannis Braun aa5052ba8a docs(security): add scanning/hardening design spec + Plan A implementation plan
Completes the design record on main: Plan B's plan and the federation spec were
already here; this adds the umbrella security spec (source of truth for the
remaining container/web/desktop/remediation workstreams) and Plan A's plan.
2026-07-13 11:36:23 +02:00
Jannis Braun 7d1895308d fix(docker): make SARIF upload non-blocking; correct seed-admin/build-stage/restore ownership docs
Final whole-branch review (opus) fixes:
- docker-publish.yml: upload-sarif was if:always() but not continue-on-error, so a
  Trivy SARIF-emit flake would fail the job and SKIP the multi-arch publish. Made it
  non-blocking so a scanner hiccup never blocks a release.
- deployment.md: seed-admin-rotated.txt is root-owned (written via docker exec, which
  bypasses the gosu drop) — reverted an over-correction. Corrected the canonical
  runtime-stage build description (no toolchain; non-root gosu). First-boot chown note.
- restore.sh: comment ownership root -> uid 1000.
2026-07-13 01:21:20 +02:00
Jannis Braun 0b3aa42a09 docs(docker): add maintainer release-gate pull-test note (per review) 2026-07-13 01:13:19 +02:00
Jannis Braun 6d81b63d86 docs(docker): document non-root runtime, data-volume migration, and image scan 2026-07-13 01:09:26 +02:00
Jannis Braun 9d2eeb0963 docs(security): build native arm64 for local Docker verification (host is Apple Silicon; arm64 = Pi target) 2026-07-13 00:54:42 +02:00
Jannis Braun fd8659a964 docs(security): lighter targeted arm64 native-module check (avoid QEMU Vite build timeout) 2026-07-13 00:53:26 +02:00
Jannis Braun 7457146846 docs(security): fold Plan B review findings (proc/1 uid check, arm64 verify, cheap idempotent chown, provenance floor, doc corrections) 2026-07-13 00:52:13 +02:00
Jannis Braun 321428ba1f docs(security): Plan B implementation plan (container hardening + image scan) 2026-07-13 00:42:02 +02:00
Jannis Braun e2d09c0d52 fix(security): point OSV-Scanner at runnable subpath action; doc accuracy fixes
- OSV-Scanner ref was google/osv-scanner-action@<sha> (metadata-only root
  action, no runs:) -> subpath google/osv-scanner-action/osv-scanner-action
  which carries the docker action + scan-args input. Root ref would fail to
  load and redden the job on every run (caught in final whole-branch review).
- security-scanning.md: note gitleaks findings land in job log (not SARIF);
  add scorecard branch_protection_rule trigger; mark SBOM/provenance as not-
  yet-live. CLAUDE.md row: image scan is a later plan, not current.
2026-07-12 23:51:00 +02:00
Jannis Braun cb524675cc docs(security): document the scanning pipeline + maintainer checklist 2026-07-12 23:51:00 +02:00
TheZwiss d76e06a023 refactor(federation): consolidate inbound S2S-auth preamble into one helper (#11)
Six S2S-HMAC endpoints repeated the same inbound-auth preamble verbatim
(parse federation headers -> resolve active peer -> optional per-peer rate
limit -> verify HMAC signature -> nonce replay protection). Extract it into
authenticateS2SPeer() so the trust boundary has a single, tested definition.

Adopters (preamble only; every post-auth side effect, body validation, and
response is unchanged):
- DELETE /api/federation/identity      (no rate limiter; warns on missing nonce)
- POST   /api/federation/relay         (relay limiter; warns; keeps in-handler
                                        epoch-baseline populate + nonce ratchet)
- POST   /api/federation/sync          (no limiter; warns with the [sync] tag;
                                        keeps in-handler nonce ratchet)
- POST   /api/federation/users/lookup       (lookup limiter, Retry-After 60)
- POST   /api/federation/users/by-home-id   (same)
- POST   /api/federation/verify-attach-proof(shares lookup bucket, Retry-After 60)

Deliberate non-adopters, each keeping a load-bearing gate the helper would
flatten (documented at each site + in the helper docstring):
- POST /api/federation/epoch        gates status != 'revoked' (peer recovery),
                                    400 on missing headers, no nonce check
- POST /api/federation/peer/rotate  active-only but no nonce check
- POST /api/federation/peer/denied  awaiting_approval gate (404/409), synthetic
                                    no-grace secret verify

Behavior-preserving. The rate limiter is injected (plain { limited, retryAfter }),
so the limit still fires BEFORE signature verification. The only ordering change:
/relay's opportunistic epoch-baseline populate now runs just after the shared
preamble (i.e. after the nonce check) instead of between signature and nonce.
This is provably equivalent for every reachable honest-peer state (a duplicate
nonce means the baseline is already non-null; a valid-signature-but-no-nonce
request from a nonce-supporting peer is unreachable in transit and carries no
security/correctness consequence) and the populate is documented as not
affecting relay accept/reject.

Adds a dedicated unit test covering the full decision table (headers, peer
status, rate-limit + Retry-After, rate-limit-before-signature ordering,
signature, nonce duplicate/missing, log flag + context suffix, success). Full
server suite green (804 tests).
2026-07-10 03:08:09 +02:00
TheZwiss 94fe73522d refactor(server): split federation routes into cohesive modules (#9)
routes/federation.ts had grown to 7.6k lines, spanning HTTP route
registration, federated identity resolution, ~30 inbound relay event
processors, DM reconciliation, and rate-limiting internals — too large
to review or hold in context, and awkward to change safely.

Split the implementation into 18 focused modules under routes/federation/
(helpers, events/, handlers/) and keep routes/federation.ts as a thin
barrel that re-exports the public API and composes the HTTP registrars
into federationRoutes(). No import paths change anywhere else.

Pure move, no behavior change:
- 61/61 named functions byte-identical; only deltas are 2 dynamic-import
  paths adjusted for the new directory depth
- public export surface unchanged (barrel re-exports all 22 symbols)
- all 30 endpoints preserved (identical verb+path set)
- typecheck, build, and full server suite (790 tests) green

Docs: update federation.md source-file map; add split design doc.
2026-07-10 02:08:03 +02:00
TheZwiss 3513a3dde9 docs: tighten public copy, add comparison, templates, and social preview
* docs: tighten README, CONTRIBUTING, and CLA prose

* docs: add platform comparison and README FAQ

* chore: add issue and pull request templates

* chore: add repository social preview image
2026-07-07 23:39:33 +02:00
Jannis Braun 85e1975fa5 fix(permissions): deny space permissions to non-members (invite-bypass)
computePermissions() returned the space @everyone role's permissions without
verifying the caller had joined the space. Because CREATE_INVITE is in
DEFAULT_EVERYONE_PERMISSIONS, any authenticated user could mint an invite code
for a request-only space — whose id is listed by /api/spaces/explore — and then
self-join via /api/spaces/:id/join, bypassing the join-request approval flow.
The same gap let non-members read message history and search default channels.

Root cause:
- computePermissions now returns 0n for non-members (space owner and instance
  admin still short-circuit first, so they are unaffected).

Defense in depth (request-only spaces are approval-gated, never invite-joinable):
- both invite-code join endpoints reject visibility='request' (private stays
  invite-joinable — its only entry path; public too).
- POST /api/spaces/:id/invite refuses to hand out a code for request spaces.
- POST /api/dm/space-invite refuses to card a local request space, checked by
  space id against the local table so a spoofed spaceInstanceOrigin can't slip
  past it.
- InviteModal hides the invite affordances for request spaces.

Also removes the unused computeCategoryPermissions(), which duplicated the
resolution algorithm without the membership gate.

Adds unit + route + component tests covering non-member/member/owner/admin
resolution and the request/private/public visibility matrix.

Reported-by: BadAtCaptchas (#2)
2026-07-07 19:45:51 +02:00
Jannis Braun 531b496618 feat(deploy): three deployment modes + prebuilt multi-arch image for robust self-hosting
Make Backspace self-hostable in any homelab environment, not just a clean host
that owns ports 80/443.

install.sh is now mode-aware and auto-detects which fits:
  - allinone (default): bundled Caddy + auto-HTTPS — unchanged behavior
  - proxy: behind your own reverse proxy (nginx / Traefik / Caddy / Nginx Proxy
    Manager / SWAG) — app published on 127.0.0.1:APP_PORT, no bundled Caddy,
    prints paste-ready proxy snippets
  - tunnel: behind a tunnel (Cloudflare / Tailscale) — same, plus a 90MB upload
    cap (under Cloudflare's 100MB body limit) and voice force-disabled (WebRTC
    over UDP can't traverse a tunnel)

Port detection is Docker-aware (consults `docker ps` published ports, not just
`ss`), so a host whose proxy already owns 80/443 via iptables DNAT — with no
listening socket for `ss` to see — is correctly detected as "taken" instead of
dead-ending.

docker-compose.proxy.yml is a small overlay, layered via COMPOSE_FILE (written
into .env so no `-f` flags are ever needed), that publishes the loopback port and
parks Caddy in an inert profile. The base compose file is untouched, so All-in-One
behaves exactly as before.

Prebuilt image: .github/workflows/docker-publish.yml builds and pushes a
multi-arch (linux/amd64 + linux/arm64) image to ghcr.io/thezwiss/backspace on
release tags (and manual dispatch), so weak/ARM hosts skip the ~1.6GB local build
(the Vite build OOMs small ARM boxes). install.sh and docker-compose.yml default
to pulling it, fall back to an image already present on the host, and finally to a
from-source build — AGPL §13 commit stamping preserved on every path. Kept
deliberately separate from the desktop-installer workflow (release.yml).

Docs: README gains a "Deployment modes" section (all three modes, nginx / Caddy /
Traefik snippets, GUI-proxy field-by-field, cloudflared ingress, the update path,
and voice-per-mode caveats); docs/systems/deployment.md updated to match.

Verified live on a throwaway VM: proxy + all-in-one end-to-end through install.sh
(with a real Let's Encrypt cert), tunnel config generation, loopback-only binding,
and the local-image fallback path.
2026-07-06 13:36:29 +02:00
Jannis Braun 90fe262676 docs(desktop): document unsigned-build auto-update status and CI release pipeline 2026-07-03 13:28:37 +02:00
Jannis Braun acde64a642 docs(federation/dm): re-attach 1-on-1 federatedId reconciliation + drift sweep 2026-07-03 12:46:12 +02:00
Jannis Braun d3af4f2170 fix(federation): close re-attach final-review findings — client/server domain normalization, merge attachment repoint, empty-domain guard, test hardening 2026-07-03 02:43:45 +02:00
Jannis Braun 521aff6e52 docs(federation/auth/api/db/client): detached-account re-attach flow; reset-cleanup panel mentions re-attach 2026-07-03 02:28:35 +02:00