license: relicense to AGPL-3.0-only with commercial dual-license

- LICENSE -> verbatim GNU AGPL-3.0; add LICENSE-COMMERCIAL.md + SECURITY.md
- CLA -> exclusive-license grant (contributors keep copyright); add README
  anti-rugpull covenant + relicense record
- NOTICE / README / CONTRIBUTING / CLAUDE.md / package.json x5 updated;
  contact routed through GitHub (no email placeholders)
- AGPL section 13 source offer: operator-configurable BACKSPACE_SOURCE_URL +
  build-injected commit; sourceCodeUrl+commit on /api/instance/info;
  SourceCodeLink on login/register/settings/desktop; docs + .env.example updated
This commit is contained in:
Jannis Braun
2026-07-01 16:38:22 +02:00
parent 16d75f2806
commit f481e1fe9e
34 changed files with 1084 additions and 165 deletions
+20 -1
View File
@@ -1,7 +1,9 @@
import React, { useState, useEffect } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { useAuthStore } from '../../stores/authStore';
import { RateLimitError } from '../../api/client';
import { api, RateLimitError } from '../../api/client';
import type { InstanceInfoResponse } from '@backspace/shared';
import { SourceCodeLink } from '../ui/SourceCodeLink';
export function LoginPage() {
const [username, setUsername] = useState('');
@@ -14,6 +16,17 @@ export function LoginPage() {
const [searchParams] = useSearchParams();
const redirect = searchParams.get('redirect');
// AGPL § 13: anonymous users must be able to reach the source of the running
// version. Fetched from the unauthenticated public info endpoint.
const [instanceInfo, setInstanceInfo] = useState<InstanceInfoResponse | null>(null);
useEffect(() => {
let cancelled = false;
api.instance.info()
.then((info) => { if (!cancelled) setInstanceInfo(info); })
.catch(() => { /* Non-critical — link is simply omitted if unreachable. */ });
return () => { cancelled = true; };
}, []);
useEffect(() => {
if (retryAfter <= 0) return;
const timer = setInterval(() => {
@@ -129,6 +142,12 @@ export function LoginPage() {
</Link>
</p>
</form>
{instanceInfo && (
<div className="mt-6 pt-4 border-t border-white/[0.04] flex justify-center">
<SourceCodeLink sourceCodeUrl={instanceInfo.sourceCodeUrl} version={instanceInfo.version} commit={instanceInfo.commit} />
</div>
)}
</div>
</div>
);
@@ -9,6 +9,7 @@ import type { AvatarColor, CheckInviteResponse, InstanceInfoResponse } from '@ba
import { api, RateLimitError } from '../../api/client';
import { useTransferStore } from '../../stores/transferStore';
import { waitForTransferAttachment } from '../../utils/waitForTransfer';
import { SourceCodeLink } from '../ui/SourceCodeLink';
// Single-source regex for extracting a bare invite token from a pasted full URL.
// Token format: 22 chars base64url ([A-Za-z0-9_-]).
@@ -727,6 +728,13 @@ export function RegisterPage() {
</div>
</div>
)}
{/* AGPL § 13: source offer for anonymous visitors, shown on both steps. */}
{instanceInfo && (
<div className="mt-6 pt-4 border-t border-white/[0.04] flex justify-center">
<SourceCodeLink sourceCodeUrl={instanceInfo.sourceCodeUrl} version={instanceInfo.version} commit={instanceInfo.commit} />
</div>
)}
</div>
</div>
@@ -1,6 +1,9 @@
import { useState, useEffect } from 'react';
import { Modal } from '../ui/Modal';
import { Avatar } from '../ui/Avatar';
import { SourceCodeLink } from '../ui/SourceCodeLink';
import { api } from '../../api/client';
import type { InstanceInfoResponse } from '@backspace/shared';
import { useUIStore } from '../../stores/uiStore';
import { useAuthStore } from '../../stores/authStore';
import { AccountPanel } from './settingsPanels/AccountPanel';
@@ -59,9 +62,21 @@ export function UserSettingsModal() {
const [tab, setTab] = useState<SettingsTab>('account');
const [mobileView, setMobileView] = useState<'tabs' | 'content'>('tabs');
// AGPL § 13: home-instance source offer. Fetched from the public info endpoint
// so the source link reflects the version this instance is actually running.
const [instanceInfo, setInstanceInfo] = useState<InstanceInfoResponse | null>(null);
const isOpen = activeModal === 'userSettings';
useEffect(() => {
if (!isOpen) return;
let cancelled = false;
api.instance.info()
.then((info) => { if (!cancelled) setInstanceInfo(info); })
.catch(() => { /* Non-critical — link falls back to hidden if unreachable. */ });
return () => { cancelled = true; };
}, [isOpen]);
// Deep-linking: read modalData.tab when opening
useEffect(() => {
if (isOpen) {
@@ -148,6 +163,12 @@ export function UserSettingsModal() {
>
Log Out
</button>
{instanceInfo && (
<div className="px-3 pt-2">
<SourceCodeLink sourceCodeUrl={instanceInfo.sourceCodeUrl} version={instanceInfo.version} commit={instanceInfo.commit} />
</div>
)}
</div>
</div>
@@ -196,6 +217,12 @@ export function UserSettingsModal() {
>
Log Out
</button>
{instanceInfo && (
<div className="px-3 pt-2">
<SourceCodeLink sourceCodeUrl={instanceInfo.sourceCodeUrl} version={instanceInfo.version} commit={instanceInfo.commit} />
</div>
)}
</div>
</div>
)}
@@ -0,0 +1,55 @@
interface SourceCodeLinkProps {
/** URL to the Corresponding Source of the running version (AGPL § 13). */
sourceCodeUrl: string;
/** Running version string, e.g. "1.0.0". Omitted from the label when unknown. */
version?: string;
/** Short git commit/tag of the running build; appended to pin the exact version. */
commit?: string | null;
/** Extra classes for layout composition (alignment, spacing) at the call site. */
className?: string;
}
/**
* AGPL-3.0 § 13 "network-use source offer".
*
* Renders an accessible external link to the Corresponding Source of the version
* the instance is actually running. The URL is operator-configurable server-side
* (BACKSPACE_SOURCE_URL) and surfaced via GET /api/instance/info, so a modified
* self-hosted fork points humans at its own source.
*
* Rendered on every network-facing surface (settings sidebars, pre-auth pages)
* so any network user — authenticated or anonymous — can reach the source.
*/
export function SourceCodeLink({ sourceCodeUrl, version, commit, className }: SourceCodeLinkProps) {
const build = version ? `v${version}${commit ? ` (${commit})` : ''}` : '';
const label = build ? `Source code (AGPL) · ${build}` : 'Source code (AGPL)';
return (
<a
href={sourceCodeUrl}
target="_blank"
rel="noreferrer noopener"
className={`inline-flex items-center gap-1.5 text-xs text-txt-tertiary hover:text-txt-secondary transition-colors${
className ? ` ${className}` : ''
}`}
title="View the source code of the version this instance is running (AGPL-3.0)"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="flex-shrink-0"
>
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
<span>{label}</span>
</a>
);
}