diff --git a/ADDITIONAL_TERMS.md b/ADDITIONAL_TERMS.md new file mode 100644 index 000000000..eda58c1f5 --- /dev/null +++ b/ADDITIONAL_TERMS.md @@ -0,0 +1,51 @@ +# BookOrbit Additional Terms to the GNU AGPLv3 + +Effective 10 September 2026. + +Copyright (C) 2025-2026 neon. Additional copyright is held by the respective BookOrbit contributors. + +These non-permissive additional terms supplement the GNU Affero General Public License, version 3 only, as permitted by sections 7(b), 7(c), 7(d), and 7(e). + +These terms apply to BookOrbit material owned by neon and distributed with this notice, and to contributions whose copyright holders expressly accept these terms. Other contributions remain subject to the license terms under which they were originally submitted. + +## 1. Attribution + +Every interactive user interface of a covered work containing BookOrbit material must provide a prominent and readily accessible **About** or **Legal Notices** feature displaying: + +> **Powered by BookOrbit** +> +> Copyright (C) 2025-2026 neon. +> +> Additional copyright is held by the respective BookOrbit contributors. +> +> Original project: +> +> Licensed under the GNU Affero General Public License, version 3 only, with additional terms. This software is provided without warranty. You may redistribute and modify it under those terms. + +"Powered by BookOrbit" must link directly to . + +The feature must be available to all users without administrative permission. It and the required notice must not be removed, disabled, hidden, obscured, or made materially less prominent than comparable About, help, or legal-notice features. + +## 2. Source Code + +When AGPLv3 section 13 applies, the same feature must offer users the complete Corresponding Source for the exact version being operated, at no charge. A link only to the original BookOrbit repository is not sufficient for a modified version. + +## 3. Modified Versions + +A modified version must prominently state: + +> This is a modified version of BookOrbit and is not an official BookOrbit release. + +It must provide a relevant modification date and must not claim or imply that it is an official, approved, sponsored, or endorsed BookOrbit release. + +## 4. Trademarks + +Except for the factual attribution required above, no trademark rights are granted. Without written permission, a modified version may not use "BookOrbit", a confusingly similar name, or a BookOrbit logo as its primary name or branding. + +## 5. Preservation and Enforcement + +Source distributions containing covered BookOrbit material must preserve this file, the GNU AGPLv3 license, applicable copyright notices, and notices identifying modifications and their dates. + +Compliance with these terms is an express condition of permission to modify or convey covered BookOrbit material or make a modified version available for remote network interaction. Noncompliance is subject to section 8 of the AGPLv3. + +These terms apply only to the extent permitted by section 7 of the AGPLv3. If any provision is unenforceable, the remaining provisions continue in effect. diff --git a/Dockerfile b/Dockerfile index 2b471754d..d6ebf4a93 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,7 +72,7 @@ ENV PORT=3000 COPY --from=server-builder --chown=node:node /deploy ./ COPY --from=client-builder --chown=node:node /app/client/dist ./public COPY --from=server-builder --chown=node:node /app/server/entrypoint.sh ./entrypoint.sh -COPY --chown=node:node LICENSE NOTICE ./ +COPY --chown=node:node LICENSE NOTICE ADDITIONAL_TERMS.md ./ COPY --chown=node:node server/bin/kepubify/ ./bin/kepubify/ COPY --chown=node:node koreader-plugin/bookorbit.koplugin/ ./koreader-plugin/bookorbit.koplugin/ diff --git a/NOTICE b/NOTICE index bc46978ad..03991d46d 100644 --- a/NOTICE +++ b/NOTICE @@ -1,21 +1,25 @@ -BookOrbit Attribution Notice +BookOrbit Attribution and Legal Notice -Copyright (C) 2025-2026 neon and BookOrbit contributors. +Copyright (C) 2025-2026 neon. +Additional copyright is held by the respective BookOrbit contributors. BookOrbit is free software licensed under the GNU Affero General Public -License, version 3. See LICENSE for the complete license terms. +License, version 3 only. See LICENSE for the complete license text. -Additional attribution term under GNU AGPL v3 section 7(b): +BookOrbit material owned by neon and contributions whose copyright holders +expressly accept the terms are also subject to the non-permissive additional +terms in ADDITIONAL_TERMS.md, as authorized by sections 7(b), 7(c), 7(d), and +7(e) of the GNU AGPL v3. -When a covered work containing BookOrbit material is conveyed, its source -distribution must preserve the following reasonable author attribution in a -top-level NOTICE, COPYRIGHT, README, or comparable legal notices file: +Other contributions remain subject to the license terms under which they were +originally submitted. - BookOrbit - Copyright (C) 2025-2026 neon and BookOrbit contributors. +The required attribution is: + + Powered by BookOrbit + Copyright (C) 2025-2026 neon. + Additional copyright is held by the respective BookOrbit contributors. Original project: https://github.com/bookorbit/bookorbit -This attribution does not grant permission to use the BookOrbit name or logo -to imply endorsement of a modified version. Modified versions that are -conveyed must identify themselves as modified, give relevant modification -dates, and otherwise comply with the GNU AGPL v3. +See ADDITIONAL_TERMS.md for the conditions governing display, source access, +modified versions, preservation, and trademarks. diff --git a/README.md b/README.md index 8dfdc9468..faccb683f 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,6 @@ When adding user-facing text in code, add the Vue I18n key only to `client/src/l ## License and Attribution -BookOrbit is licensed under the **[GNU Affero General Public License v3.0](LICENSE)**. +BookOrbit is licensed under the **[GNU Affero General Public License v3.0 only](LICENSE)**. -Forks and modified distributions must preserve BookOrbit's reasonable author attribution as described in the **[BookOrbit Attribution Notice](NOTICE)**, in accordance with section 7(b) of the GNU AGPL v3. +BookOrbit material whose copyright holders have authorized them is also subject to the **[BookOrbit Additional Terms](ADDITIONAL_TERMS.md)** under sections 7(b), 7(c), 7(d), and 7(e) of the GNU AGPL v3. See the **[Attribution and Legal Notice](NOTICE)** for the required attribution. diff --git a/client/package.json b/client/package.json index 463230db2..b0b26425f 100644 --- a/client/package.json +++ b/client/package.json @@ -4,7 +4,7 @@ "description": "BookOrbit client - self-hosted library management and reading platform", "author": "neonsolstice", "private": true, - "license": "AGPL-3.0", + "license": "AGPL-3.0-only", "homepage": "https://bookorbit.app", "repository": { "type": "git", diff --git a/client/src/App.vue b/client/src/App.vue index a95fdd8f1..7d3efc791 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -12,6 +12,7 @@ import { useAuth } from '@/features/auth/composables/useAuth' import { Toaster } from '@/components/ui/sonner' import { TooltipProvider } from '@/components/ui/tooltip' import { resolveRouteViewKey } from '@/router/view-key' +import LegalNotices from '@/components/legal/LegalNotices.vue' const { isOpen } = useChangePasswordDialog() const themeStore = useThemeStore() @@ -53,6 +54,7 @@ provide( + diff --git a/client/src/components/AppHeader.vue b/client/src/components/AppHeader.vue index 3c6351233..e178fa501 100644 --- a/client/src/components/AppHeader.vue +++ b/client/src/components/AppHeader.vue @@ -17,6 +17,7 @@ import { ExternalLink, Sparkles, Languages, + Info, } from '@lucide/vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' @@ -57,6 +58,7 @@ import { DEFAULT_FORMAT_PRIORITY, LOCALE_LABELS, Permission, type Locale } from import { useThemeStore } from '@/stores/theme' import { useLocaleStore } from '@/stores/locale' import { getFormatColor } from '@/features/book/lib/format-colors' +import { useLegalNotices } from '@/components/legal/useLegalNotices' const { t } = useI18n() const router = useRouter() @@ -69,6 +71,7 @@ const { subscribe: subscribeNotifications } = useNotifications() const { hasUnseen: hasUnseenWhatsNew } = useWhatsNew() const themeStore = useThemeStore() const localeStore = useLocaleStore() +const { openLegalNotices } = useLegalNotices() const currentLanguageLabel = computed(() => LOCALE_LABELS[localeStore.locale]) const documentationUrl = 'https://bookorbit.app/what-is-bookorbit' @@ -669,6 +672,10 @@ function formatBadgeStyle(fmt: string) { + + + {{ t('components.legalNotices.about') }} + @@ -742,6 +749,10 @@ function formatBadgeStyle(fmt: string) { {{ t('components.appHeader.whatsNew') }} + + + {{ t('components.legalNotices.about') }} + {{ t('components.appHeader.help') }} diff --git a/client/src/components/legal/LegalNotices.spec.ts b/client/src/components/legal/LegalNotices.spec.ts new file mode 100644 index 000000000..8b5876e05 --- /dev/null +++ b/client/src/components/legal/LegalNotices.spec.ts @@ -0,0 +1,60 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { nextTick } from 'vue' + +import LegalNotices from './LegalNotices.vue' +import PublicLegalNotices from './PublicLegalNotices.vue' +import { useLegalNotices } from './useLegalNotices' + +vi.mock('reka-ui', () => ({ + DialogRoot: { props: ['open'], template: '
' }, + DialogPortal: { template: '
' }, + DialogOverlay: { template: '
' }, + DialogContent: { template: '' }, + DialogTitle: { template: '

' }, + DialogDescription: { template: '

' }, +})) + +describe('LegalNotices', () => { + beforeEach(() => { + useLegalNotices().closeLegalNotices() + }) + + it('keeps the legal notice available without authentication or elevated permission', async () => { + const wrapper = mount(LegalNotices) + + useLegalNotices().openLegalNotices() + await nextTick() + + expect(wrapper.get('.legal-dialog').text()).toContain('Powered by BookOrbit') + expect(wrapper.get('.legal-dialog').text()).toContain('Copyright (C) 2025-2026 neon.') + expect(wrapper.get('.legal-dialog').text()).toContain('Additional copyright is held by the respective BookOrbit contributors.') + expect(wrapper.get('.legal-dialog').text()).toContain('provided without warranty') + }) + + it('links attribution, source, license, and additional terms to the project', async () => { + const wrapper = mount(LegalNotices) + useLegalNotices().openLegalNotices() + await nextTick() + + const links = wrapper.findAll('a') + expect(links.map((link) => link.attributes('href'))).toEqual([ + 'https://github.com/bookorbit/bookorbit', + 'https://github.com/bookorbit/bookorbit', + 'https://github.com/bookorbit/bookorbit/blob/main/LICENSE', + 'https://github.com/bookorbit/bookorbit/blob/main/ADDITIONAL_TERMS.md', + ]) + }) + + it('uses an inline linked attribution on public pages', async () => { + const wrapper = mount(PublicLegalNotices) + + const attribution = wrapper.get('a') + expect(attribution.text()).toBe('Powered by BookOrbit') + expect(attribution.attributes('href')).toBe('https://github.com/bookorbit/bookorbit') + expect(wrapper.classes()).not.toContain('fixed') + + await wrapper.get('button').trigger('click') + expect(useLegalNotices().open.value).toBe(true) + }) +}) diff --git a/client/src/components/legal/LegalNotices.vue b/client/src/components/legal/LegalNotices.vue new file mode 100644 index 000000000..61569f3bb --- /dev/null +++ b/client/src/components/legal/LegalNotices.vue @@ -0,0 +1,94 @@ + + + diff --git a/client/src/components/legal/PublicLegalNotices.vue b/client/src/components/legal/PublicLegalNotices.vue new file mode 100644 index 000000000..7f20d197b --- /dev/null +++ b/client/src/components/legal/PublicLegalNotices.vue @@ -0,0 +1,30 @@ + + + diff --git a/client/src/components/legal/useLegalNotices.ts b/client/src/components/legal/useLegalNotices.ts new file mode 100644 index 000000000..e2deabdd5 --- /dev/null +++ b/client/src/components/legal/useLegalNotices.ts @@ -0,0 +1,19 @@ +import { ref } from 'vue' + +const open = ref(false) + +export function useLegalNotices() { + function openLegalNotices() { + open.value = true + } + + function closeLegalNotices() { + open.value = false + } + + function handleOpenChange(value: boolean) { + open.value = value + } + + return { open, openLegalNotices, closeLegalNotices, handleOpenChange } +} diff --git a/client/src/features/auth/ForgotPasswordPage.vue b/client/src/features/auth/ForgotPasswordPage.vue index dbeb2bb27..76bb2b9be 100644 --- a/client/src/features/auth/ForgotPasswordPage.vue +++ b/client/src/features/auth/ForgotPasswordPage.vue @@ -5,6 +5,7 @@ import { Moon, Sun, Wallpaper } from '@lucide/vue' import { ACCENT_OPTIONS, ACCENT_ROWS, RADIUS_OPTIONS, BACKGROUND_OPTIONS, useThemeStore } from '@/stores/theme' import { api } from '@/lib/api' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue' const { t } = useI18n() const themeStore = useThemeStore() @@ -75,7 +76,7 @@ async function handleSubmit() { diff --git a/client/src/features/auth/LoginPage.vue b/client/src/features/auth/LoginPage.vue index f15110f64..f1a3b79bb 100644 --- a/client/src/features/auth/LoginPage.vue +++ b/client/src/features/auth/LoginPage.vue @@ -10,6 +10,7 @@ import { useOidc } from './composables/useOidc' import { useSetupStatus } from './composables/useSetupStatus' import { useLoginOptions } from './composables/useLoginOptions' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue' const { t } = useI18n() const themeStore = useThemeStore() @@ -125,7 +126,7 @@ async function handleOidcLogin(provider: OidcProviderPublic) { diff --git a/client/src/features/auth/MagicLinkLoginView.vue b/client/src/features/auth/MagicLinkLoginView.vue index d6626105d..83d773efc 100644 --- a/client/src/features/auth/MagicLinkLoginView.vue +++ b/client/src/features/auth/MagicLinkLoginView.vue @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n' import { useRoute, useRouter } from 'vue-router' import { Loader2, AlertCircle } from '@lucide/vue' import { useAuth } from '@/features/auth/composables/useAuth' +import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue' const { t } = useI18n() const route = useRoute() @@ -41,7 +42,7 @@ function goToLogin() { diff --git a/client/src/features/auth/OidcCallbackPage.vue b/client/src/features/auth/OidcCallbackPage.vue index dc7ee6815..3aaa209d7 100644 --- a/client/src/features/auth/OidcCallbackPage.vue +++ b/client/src/features/auth/OidcCallbackPage.vue @@ -5,6 +5,7 @@ import { useRouter } from 'vue-router' import type { OidcCallbackResponse } from '@bookorbit/types' import { OidcErrorCode } from '@bookorbit/types' import { setAccessToken } from '@/lib/api' +import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue' import { useAuth } from './composables/useAuth' import { useOidc, OidcLoginError } from './composables/useOidc' @@ -82,7 +83,7 @@ onMounted(async () => { diff --git a/client/src/features/auth/RegisterPage.vue b/client/src/features/auth/RegisterPage.vue index 1bf2dcfa1..971c508cf 100644 --- a/client/src/features/auth/RegisterPage.vue +++ b/client/src/features/auth/RegisterPage.vue @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n' import { Moon, Sun, Wallpaper } from '@lucide/vue' import { ACCENT_OPTIONS, ACCENT_ROWS, RADIUS_OPTIONS, BACKGROUND_OPTIONS, useThemeStore } from '@/stores/theme' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue' import { RegisterError, useAuth } from './composables/useAuth' import { checkIdentityText } from './lib/identity-text' @@ -110,7 +111,7 @@ async function handleSubmit() { diff --git a/client/src/features/auth/ResetPasswordPage.vue b/client/src/features/auth/ResetPasswordPage.vue index 93f8958c3..46113be7b 100644 --- a/client/src/features/auth/ResetPasswordPage.vue +++ b/client/src/features/auth/ResetPasswordPage.vue @@ -5,6 +5,7 @@ import { useRoute, useRouter } from 'vue-router' import { Moon, Sun, Wallpaper } from '@lucide/vue' import { ACCENT_OPTIONS, ACCENT_ROWS, RADIUS_OPTIONS, BACKGROUND_OPTIONS, useThemeStore } from '@/stores/theme' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue' const { t } = useI18n() const themeStore = useThemeStore() @@ -95,7 +96,7 @@ async function handleSubmit() { diff --git a/client/src/features/auth/SetupPage.vue b/client/src/features/auth/SetupPage.vue index bb38b59d9..255504123 100644 --- a/client/src/features/auth/SetupPage.vue +++ b/client/src/features/auth/SetupPage.vue @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n' import { Moon, Sun, Wallpaper } from '@lucide/vue' import { ACCENT_OPTIONS, ACCENT_ROWS, RADIUS_OPTIONS, BACKGROUND_OPTIONS, useThemeStore } from '@/stores/theme' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue' import { useAuth } from './composables/useAuth' const { t } = useI18n() @@ -85,7 +86,7 @@ async function handleSubmit() { diff --git a/client/src/features/reader/epub/components/KeyboardShortcutsModal.vue b/client/src/features/reader/epub/components/KeyboardShortcutsModal.vue index 439cea305..a8a9f0c3a 100644 --- a/client/src/features/reader/epub/components/KeyboardShortcutsModal.vue +++ b/client/src/features/reader/epub/components/KeyboardShortcutsModal.vue @@ -1,9 +1,11 @@ @@ -87,6 +94,17 @@ onUnmounted(() => document.removeEventListener('keydown', onKeyDown, true))
+ +
+ +
diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 7b8311097..9a14f2c0a 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -7266,6 +7266,20 @@ "librariesEmptyHint": "Create a library to start adding books.", "smartScopesEmptyHint": "Smart scopes save a set of filters you use often.", "collectionsEmptyHint": "Collections group books you pick by hand." + }, + "legalNotices": { + "about": "About BookOrbit", + "trigger": "Legal notices", + "title": "About BookOrbit", + "description": "BookOrbit attribution, licensing, and source code.", + "poweredBy": "Powered by BookOrbit", + "originalDevelopment": "This software contains material originally developed as BookOrbit.", + "copyright": "Copyright (C) 2025-2026 neon.", + "contributors": "Additional copyright is held by the respective BookOrbit contributors.", + "licenseSummary": "Licensed under the GNU Affero General Public License, version 3 only, with additional terms. This software is provided without warranty. You may redistribute and modify it under those terms.", + "sourceCode": "Source code", + "license": "AGPLv3 license", + "additionalTerms": "Additional terms" } }, "customIcons": { diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index fccf55685..3316e8de0 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -281,4 +281,4 @@ See [AI_POLICY.md](AI_POLICY.md). The short version: disclose all AI usage in yo ## License -By contributing, you agree that your contributions are licensed under the same license as the project (AGPL-3.0), including the attribution term in the [BookOrbit Attribution Notice](../NOTICE). +By submitting a contribution, you license it under the [GNU Affero General Public License, version 3 only](../LICENSE), together with the [BookOrbit Additional Terms](../ADDITIONAL_TERMS.md). You represent that you have the authority to grant this license. Copyright in your contribution remains yours. diff --git a/package.json b/package.json index 472dd001c..a0bcf5542 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bookorbit", "description": "Self-hosted library management and reading platform for ebooks, PDFs, audiobooks, and comics", "author": "neonsolstice", - "license": "AGPL-3.0", + "license": "AGPL-3.0-only", "homepage": "https://bookorbit.app", "repository": { "type": "git", diff --git a/packages/plugin-api/package.json b/packages/plugin-api/package.json index 44504d171..85f4a031f 100644 --- a/packages/plugin-api/package.json +++ b/packages/plugin-api/package.json @@ -2,7 +2,7 @@ "name": "@bookorbit/plugin-api", "version": "1.0.0", "private": true, - "license": "AGPL-3.0", + "license": "AGPL-3.0-only", "main": "./dist/index.js", "types": "./dist/index.d.ts", "devDependencies": { diff --git a/packages/types/package.json b/packages/types/package.json index b5b427fe9..1ac7404c4 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -2,7 +2,7 @@ "name": "@bookorbit/types", "version": "0.0.1", "private": true, - "license": "AGPL-3.0", + "license": "AGPL-3.0-only", "main": "./dist/index.js", "types": "./dist/index.d.ts", "devDependencies": { diff --git a/server/package.json b/server/package.json index 256410c2e..a722cd8e9 100644 --- a/server/package.json +++ b/server/package.json @@ -4,7 +4,7 @@ "description": "BookOrbit server - self-hosted library management and reading platform", "author": "neonsolstice", "private": true, - "license": "AGPL-3.0", + "license": "AGPL-3.0-only", "homepage": "https://bookorbit.app", "repository": { "type": "git",