From 7865e5ff43f7b4b91f8a2c0f56576b85b887094c Mon Sep 17 00:00:00 2001 From: Origo Date: Mon, 20 Jul 2026 14:13:57 +0800 Subject: [PATCH] Add rights-transparency discovery members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An independent ORSP-compatible reader (Open Reading) already ships operatorName, contactUrl, contentLicense, and rightsStatement as optional discovery-document fields, letting a source identify its operator and content authorization basis for client-side transparency display. They were never upstreamed here, so this spec has no normative definition for fields a real implementation already relies on. This adds them as optional discovery members (purely additive, no existing field or endpoint behavior changes), documents them in both SPECIFICATION.md and SPECIFICATION.zh-CN.md, types them in schemas/discovery.schema.json, and has the reference server declare them with conformance checks covering their presence and shape. Left protocolVersion / spec version headers untouched — bumping those is a release decision for maintainers, not something this PR assumes. --- SPECIFICATION.md | 15 ++++++++++++++- SPECIFICATION.zh-CN.md | 10 +++++++++- examples/dart_server.dart | 8 ++++++++ schemas/discovery.schema.json | 19 +++++++++++++++++++ tool/conformance_test.dart | 18 ++++++++++++++++++ 5 files changed, 68 insertions(+), 2 deletions(-) diff --git a/SPECIFICATION.md b/SPECIFICATION.md index 9897647..ae2b057 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -109,7 +109,8 @@ MUST NOT use the `orsp` namespace unless it is defined by a future ORSP specification. Unknown extension tokens do not make a source incompatible. Optional discovery members are `description`, `iconUrl`, `websiteUrl`, -`languages`, `supportedVersions`, and `maxCatalogPageSize`. Language tags MUST +`languages`, `supportedVersions`, `maxCatalogPageSize`, `operatorName`, +`contactUrl`, `contentLicense`, and `rightsStatement`. Language tags MUST follow BCP 47, such as `en`, `zh-CN`, or `sr-Latn`. `supportedVersions`, when present, is a non-empty array of ORSP @@ -121,6 +122,18 @@ clients MAY use it for diagnostics and MUST NOT require it. declaring the largest `pageSize` the source accepts on the chapter-catalog endpoint (Section 11). When absent, the limit is 100. +`operatorName`, `contactUrl`, `contentLicense`, and `rightsStatement` are +rights-transparency members. `operatorName` identifies the entity that +actually operates the source and can act on rights requests. `contactUrl`, +when present, MUST be an absolute HTTP(S) URL for a contact or rights-report +page controlled by that operator. `contentLicense` is a short license name or +authorization basis, for example `CC BY 4.0` or `Public Domain`. +`rightsStatement` is a concise, source-supplied description of content +provenance or authorization scope. A client MAY display these members for +transparency but MUST NOT treat their presence, absence, or content as a +legality, authorization, or identity certification by any reader, registry, +or the protocol itself. + `apiBaseUrl` MUST NOT contain user information, a query, or a fragment and MUST end in `/`. Endpoint paths in this document are resolved relative to that URL. For example, an API base URL of `https://books.example/api/` and endpoint diff --git a/SPECIFICATION.zh-CN.md b/SPECIFICATION.zh-CN.md index 3287ad3..1ccf38a 100644 --- a/SPECIFICATION.zh-CN.md +++ b/SPECIFICATION.zh-CN.md @@ -93,7 +93,8 @@ ORSP 1.1 定义三项可选的标准能力: 客户端遇到未知扩展能力时,不得因此判定书源不兼容。 发现文档的可选字段包括 `description`、`iconUrl`、`websiteUrl`、`languages`、 -`supportedVersions` 和 `maxCatalogPageSize`。语言必须使用 BCP 47 标签,例如 `en`、 +`supportedVersions`、`maxCatalogPageSize`、`operatorName`、`contactUrl`、 +`contentLicense` 和 `rightsStatement`。语言必须使用 BCP 47 标签,例如 `en`、 `zh-CN` 或 `sr-Latn`。 `supportedVersions` 是非空的 ORSP `主版本.次版本` 字符串数组,出现时必须包含 @@ -103,6 +104,13 @@ ORSP 1.1 定义三项可选的标准能力: `maxCatalogPageSize` 是 100 至 1000 之间的整数,声明章节目录接口(第 11 节)接受的 最大 `pageSize`。省略时上限为 100。 +`operatorName`、`contactUrl`、`contentLicense` 和 `rightsStatement` 是权利透明度 +字段。`operatorName` 标识实际运营该书源、能够处理权利请求的主体。`contactUrl` 如果 +出现,必须是该运营者控制的绝对 HTTP(S) 联系或权利投诉页面。`contentLicense` 是简短 +的许可名称或授权依据,例如 `CC BY 4.0` 或 `Public Domain`。`rightsStatement` 是书源 +自行提供的内容来源或授权范围简述。客户端可以为了透明度展示这些字段,但不得把它们的 +存在、缺失或内容当作任何阅读器、注册表或协议本身对合法性、授权或身份的认证。 + `apiBaseUrl` 不得包含用户名、密码、查询参数或片段,并且必须以 `/` 结尾。本文接口路径 相对于该 URL 解析。例如,API 基础地址为 `https://books.example/api/`,接口关系为 `v1/search`,最终地址就是 `https://books.example/api/v1/search`。 diff --git a/examples/dart_server.dart b/examples/dart_server.dart index 560812e..27450f0 100644 --- a/examples/dart_server.dart +++ b/examples/dart_server.dart @@ -75,6 +75,14 @@ class ReferenceSourceServer { 'apiBaseUrl': baseUri.resolve('api/').toString(), 'languages': ['zh-CN', 'en'], 'maxCatalogPageSize': maxCatalogPageSize, + 'operatorName': 'ORSP Reference Project', + 'contactUrl': + 'https://github.com/miloquinn/open-reading-source-protocol/issues', + 'contentLicense': + 'Original example content under the repository license', + 'rightsStatement': + 'This reference source contains only original protocol test ' + 'content created for ORSP development.', 'capabilities': [ 'search', 'discover', diff --git a/schemas/discovery.schema.json b/schemas/discovery.schema.json index 00ae978..d99d1fc 100644 --- a/schemas/discovery.schema.json +++ b/schemas/discovery.schema.json @@ -76,6 +76,25 @@ "minimum": 100, "maximum": 1000 }, + "operatorName": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "contactUrl": { + "type": "string", + "format": "uri", + "pattern": "^https?://" + }, + "contentLicense": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "rightsStatement": { + "type": "string", + "maxLength": 2000 + }, "capabilities": { "type": "array", "minItems": 4, diff --git a/tool/conformance_test.dart b/tool/conformance_test.dart index 41831c9..3cb12e1 100644 --- a/tool/conformance_test.dart +++ b/tool/conformance_test.dart @@ -59,6 +59,24 @@ Future main() async { maxCatalogPageSize >= 100 && maxCatalogPageSize <= 1000, 'maxCatalogPageSize is within the allowed range', ); + check( + (discoveryJson['operatorName'] as String).isNotEmpty, + 'operatorName is present and non-empty', + ); + check( + Uri.parse( + discoveryJson['contactUrl'] as String, + ).scheme.startsWith('http'), + 'contactUrl is an absolute HTTP(S) URL', + ); + check( + (discoveryJson['contentLicense'] as String).isNotEmpty, + 'contentLicense is present and non-empty', + ); + check( + (discoveryJson['rightsStatement'] as String).isNotEmpty, + 'rightsStatement is present and non-empty', + ); final discover = await request(client, apiBase.resolve('v1/discover')); check(discover.status == 200, 'discover returns 200');