Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/40-deployment/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/40-deployment/production-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`。

Expand Down
13 changes: 1 addition & 12 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions scripts/check_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
17 changes: 1 addition & 16 deletions src/inkcre_extension_registry/service/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
Loading