news 2026/9/14 17:53:02

Hindsight Supabase 租户扩展深度解析:本地 JWKS 验证、按用户 Schema 隔离与内置版本迁移

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Hindsight Supabase 租户扩展深度解析:本地 JWKS 验证、按用户 Schema 隔离与内置版本迁移

Hindsight Supabase 租户扩展深度解析:本地 JWKS 验证、按用户 Schema 隔离与内置版本迁移

【免费下载链接】hindsightHindsight: Agent Memory That Learns项目地址: https://gitcode.com/GitHub_Trending/hindsight2/hindsight

本篇指南聚焦 Hindsight 仓库中supabase-tenant扩展:它如何用 Supabase Auth 的 JWT 完成请求认证,并把每个用户的记忆隔离到独立的 PostgreSQL schema 中。读完后你将掌握该扩展的完整配置项(HINDSIGHT_API_TENANT_*系列环境变量)、Docker 镜像构建与部署方式、JWKS 本地验签与 HS256 回退两条认证路径的底层机制,以及从 0.9.2 内置版本迁移到独立打包版本的全部注意事项。

扩展是什么:独立打包的 TenantExtension

supabase-tenant是 Hindsight 的TenantExtension实现:它用 Supabase Auth):

  • 本地 JWT 验证:使用项目 JWKS 公钥在本地完成验签,每个请求无网络调用;对使用旧式 HS256 签名的项目回退到/auth/v1/user接口;
  • 每用户一个 schema:用户 ida1b2…7890得到 schemauser_a1b2…7890(连字符转为下划线),首次访问时执行迁移,之后走缓存;
  • 零用户管理:身份来源就是你现有的 Supabase 项目,扩展不维护任何用户表。

这个扩展位于 hindsight-extensions 注册表中,是TENANT槽位的两个实现之一(另一个是static-keys-tenant)。它不发布到 PyPI、不打 wheel:Hindsight 的官方镜像不带任何扩展,分发的单位是你在官方镜像之上构建出来的派生镜像。这一点决定了它的安装方式——后面会详细展开。

历史背景:该扩展在 Hindsight0.9.2 及之前内置于服务端,路径为hindsight_api.extensions.builtin.supabase_tenant。此后不再随服务器打包,但配置项名称、schema 命名规则完全不变(详见 迁移章节)。

认证与隔离模型:TenantExtension 接口如何驱动 schema 隔离

Hindsight 服务端在 tenant.py 中定义了租户扩展契约。扩展实现两个抽象方法:

  • authenticate(context) -> TenantContext:验证context.api_key(即Authorization头去掉Bearer后的值),返回TenantContext(schema_name=...)TenantContext的文档注释明确说明:后续所有数据库查询都会使用该 schema 做全限定表名(例如user_xxx.memory_units),隔离因此发生在 SQL 层而非应用层;
  • list_tenants() -> list[Tenant]:返回后台 worker 需要轮询任务的 schema 列表——这是 worker 端租户发现的唯一入口。

SupabaseTenantExtensionauthenticate()主流程(extension.py)按以下顺序执行:

  1. 缺失 token →AuthenticationError("Missing Authorization header...")
  2. 长度低于MIN_TOKEN_LENGTH(源码常量,20 字符)→ 判定为格式非法,直接拒绝,避免把垃圾值送进验签;
  3. 按模式分发:_use_jwks为真走本地 JWKS 验签,否则走/auth/v1/user回退路径;
  4. subclaim 取出用户 id,必须匹配 UUID 正则^[0-9a-f]{8}-...-12}$)才允许进入 schema 名——扩展运行在服务进程内、处于认证边界上,schema_prefix同样受正则^[a-zA-Z_][a-zA-Z0-9_]*$约束(extension.py),防止用户输入污染 schema 名;
  5. 拼出f"{prefix}_{user_id.replace('-', '_')}",若该 schema 不在进程内缓存_initialized_schemas中,调用self.context.run_migration(schema_name)首次建 schema,随后返回TenantContext

缓存的意义:authenticate()每个请求都会跑,而run_migration只做一次。list_tenants()则直接返回_initialized_schemas中自进程启动以来见过的全部 schema——注意这意味着重启后租户列表从空开始累积,直到各租户再次发起请求(这正是 README 强调 worker 必须配置相同扩展变量的原因,见下文)。

两条 JWT 验证路径

路径一:本地 JWKS 验签(默认,推荐)

服务端启动时on_startup()会拉取{SUPABASE_URL}/auth/v1/.well-known/jwks.json(extension.py),按kid建立公钥缓存,此后每个请求零网络调用。源码中几个关键常量(extension.py):

常量作用
MIN_TOKEN_LENGTH20过短 token 直接拒绝
REQUEST_TIMEOUT_SECONDS10.0连接/读取超时(按阶段设置,非总超时)
JWKS_CACHE_TTL_SECONDS600缓存过期阈值,与 Supabase Edge 侧 10 分钟缓存对齐
JWKS_MIN_REFRESH_INTERVAL_SECONDS30防抖:两次强制刷新之间的最小间隔
SUPPORTED_ALGORITHMSRS256,ES256Supabase Auth 非对称签名支持的两个算法

签名字段校验在_verify_token_jwks()(extension.py)中通过pyjwt.decode()完成,同时校验 audience(authenticated)与 issuer({SUPABASE_URL}/auth/v1,并逐类映射ExpiredSignatureErrorInvalidAudienceErrorInvalidIssuerErrorDecodeErrorAuthenticationError——过期 token 会得到明确的 "Token has expired" 而非笼统的 500。

密钥轮转处理_get_signing_key()解析 token 的kid头后,先检查缓存是否超过 600s TTL(是则刷新);若kid不在缓存中,则再触发一次强制刷新(受 30s 最小间隔保护)以覆盖密钥轮转场景;仍找不到才抛AuthenticationError("Unable to find signing key for token")

路径二:HS256 遗留项目回退

若 JWKS 端点返回空(项目仍用 HS256 对称签名),扩展回退为每请求调用{SUPABASE_URL}/auth/v1/user,携带客户端 token(Authorization)与service_rolekey(apikey头)(extension.py):

  • 401 →Invalid or expired token;非 200 → 透传状态码;
  • 超时(asyncio.TimeoutError)→Authentication timeout - please retry
  • 连接失败(aiohttp.ClientError)→Connection error: ...

源码注释提示了一个版本细节:HTTP 传输层当前是aiohttpClientTimeout(connect=10, sock_read=10),按"每阶段"而非"总时长"计时),早期版本用 httpx;README 中"安装PyJWT[crypto]httpx"一句是遗留表述,以 Dockerfile 实际安装的PyJWT[crypto]>=2.12.0aiohttp>=3.14.3为准。

配置

HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_supabase_tenant:SupabaseTenantExtension HINDSIGHT_API_TENANT_SUPABASE_URL=https://xxx.supabase.co

Hindsight 的扩展加载器约定:与槽位同前缀的其他环境变量都会去掉HINDSIGHT_API_TENANT_前缀、转小写后成为构造函数的config字典键(SUPABASE_URLconfig["supabase_url"])。完整参数表:

变量必填默认值说明
HINDSIGHT_API_TENANT_SUPABASE_URLSupabase 项目 URL
HINDSIGHT_API_TENANT_SUPABASE_SERVICE_KEY仅 HS256 项目service_rolekey。JWKS 不可用时的回退验签必需;设置后启动时还会执行一次/auth/v1/health连通性检查
HINDSIGHT_API_TENANT_SCHEMA_PREFIXuserschema 名前缀,必须是合法 Postgres 标识符片段

启动即校验,而非首请求才报错__init__在缺supabase_url、前缀不合法(含空、以数字开头)时直接抛ValueError(extension.py);on_startup中若 JWKS 拉不到且没配 service key,同样抛ValueError阻止启动——README 原话:"the server fails at startup rather than accepting unverifiable tokens"。另外supabase_url尾部/会被rstrip掉,保证 issuer 拼写正确。

worker 与 API 必须配置同一组变量。worker 通过list_tenants()决定对哪些 schema 执行整合(consolidation)与后台维护;从 extensions README 的接口文档可见,"A schema you never return gets no consolidation or maintenance"。worker 侧没加载该扩展时,返回的租户列表永远为空,所有租户的后台处理都会停摆

安装与运行

扩展不发布到 PyPI,分发的单位是镜像。从仓库根目录构建(扩展源码必须在 build context 内):

docker build -f hindsight-extensions/supabase-tenant/Dockerfile -t hindsight-with-supabase .

Dockerfile 的关键步骤值得逐条看:

ARG HINDSIGHT_IMAGE=ghcr.io/vectorize-io/hindsight:latest FROM ${HINDSIGHT_IMAGE} # 装进服务端的虚拟环境。该 venv 由 uv sync 创建、自身不带 pip, # 裸 pip install 会落到 user site-packages,运行中的服务器看不到。 RUN uv pip install --python /app/api/.venv/bin/python --no-cache \ 'PyJWT[crypto]>=2.12.0' \ 'aiohttp>=3.14.3' # /app/extensions 是派生镜像自有的目录,不会遮蔽服务器自带的任何包。 COPY hindsight-extensions/supabase-tenant/hindsight_ext_supabase_tenant \ /app/extensions/hindsight_ext_supabase_tenant ENV PYTHONPATH=/app/extensions # 构建期就 import 一次:打包错误应该死在 build,而不是死在第一个带认证的请求。 RUN /app/api/.venv/bin/python -c "import hindsight_ext_supabase_tenant"

最后一行 import 检查是整套打包方案的保险丝:没有它,任何 PYTHONPATH 拼写错误都会"打包成功、上线即炸"。若不需要镜像内置的本地 embedding/reranking 模型,可用--build-arg HINDSIGHT_IMAGE=ghcr.io/vectorize-io/hindsight:latest-slim换 slim 基座。

运行示例(API 与 worker 使用同一镜像时,环境变量自然一致):

docker run -p 8888:8888 \ -e HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_supabase_tenant:SupabaseTenantExtension \ -e HINDSIGHT_API_TENANT_SUPABASE_URL=https://xxx.supabase.co \ hindsight-with-supabase

docker-compose.yml形态(构建上下文必须是仓库根目录):

services: hindsight-api: build: context: . dockerfile: hindsight-extensions/supabase-tenant/Dockerfile environment: HINDSIGHT_API_TENANT_EXTENSION: hindsight_ext_supabase_tenant:SupabaseTenantExtension HINDSIGHT_API_TENANT_SUPABASE_URL: https://xxx.supabase.co

独立 worker 部署时,给 worker 容器传入完全相同HINDSIGHT_API_TENANT_*变量。extensions README 还明确警告:不要在容器入口脚本里pip install扩展——那等同于每次重启都从网络解析未锁定的代码进运行中的服务器,扩展应固化在镜像层。

若不用 Docker 直接跑服务端,则把hindsight_ext_supabase_tenant/目录加入 Hindsight 运行环境的PYTHONPATH,并在该环境安装PyJWT[crypto](JWKS/RS256 验签)与aiohttp(当前传输层依赖)。

使用:客户端如何调用

客户端把 Supabase 签发的 JWT 作为 bearer token 发出,扩展验签后把请求路由到对应用户的 schema:

curl -H "Authorization: Bearer <supabase_jwt>" \ https://your-hindsight-server/v1/default/banks/my-bank/memories/recall

认证失败时,扩展抛出的AuthenticationError.reason(如Missing Authorization headerToken has expiredInvalid token audience)会原样出现在响应中,便于区分"token 过期""audience 不匹配""issuer 不符"等具体失败原因。

从内置版本迁移

这是纯打包层面的移动,不是行为变更。步骤:

  1. 按上文把扩展装进镜像;
  2. 更新扩展路径:
-HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.supabase_tenant:SupabaseTenantExtension +HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_supabase_tenant:SupabaseTenantExtension

所有HINDSIGHT_API_TENANT_*配置项名称与含义不变,schema 命名规则不变,已有租户 schema 会被原样接管list_tenants()基于运行时见过的 schema 累积,数据库里已迁移的 schema 无需重建)。

开发:在仓库内跑测试

在扩展目录内:

uv sync uv run pytest tests -v

pyproject.toml 存在只为跑测试:[tool.uv] package = false让 uv 只建 venv、把源码留在 path 上而不构建任何发行物;hindsight-api-slim以本地路径 editable 方式引入(tool.uv.sources),因为它是"接口提供方"、仅测试期依赖——运行时服务端才是宿主进程,反过来由扩展引入服务器依赖会把服务器版本从部署脚下抽走。

测试值得细看的地方(test_supabase_tenant.py):

  • Supabase 被打桩成真实的进程内aiohttp.web服务器,而非 mock 客户端——状态码处理、JSON 解析、超时、连接错误都走扩展真实的 aiohttp 传输路径;
  • 用例覆盖:配置校验(缺 URL、前缀以数字开头、尾斜杠剥离)、启动期 JWKS 拉取与三种回退分支(空 keys / 拉取失败 / HTTP 错误)、"无 JWKS 且无 service key 必须启动失败"、签名键缓存刷新与轮转、kid缺失、token 各失败分支(过期/audience/issuer/非 UUID 的sub)、schema 首次初始化/二次缓存/初始化失败、list_tenants从空到累积等;
  • test_package_entrypoint.py 则直接断言"README 里写的那个环境变量值能加载到扩展类"——文档与代码的契约由测试兜底。

小结

supabase-tenant展示了 Hindsight 多租户隔离的标准做法:认证边界上完成身份解析,隔离下沉到 PostgreSQL schema,JWKS 本地验签把每请求网络开销降到零,HS256 回退保证旧项目可平滑接入。其"镜像即发行物"的打包模型、启动期即校验的配置策略、以及构建期 import 检查,都是部署第三方扩展时可直接复用的工程模式。扩展由社区(BrighterBalance)贡献,MIT 许可;完整接口契约可参考 TenantExtension 基类,更多打包约定见 extensions 总 README。

【免费下载链接】hindsightHindsight: Agent Memory That Learns项目地址: https://gitcode.com/GitHub_Trending/hindsight2/hindsight

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 17:52:12

电力系统单机无穷大模型解析与应用

1. 单机无穷大系统示意图解析 这张单机无穷大系统示意图展示了电力系统分析中的一个经典模型概念。作为一名在电力行业摸爬滚打十多年的工程师&#xff0c;我经常用这个模型来给新人讲解电网稳定性的基本原理。 单机无穷大系统是电力系统暂态稳定分析中最基础的模型&#xff0…

作者头像 李华
网站建设 2026/9/14 17:51:16

Bun+Oxc+Remix前端工具链深度整合实战指南

1. 项目概述&#xff1a;一场没有硝烟的前端技术“爆破实验”“9月第一周&#xff0c;前端圈又炸了四次”——这句话不是标题党&#xff0c;是过去七天里我刷完23个技术群、翻完47篇源码提交记录、重装了5次开发环境后&#xff0c;最真实的体感。它背后不是情绪宣泄&#xff0c…

作者头像 李华
网站建设 2026/9/14 17:50:14

SpringBoot+Vue便利店管理系统开发实战

1. 华府便利店信息管理系统项目概述华府便利店信息管理系统是一套面向连锁便利店业态的综合性管理解决方案&#xff0c;采用当前主流的前后端分离架构。系统基于SpringBoot 3.2后端框架和Vue 3组合式API前端架构&#xff0c;通过RESTful API实现数据交互&#xff0c;底层采用My…

作者头像 李华
网站建设 2026/9/14 17:50:02

3月9日技术进阶:高效学习方法与实战计划

1. 3月9日进阶学习指南 作为一名从业多年的技术博主&#xff0c;我经常收到读者关于如何系统提升技能的咨询。今天我想分享一个被很多人忽视但极其有效的学习方法——"日期进阶法"&#xff0c;即以特定日期为节点制定阶段性提升计划。3月9日作为一个春季关键时间点&a…

作者头像 李华