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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user