feat(licence): contributed manifests are CC0 1.0

Contributed manifests were in no declared condition at all, which left §9a
replication with no grant flowing through it: peers mirror each other's
catalogues wholesale, and every hop of that was unlicensed.

CC0 rather than a share-alike licence, because a share-alike works by asserting
a right in the data and then conditioning its use. The position in
docs/legal-posture.md §3 is that presence timings are facts rather than
protectable expression — asserting copyright in them in order to license them
would contradict that argument in the same repository, and that contradiction is
worth more to an opponent than the licence is worth to us. CC0 also waives the
sui generis database right by name, closing the EU-specific residual exposure
from the contributor's side.

The grant is taken at token issuance, and that is not incidental. There are no
accounts, so there is no sign-up to attach terms to, and a manifest arrives over
POST /manifests with no channel to negotiate over. Acquiring the contribute
capability is the only moment a grant can be made, so POST /tokens now returns
the licence and its terms alongside the token — a licence the server publishes
but never delivers is one no contributor agreed to.

The test pins the scope limit as well as the identifier. Bounding the grant to
the manifest is the half that can fail silently: a reworded term reading onto
the underlying work would purport to grant what no contributor can.

Also records the settled code-licence position across all four repositories in
the legal posture, correcting an earlier claim there that the plugin and
extraction repos declared nothing. Both already carried LICENSE files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

TRACES: UR-019 | PR-006
This commit is contained in:
2026-07-31 10:43:43 +02:00
co-authored by Claude Opus 5
parent d5dd8113ef
commit 4afa36e7a2
8 changed files with 298 additions and 28 deletions
+121
View File
@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
+19
View File
@@ -10,6 +10,25 @@ point at it. If you are considering **running an instance**, read
[`docs/legal-posture.md`](docs/legal-posture.md) first: it states what an
instance holds, what it structurally cannot do, and the operator checklist.
## Licensing — two licences, do not conflate them
| | Licence | Where |
|---|---|---|
| **Code** | `GPL-3.0-or-later` | [`LICENSE`](LICENSE), declared in `Cargo.toml` |
| **Contributed manifests** | **CC0 1.0 Universal** | [`LICENSE-DATA`](LICENSE-DATA), specified in [SPEC.md](SPEC.md) §5b |
Both files ship with any distribution. They answer different questions, and a
package carrying only `LICENSE` leaves the one that matters for federation
unanswered.
The data licence is the one that matters operationally, because manifests are
what replicate between instances (§9a) — a code licence grants nothing over them.
`POST /tokens` returns the licence and its terms with every issued token, so the
grant is one contributors actually make rather than one the server announces.
The grant covers **the manifest only** — timings, identifiers, audio signature.
It does not, and cannot, license the underlying work.
The community instance is **`https://jray.tourolle.paris`**. The JRay plugin
ships with it pre-configured but **disabled** — §9 requires that no traffic leave
an installation until an admin opts in, so the default entry exists to save the
+48
View File
@@ -951,6 +951,54 @@ this spec, not alongside it: every claim in it is a consequence of a design
property recorded here, so **a change that weakens `SR-004` or `SR-005` silently
invalidates it.** Its §7 is the list of changes that would.
---
## 5b. The licence contributed manifests carry — UR-019
**Contributed manifests are CC0 1.0 Universal.** The full text is
[`LICENSE-DATA`](LICENSE-DATA); the code is separately `GPL-3.0-or-later`, and
the two must not be conflated — the code licence says nothing about the data,
and the data is the part that replicates between instances.
This follows the practice of every comparable service (MusicBrainz core data and
AcousticBrainz are CC0; AcoustID is CC BY-SA), and it closes a gap rather than
adding a feature: until it was stated, contributed manifests were in no declared
condition at all, and §9a replication had no grant flowing through it.
**Why CC0 and not a share-alike licence.** A share-alike licence works by
*asserting* a right in the data and then conditioning its use. The position
throughout — [`docs/legal-posture.md`](docs/legal-posture.md) §3 — is that
presence timings are facts rather than protectable expression. Asserting
copyright in them in order to license them would contradict that argument in the
same repository, and that contradiction is worth more to an opponent than a
share-alike licence is worth to the project. CC0 asserts nothing, which is the
position actually taken.
Two further consequences, both load-bearing:
- **CC0 waives the sui generis database right by name**, not merely copyright.
That closes the EU-specific residual exposure from the contributor's side, in
the one jurisdiction where such a right exists to be waived.
- **Federation needs no per-peer negotiation.** §9a has independent operators
replicating each other's catalogues wholesale; without a grant reaching every
peer, each hop is unlicensed. CC0 makes each one a non-event, and means no
instance can become a chokepoint by withholding permission to mirror.
**The grant is taken at token issuance, and this is not incidental.** There are
no accounts, so there is no sign-up to attach terms to; and a manifest arrives
over `POST /manifests` with no channel to negotiate over. Acquiring the
contribute capability is therefore the only moment at which a grant can be made,
so `POST /tokens` returns the licence and its terms alongside the token. A
licence the server publishes but never delivers is one no contributor agreed to.
**Scope, stated precisely and repeated in the terms themselves:** the grant
covers *the manifest* — timings, identifiers, audio signature. It does not, and
cannot, license the underlying work, which is not the contributor's to license
and which this server does not hold. That sentence is the whole of §§1–4 of the
legal posture restated, and it belongs in the terms in exactly that form.
---
### Client-side hardening
Independent of the server, because a compromised or hostile server must not be
+44 -17
View File
@@ -333,10 +333,15 @@ inapplicable for the simple reason that there are no links.
## 6. The licence Jmanifests are published under
**Recommended: CC0 1.0 Universal.** This matches MusicBrainz core data and
**Adopted: CC0 1.0 Universal** — recorded in SPEC §5b, full text in
[`LICENSE-DATA`](../LICENSE-DATA). This matches MusicBrainz core data and
AcousticBrainz exactly, and it is the one choice consistent with the rest of this
document.
Note the separation, which must not be blurred: the **code** is
`GPL-3.0-or-later`, the **data** is CC0. The code licence says nothing about the
manifests, and the manifests are the part that replicates between instances.
**Why CC0 and not a share-alike licence.** AcoustID uses CC BY-SA 3.0, and the
temptation is to follow it — but a share-alike licence works by *asserting* a
right in the data and then conditioning its use. §3 argues at length that
@@ -365,12 +370,13 @@ position §3 actually takes.
closed commercially — which is the failure mode this whole ecosystem learned
from.
**The grant must be taken at upload.** A licence the server declares is worth
nothing if contributors never granted it. The contribution terms must state that
submitting a manifest places its content under CC0, and the JRay plugin's
contribution opt-in (SPEC §9) is where a user encounters that — it is already an
**The grant is taken at token issuance.** A licence the server declares is worth
nothing if contributors never granted it, so `POST /tokens` returns the licence
and its terms alongside the token — there are no accounts, so acquiring the
contribute capability is the only moment at which a grant can be made. The JRay
plugin's contribution opt-in (SPEC §9) is where a user encounters it: already an
explicit, off-by-default choice, so this adds a sentence to a decision the user
is already making rather than a new one.
is making anyway rather than a new one.
**Scope, stated precisely.** The licence covers *the manifest* — the timings,
identifiers and signature contributed. It does not and cannot purport to license
@@ -378,10 +384,30 @@ the underlying film, which is not the contributor's to license and which the
server does not hold. This distinction is the whole of §§1–4 restated in a
sentence, and it belongs in the terms in exactly that form.
> **Status: recommended, not yet adopted.** No repository currently carries a
> `LICENSE` file, and nothing outside this document states a data licence.
> Adopting this means a recorded decision, a `LICENSE` file, a line in the
> contribution terms, and a sentence in the plugin's contribution opt-in.
> **Status: adopted.** Recorded in SPEC §5b as `UR-019`, with the text in
> [`LICENSE-DATA`](../LICENSE-DATA) and the grant delivered by `POST /tokens`.
### Code licences, for completeness
Distinct from the above, and settled across all three repositories:
| Repository | Code licence | File |
|---|---|---|
| `JRay-public-server` | `GPL-3.0-or-later` | `LICENSE` — verified byte-identical to the FSF text |
| `jRay` (plugin) | `GPL-3.0` | `LICENSE` |
| `scene-actor-extraction` | `MIT`, with a model/third-party addendum | `LICENSE` |
Two notes an operator or contributor may need:
- **The MIT/GPL split is directionally fine.** MIT-licensed extraction output and
code can be used by the GPL components; the reverse would not hold. Nothing in
the current data flow runs the wrong way.
- **`scene-actor-extraction`'s addendum is the one to actually read.** It records
that some bundled models are licensed for **non-commercial research use only**,
which is a restriction on *deploying the extraction pipeline* and has no
bearing on the manifests it produces or on this server. The distinction matters
precisely because CC0 manifests could otherwise be mistaken for a statement
about the tooling that generated them.
---
@@ -500,13 +526,14 @@ than as incremental features:
6. **Keep the kill switch usable.** SPEC §5a makes delisting a single `UPDATE`:
one manifest, every manifest from a token, or every manifest for a title.
Delisting is instant and reversible; deletion is a separate, logged action.
7. **Adopt a data licence and add a `LICENSE` file.** §6 recommends CC0 1.0.
No repository currently carries one, and the server's `GPL-3.0-or-later`
covers the *code* only — it says nothing about the manifests, which are the
thing that actually replicates between instances. This is the largest
outstanding gap in the posture.
8. **State the licence in the contribution terms**, so contributors grant it
rather than the server merely declaring it (§6).
7. **Do not modify the contribution terms without re-reading §6.** The data
licence is CC0 1.0 (SPEC §5b), delivered by `POST /tokens`. Its scope limit —
the manifest, never the underlying work — is the operative half, and a
reworded term that blurs it grants something no contributor can grant.
8. **Ship both licence files with any distribution.** `LICENSE` (code,
`GPL-3.0-or-later`) and `LICENSE-DATA` (manifests, CC0 1.0) answer different
questions, and a package containing only the first leaves the question that
actually matters for federation unanswered.
---
+2
View File
@@ -45,6 +45,7 @@ requirement and no fixture-generation step, unlike `scene-actor-extraction`.
| UR-016 | Accept and store `extraction.gallery_scope`; rank on it (§7) | SR-003 | Medium | Done |
| UR-017 | Accept per-window belief and identification route; `scenes` are objects | SR-003 | High | Done |
| UR-018 | Exclude belief and route from `content_id`, replicating them as attributes | SR-003 | High | Done |
| UR-019 | Contributed manifests are CC0 1.0; the grant is delivered with the token, not merely published | PR-006 | High | Done |
### Notes on status
@@ -133,6 +134,7 @@ topology is the point, so this is a deliberate choice rather than an oversight.
| UR-012 | T2 | No endpoint accepts embeddings or image data | An `embedding` or `crop` field is an unknown-field `400` |
| UR-013 | T1 | Stored windows are byte-identical to those submitted | Adjacent windows never merged; a window is never trimmed to a shorter one |
| UR-014 | T1 | Unknown `jmanifest_version` rejected | Version `2` and version `0` both refused, naming the field |
| UR-019 | T2 | `POST /tokens` returns the licence and its terms | The terms **bound the grant to the manifest** — a reading that covers the underlying work is the failure, not a missing field |
| DR-001 | T1 | Unknown field at any nesting depth fails to parse | `movie` and `jellyfin_id` named in the error |
| DR-003 | T1 | Concurrent writes serialize rather than returning `SQLITE_BUSY` | Failed transaction rolls back fully |
| DR-005 | T1 | Jobs lease once, reschedule with backoff, survive restart | Stranded lease released at startup; future job not leased early |
+17 -9
View File
@@ -3,7 +3,7 @@
<!-- GENERATED FILE - do not edit by hand. -->
<!-- Regenerate: scripts/traceability/traceability-gate.sh -->
**Generated:** 2026-07-31T07:49:04+00:00
**Generated:** 2026-07-31T08:06:49+00:00
Denominators are read from [`requirements.md`](requirements.md) at run time, never hardcoded. Coverage counts a requirement only when it is tagged in source **and** has a verification tier this repo's CI host can execute (`T1, T2, static`).
@@ -14,10 +14,10 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev
| Source files scanned | 29 |
| TRACES tags found | 43 |
| EXCEPTION tags found | 0 |
| Requirements defined | 32 |
| Requirements covered | 25 |
| **Coverage** | **78.1%** (25/32) |
| Coverage of CI-executable scope | 78.1% (25/32) |
| Requirements defined | 33 |
| Requirements covered | 26 |
| **Coverage** | **78.8%** (26/33) |
| Coverage of CI-executable scope | 78.8% (26/33) |
| Tagged but unexecuted in CI | 0 |
| Orphan tags | 0 |
@@ -25,7 +25,7 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev
| Type | Covered | Tagged but unexecuted | Defined |
|---|---|---|---|
| UR | 15 | 0 | 18 |
| UR | 16 | 0 | 19 |
| DR | 10 | 0 | 14 |
- **PR** tags present (separate taxonomy, not counted in coverage): PR-004, PR-005, PR-006
@@ -77,6 +77,7 @@ _None._
| UR-016 | Done | T2 | SR-003 | untagged | - | Accept and store `extraction.gallery_scope`; rank on it (§7) |
| UR-017 | Done | T1, T2 | SR-003 | covered | `src/model.rs` | Accept per-window belief and identification route; `scenes` are objec… |
| UR-018 | Done | T1 | SR-003 | untagged | - | Exclude belief and route from `content_id`, replicating them as attri… |
| UR-019 | Done | T2 | PR-006 | covered | `src/api/upload.rs` | Contributed manifests are CC0 1.0; the grant is delivered with the to… |
| DR-001 | Done | T1 | SR-004 | untagged | - | Strict parse boundary: unknown fields rejected structurally, not by v… |
| DR-002 | Done | unset | SR-004 | covered | `src/api/fetch.rs`, `src/db/repo.rs` | Fully relational storage — no JSON blob on the write path |
| DR-003 | Done | T1 | PR-004 | covered | `src/db/mod.rs` | Single serialized writer connection, with a read pool alongside |
@@ -178,7 +179,7 @@ _None._
### PR-006
**Locations:** 9
**Locations:** 10
- [`src/api/federation.rs:66`](../src/api/federation.rs#L66) — `pub async fn get_changes(`
- [`src/api/federation.rs:108`](../src/api/federation.rs#L108) — `pub async fn get_manifest_by_content_id(`
@@ -186,6 +187,7 @@ _None._
- [`src/api/fetch.rs:127`](../src/api/fetch.rs#L127) — `pub async fn get_series(`
- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `pub async fn post_manifest(`
- [`src/api/upload.rs:101`](../src/api/upload.rs#L101) — `pub async fn post_bundle(`
- [`src/api/upload.rs:245`](../src/api/upload.rs#L245) — `pub async fn post_token(`
- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `pub fn persist(`
- [`src/validate.rs:572`](../src/validate.rs#L572) — `pub fn validate_bundle_envelope(b: &SeriesBundle) -> VResult<()>`
- [`src/worker.rs:107`](../src/worker.rs#L107) — `async fn run_federation_pull(&self, payload: &str) -> Result<(), JobError>`
@@ -233,7 +235,7 @@ _None._
- [`src/api/report.rs:56`](../src/api/report.rs#L56) — `pub async fn post_report(`
- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `pub async fn post_manifest(`
- [`src/api/upload.rs:223`](../src/api/upload.rs#L223) — `pub async fn post_token(`
- [`src/api/upload.rs:245`](../src/api/upload.rs#L245) — `pub async fn post_token(`
- [`src/app.rs:26`](../src/app.rs#L26) — `pub fn router(state: AppState) -> Router`
- [`src/auth.rs:22`](../src/auth.rs#L22) — `pub fn hash_token(token: &str) -> String`
- [`src/auth.rs:76`](../src/auth.rs#L76) — `pub fn client_ip(headers: &HeaderMap, peer: Option<IpAddr>, trusted_proxies: &[IpAddr]) -…`
@@ -293,7 +295,7 @@ _None._
**Locations:** 6
- [`src/api/report.rs:56`](../src/api/report.rs#L56) — `pub async fn post_report(`
- [`src/api/upload.rs:223`](../src/api/upload.rs#L223) — `pub async fn post_token(`
- [`src/api/upload.rs:245`](../src/api/upload.rs#L245) — `pub async fn post_token(`
- [`src/auth.rs:22`](../src/auth.rs#L22) — `pub fn hash_token(token: &str) -> String`
- [`src/castcheck.rs:82`](../src/castcheck.rs#L82) — `pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcome`
- [`src/castcheck.rs:215`](../src/castcheck.rs#L215) — `pub fn category_guard_violation(matched: &[MatchedActor], title_is_adult: bool) -> Option…`
@@ -379,3 +381,9 @@ _None._
- [`src/model.rs:191`](../src/model.rs#L191) — `Unknown`
- [`src/model.rs:232`](../src/model.rs#L232) — `pub fn from_stored(s: &str) -> Option<Self>`
### UR-019
**Locations:** 1
- [`src/api/upload.rs:245`](../src/api/upload.rs#L245) — `pub async fn post_token(`
+28 -2
View File
@@ -209,9 +209,26 @@ pub async fn post_bundle(
Ok(with_quota_headers(resp, quota))
}
/// SPDX identifier of the licence a contributed manifest is placed under (§5b).
pub const CONTRIBUTION_LICENSE: &str = "CC0-1.0";
/// The grant a contributor makes, in the words §5b specifies.
///
/// Scope is the operative part: it covers *the manifest*, and cannot purport to
/// license the underlying work, which is not the contributor's to license and
/// which this server does not hold.
pub const CONTRIBUTION_TERMS: &str = "Contributing a manifest places its content \
— timings, identifiers and audio signature — under CC0 1.0 Universal. This \
covers the manifest only. It does not, and cannot, license the underlying \
work, which the contributor does not own and this server does not hold.";
#[derive(Debug, Serialize)]
pub struct TokenIssued {
pub token: String,
/// Delivered with the capability, not merely published: a licence the server
/// declares unilaterally is not one any contributor granted (§5b).
pub contribution_license: &'static str,
pub contribution_terms: &'static str,
}
/// Issues an anonymous bearer capability (§5a).
@@ -220,7 +237,12 @@ pub struct TokenIssued {
/// only as a hash, so the server cannot enumerate who holds tokens. Discarding a
/// token and requesting another is trivially easy — and that is fine, because the
/// token is not the defence; the content checks are.
/// TRACES: UR-005 | SR-004
///
/// The response carries the §5b contribution licence. This is the only moment
/// the server can obtain a grant: there are no accounts, so there is no sign-up
/// to attach terms to, and a manifest arrives with no channel to negotiate over.
/// Acquiring the capability is therefore where the grant has to be made.
/// TRACES: UR-005, UR-019 | SR-004 | PR-006
pub async fn post_token(
State(state): State<AppState>,
peer: crate::state::PeerIp,
@@ -240,5 +262,9 @@ pub async fn post_token(
.await
.map_err(ApiError::Internal)?;
Ok(Json(TokenIssued { token }))
Ok(Json(TokenIssued {
token,
contribution_license: CONTRIBUTION_LICENSE,
contribution_terms: CONTRIBUTION_TERMS,
}))
}
+19
View File
@@ -218,6 +218,25 @@ async fn tokens_are_issued_anonymously_and_are_distinct() {
assert!(a.starts_with("jray_"));
}
#[tokio::test]
async fn the_token_response_carries_the_contribution_licence() {
// §5b / UR-019. There are no accounts, so token issuance is the only moment
// a grant can be taken — a licence the server publishes but never delivers
// is one no contributor agreed to, which is precisely the gap that leaves
// federated replication (UR-008) without a grant flowing through it.
let s = TestServer::new("token-licence");
let (status, body, _) = s.post_json("/api/v1/tokens", &json!({})).await;
assert_eq!(status, StatusCode::OK, "body: {body}");
assert_eq!(body["contribution_license"].as_str(), Some("CC0-1.0"));
let terms = body["contribution_terms"].as_str().expect("terms in response");
assert!(terms.contains("CC0 1.0"), "terms must name the licence: {terms}");
// The scope limit is the operative half: it must be impossible to read the
// grant as covering the film rather than the manifest.
assert!(terms.contains("manifest only"), "terms must bound the grant to the manifest: {terms}");
}
// ---------------------------------------------------------------------------
// §6 — upload validation
// ---------------------------------------------------------------------------