diff --git a/docs/40-deployment/local-development.md b/docs/40-deployment/local-development.md index e83bad5..f0ba2b6 100644 --- a/docs/40-deployment/local-development.md +++ b/docs/40-deployment/local-development.md @@ -7,9 +7,9 @@ pdm install --frozen-lockfile pnpm install --frozen-lockfile ``` -配置 `DATABASE_URL`、`PUBLIC_ORIGIN`、`S3_ENDPOINT_URL`、`S3_BUCKET` 和标准 AWS 凭据 `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`。启用插件文档托管时,另设 `DOCUMENTATION_ORIGIN_TEMPLATE`,例如本地的 `http://{snapshot}.docs.localhost`;模板必须把 32 位快照标识放在第一个 DNS label,并且不能与管理 origin 共站。数据库必须是 PostgreSQL;远程连接默认验证 TLS,localhost 可以不启用 TLS。`PUBLIC_ORIGIN` 是 HTTPS origin,本地允许 localhost HTTP,不含路径。文档内容 origin 在生产必须使用 HTTPS;本地只允许 `.localhost`。文件配置是 dotenv 数据,不应当作 shell 脚本执行;应用从进程环境读取配置。 +配置 `DATABASE_URL`、`PUBLIC_ORIGIN`、`S3_ENDPOINT_URL`、`S3_BUCKET` 和标准 AWS 凭据 `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`。启用插件文档托管时,另设 `DOCUMENTATION_ORIGIN_TEMPLATE`,例如本地的 `http://{snapshot}.docs.localhost`;模板必须把 32 位快照标识放在第一个 DNS label,使每份不可变快照拥有独立 origin。数据库必须是 PostgreSQL;远程连接默认验证 TLS,localhost 可以不启用 TLS。`PUBLIC_ORIGIN` 是 HTTPS origin,本地允许 localhost HTTP,不含路径。文档内容 origin 在生产必须使用 HTTPS;本地只允许 `.localhost`。文件配置是 dotenv 数据,不应当作 shell 脚本执行;应用从进程环境读取配置。 -生产内容域必须使用不同的可注册域,例如 `registry.example.com` 与 `{snapshot}.exampleusercontent.net`;`{snapshot}.docs.example.com` 会被拒绝。配置校验使用 `publicsuffixlist` 包内的 Public Suffix List(含 private section),不在启动时访问网络;PSL 随锁定依赖升级更新,未知后缀和没有固定可注册域的模板被拒绝。域名应采用 ASCII/Punycode 配置。仅管理端是 HTTP loopback、内容端是 HTTP `.localhost` 的配对配置获得本地例外。内容域必须专用于不可信静态内容,不得部署 SSO、转发认证 Cookie,或与其他持有认证 Cookie 的服务共用父域;PSL 校验只验证所配置的管理域与内容域,无法盘点其他服务的 Cookie 配置。 +生产内容域可以与管理站使用同一可注册域。这样部署时,作者提供的 HTML/JS 可能接收或干扰父域 Cookie;不得在共享父域设置敏感 Cookie。需要浏览器级站点隔离时,应改用独立可注册域,例如 `registry.example.com` 与 `{snapshot}.exampleusercontent.net`。域名应采用 ASCII/Punycode 配置。 ```bash pnpm db:migrate diff --git a/docs/40-deployment/production-registry.md b/docs/40-deployment/production-registry.md index 55d5582..ccfc39d 100644 --- a/docs/40-deployment/production-registry.md +++ b/docs/40-deployment/production-registry.md @@ -4,7 +4,7 @@ `production.yml` 只接受精确 current-main SHA。`verify` 执行仓库检查和容器构建,不修改远程资源。`deploy` 在受保护 production 环境中,重新核验 main 后对已配置 app 向前迁移、配置数据库和单桶 S3 凭据、发布同一镜像,并验证 Heroku app origin。它不自动导入 D1、不改变域名、不创建示例数据,也不删除旧资源。 -production 环境需要 `HEROKU_APP_NAME`、`S3_ENDPOINT_URL`、`S3_BUCKET`、`DOCUMENTATION_ORIGIN_TEMPLATE` variables,以及 `HEROKU_API_KEY`、`MIGRATION_DATABASE_URL`、`DATABASE_URL`、`AWS_ACCESS_KEY_ID`、`AWS_SECRET_ACCESS_KEY` secrets。文档模板必须指向已经配置 wildcard DNS/TLS 的专用内容域,并符合本仓库的跨站校验。首次配置保持既有 R2 桶,S3 token 仅授予该桶的对象读写。`MIGRATION_DATABASE_URL` 使用 `registry_owner`,仅交给迁移容器与可信角色配置命令;`DATABASE_URL` 使用同一数据库的普通 `registry_app` 角色和独立密码。迁移创建该角色及业务表授权,控制器设置密码后以 `web=1:eco` 启动应用。owner 连接与平台控制 token 不进入运行服务配置。数据库连接属于独立 Registry 项目,不复用 core-py 的数据库或发布生命周期。 +production 环境需要 `HEROKU_APP_NAME`、`S3_ENDPOINT_URL`、`S3_BUCKET`、`DOCUMENTATION_ORIGIN_TEMPLATE` variables,以及 `HEROKU_API_KEY`、`MIGRATION_DATABASE_URL`、`DATABASE_URL`、`AWS_ACCESS_KEY_ID`、`AWS_SECRET_ACCESS_KEY` secrets。文档模板必须指向已经配置 wildcard DNS/TLS 的专用内容 origin。生产当前使用 `https://{snapshot}.docs.registry.inkcre.dev`;它与管理站同属 `inkcre.dev`,因此部署接受作者 HTML/JS 与管理站同站、可能接收或干扰父域 Cookie 的风险。`inkcre.dev` 下的服务不得设置可发送到该内容域的敏感父域 Cookie。首次配置保持既有 R2 桶,S3 token 仅授予该桶的对象读写。`MIGRATION_DATABASE_URL` 使用 `registry_owner`,仅交给迁移容器与可信角色配置命令;`DATABASE_URL` 使用同一数据库的普通 `registry_app` 角色和独立密码。迁移创建该角色及业务表授权,控制器设置密码后以 `web=1:eco` 启动应用。owner 连接与平台控制 token 不进入运行服务配置。数据库连接属于独立 Registry 项目,不复用 core-py 的数据库或发布生命周期。 Heroku 交付为 Uvicorn 设置 `FORWARDED_ALLOW_IPS=*`,由平台 HTTP 入口提供外部请求协议,补斜杠跳转保持 HTTPS。转发头不参与 namespace 授权或身份判断;认证仍由 publisher credential 决定。部署 smoke 同时检查 `/simple` 跳转到该 origin 的 HTTPS `/simple/`。 diff --git a/pdm.lock b/pdm.lock index 0f790cd..7486002 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:5597c6ce7689a33aa9b862a4fbedaf1367988150d44be8e092ca73185a564aba" +content_hash = "sha256:72ae9e70826955d7d25bde377160b76698338d79c888e38b6dc466fcff46a13e" [[metadata.targets]] requires_python = ">=3.12,<3.14" @@ -1150,17 +1150,6 @@ files = [ {file = "psycopg-3.3.5.tar.gz", hash = "sha256:d0a3d9ccf5788af054cbd745278cb02401b5c312aeaafbf2c6144460aec47da4"}, ] -[[package]] -name = "publicsuffixlist" -version = "1.0.2.20260919" -requires_python = ">=3.5" -summary = "publicsuffixlist implement" -groups = ["default"] -files = [ - {file = "publicsuffixlist-1.0.2.20260919-py2.py3-none-any.whl", hash = "sha256:60cd2c1e09a2bd66044e562d4f977b96f21e6b151d161a02e41e001fff42fb27"}, - {file = "publicsuffixlist-1.0.2.20260919.tar.gz", hash = "sha256:d9383e510e3464fde4478b9d0f97a24911b6c2bd397aa149c84a88534e98e3eb"}, -] - [[package]] name = "py-partiql-parser" version = "0.6.3" diff --git a/pyproject.toml b/pyproject.toml index 349d7cc..81c7da3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ authors = [{ name = "InKCre" }] dependencies = [ "fastapi>=0.139.2,<0.142", "inkcre-extension-toolkit>=0.3,<0.4", - "publicsuffixlist>=1,<2", "packaging>=25,<27", "python-multipart>=0.0.22,<0.1", "jinja2<4,>=3.1.6", diff --git a/scripts/check_documentation.py b/scripts/check_documentation.py index b88c8fd..f8a93dd 100644 --- a/scripts/check_documentation.py +++ b/scripts/check_documentation.py @@ -27,12 +27,12 @@ def check_content_sites() -> None: - """Configuration must enforce the promised cookie-site isolation before serving HTML.""" + """Configuration must keep each snapshot on its own valid content origin.""" for management, content, accepted in ( - ("https://registry.example.com", "https://{snapshot}.docs.example.com", False), - ("https://registry.example.co.uk", "https://{snapshot}.docs.example.co.uk", False), + ("https://registry.example.com", "https://{snapshot}.docs.example.com", True), + ("https://registry.example.co.uk", "https://{snapshot}.docs.example.co.uk", True), ("https://registry.example.com", "https://{snapshot}.exampleusercontent.net", True), - ("https://registry.team.github.io", "https://{snapshot}.docs.team.github.io", False), + ("https://registry.team.github.io", "https://{snapshot}.docs.team.github.io", True), ("https://registry.team.github.io", "https://{snapshot}.other.github.io", True), ("http://localhost", "http://{snapshot}.docs.localhost", True), ("https://registry.example.com", "http://{snapshot}.docs.localhost", False), diff --git a/src/inkcre_extension_registry/service/settings.py b/src/inkcre_extension_registry/service/settings.py index 4172392..236dacc 100644 --- a/src/inkcre_extension_registry/service/settings.py +++ b/src/inkcre_extension_registry/service/settings.py @@ -8,8 +8,6 @@ from dataclasses import dataclass, field from urllib.parse import parse_qs, unquote, urlparse -from publicsuffixlist import PublicSuffixList - def database_config(url: str) -> dict: parsed = urlparse(url) @@ -109,6 +107,7 @@ def from_env(cls) -> Settings: documentation_origin.count("{snapshot}") != 1 or not (content.hostname or "").startswith("{snapshot}.") or (content.scheme != "https" and not local_content) + or (local_content and not local_http) or content.username is not None or content.password is not None or content.path @@ -122,20 +121,6 @@ def from_env(cls) -> Settings: "DOCUMENTATION_ORIGIN_TEMPLATE must be a separate wildcard HTTPS origin " "with a leading {snapshot} label" ) - # Author HTML/JS must not share the management site's cookie domain. - # Use the packaged ICANN + private PSL, never a startup network fetch. - if not (local_http and local_content): - psl = PublicSuffixList(accept_unknown=False, only_icann=False) - management_host = (parsed.hostname or "").encode("idna").decode("ascii") - content_host = (content.hostname or "").removeprefix("{snapshot}.") - content_host = content_host.encode("idna").decode("ascii") - management_site = psl.privatesuffix(management_host) - content_site = psl.privatesuffix(content_host) - if not management_site or not content_site or management_site == content_site: - raise ValueError( - "documentation and management must use different registrable domains " - "recognized by the packaged Public Suffix List" - ) return cls( database_url=os.environ["DATABASE_URL"], public_origin=origin,