Federation & Service

Organizations Federation API

Organizations Service 的完整 Federation HTTP 与 SDK API Reference

Organizations Federation API

这篇文档面向调用 Federation 的客户端和产品后端,完整描述 OrganizationsService 公开的 Action。City Server 如何消费 organization_token 和撤权事件,请阅读 City Server 接入 Organizations

安装与配置

import { Federation } from "@downcity/city";
import { OrganizationsService } from "@downcity/services";

const federation = new Federation({ db });

federation.use(new OrganizationsService({
  max_organizations_per_user: 3,
  organization_token_ttl: "7d",
}));
Option必填默认值说明
max_organizations_per_user一个用户在同一 City 中最多拥有的 active Organization 数量
organization_token_ttl7dOrganization Token 有效期;支持秒数或 s/m/h/d 字符串,最大 30d
fetchglobalThis.fetch撤权事件投递函数,主要用于自定义 Runtime 或测试

当前支持 better-sqlite3 与 PostgreSQL。D1 缺少交互式多表事务,因此 Service 初始化时 会明确拒绝 D1。生产环境应配置 Federation Queue Adapter 来重试 Outbox 事件。

Base URL 与认证

所有公开 Action 位于:

{federation_origin}/v1/organizations/{action}

events/deliver 外,所有接口都要求当前用户的 Federation user_token

Authorization: Bearer ub_<JWT>

user_idcity_id 始终从已经验证的 user_token 取得。客户端不能通过 Body 或 Query 覆盖它们。Token 中的 City 必须和目标 Organization 的 city_id 一致。

events/deliver 是运维接口,只接受 Federation Admin 凭证或 Runtime 内部可信身份。

HTTP 与 SDK 调用规则

  • GET Action 使用 Query String,不读取 JSON Body;
  • POST Action 使用 application/json Body;
  • 成功响应均为 200 application/json
  • 时间字段均为 ISO 8601 字符串;
  • 未发生的时间和执行者字段当前返回空字符串 ""

原始 HTTP:

GET /v1/organizations/get?organization_id=org_01J...
Authorization: Bearer ub_...
POST /v1/organizations/create
Authorization: Bearer ub_...
Content-Type: application/json

{
  "name": "Research Team",
  "server_url": "https://city.example.com"
}

通过 City SDK 调用:

const organizations = city.service("organizations");

const my = await organizations.get("my");

const created = await organizations.action("create").invoke({
  name: "Research Team",
  server_url: "https://city.example.com",
});

公共数据结构

Organization

type Organization = {
  organization_id: string; // org_<ULID>
  city_id: string;
  name: string;
  server_url: string;
  state: "active" | "archived";
  created_by: string;
  created_at: string;
  updated_at: string;
  archived_at: string; // active 时为 ""
};

server_url 是标准化后的 City Server Origin:只能使用绝对 HTTP/HTTPS URL,不允许 凭证、Path、Query 或 Fragment,保存时移除末尾 /。生产环境应使用 HTTPS。

Membership

type Membership = {
  membership_id: string; // mem_<ULID>
  organization_id: string;
  user_id: string;
  role: "owner" | "admin" | "member";
  state: "active" | "removed";
  created_at: string;
  updated_at: string;
  removed_at: string; // active 时为 ""
  removed_by: string; // active 时为 ""
};

removed Membership 永不恢复。用户重新加入会创建新的 membership_id

Join Request

type JoinRequest = {
  request_id: string; // join_<ULID>
  organization_id: string;
  user_id: string;
  state: "pending" | "approved" | "rejected" | "canceled";
  requested_at: string;
  decided_at: string; // pending 时为 ""
  decided_by: string; // pending 时为 ""
};

角色权限

操作OwnerAdminMember
读取 Organization 与成员列表
修改 Organization 名称
修改 server_url
归档 Organization
批准或拒绝 Join Request
任命或撤销 Admin
移除 Member
移除 Admin
转移 Owner
主动退出先转移
签发自己的 Organization Token

Owner/Admin 不能直接添加用户。用户必须先主动创建 Join Request。

Action 总览

MethodAction权限用途
GETmyUser列出当前用户的 Organization
GETgetMember读取一个已加入的 Organization
POSTcreateUser创建 Organization 并成为 Owner
POSTupdateOwner/Admin修改名称
POSTserver/updateOwner修改 City Server URL
POSTarchiveOwner不可恢复地归档 Organization
GETmembership/getMember读取当前用户的 Membership
GETmembers/listMember列出 active Membership
POSTmembers/roleOwner设置 Admin/Member
POSTmembers/removeOwner/Admin移除允许管理的成员
POSTmembers/leaveAdmin/Member主动退出
POSTowner/transferOwner转移唯一 Owner
POSTjoin-requests/createUser主动申请加入
POSTjoin-requests/cancelApplicant取消自己的 pending 申请
GETjoin-requests/listOwner/Admin列出 pending 申请
POSTjoin-requests/decideOwner/Admin批准或拒绝申请
POSTtoken/createMember换取 Organization Token
POSTevents/deliverFederation Admin手动推进撤权 Outbox

Organization API

GET my

列出当前 Token City 中,用户仍有 active Membership 的 Organization。Organization 归档后,如果 Membership 仍 active,仍会出现在列表中并标记 state: "archived"

GET /v1/organizations/my
Authorization: Bearer ub_...
{
  "items": [
    {
      "organization_id": "org_01J...",
      "city_id": "city_01J...",
      "name": "Research Team",
      "server_url": "https://city.example.com",
      "state": "active",
      "created_by": "user_1",
      "created_at": "2026-07-29T10:00:00.000Z",
      "updated_at": "2026-07-29T10:00:00.000Z",
      "archived_at": "",
      "role": "owner",
      "membership_id": "mem_01J...",
      "membership_state": "active"
    }
  ]
}

GET get

读取 Organization 和当前用户的 active Membership。

GET /v1/organizations/get?organization_id=org_01J...
Authorization: Bearer ub_...
{
  "organization": { "organization_id": "org_01J...", "state": "active" },
  "membership": { "membership_id": "mem_01J...", "role": "member", "state": "active" }
}

只有 active Member 可以读取。接口允许读取自己仍有 Membership 的 archived Organization。

POST create

任何已认证用户都可以在额度内创建 Organization。city_id 来自 user_token,创建者 自动获得唯一 Owner Membership。

{
  "name": "Research Team",
  "server_url": "https://city.example.com/"
}

返回:

{
  "organization": {
    "organization_id": "org_01J...",
    "city_id": "city_01J...",
    "name": "Research Team",
    "server_url": "https://city.example.com",
    "state": "active"
  },
  "membership": {
    "membership_id": "mem_01J...",
    "role": "owner",
    "state": "active"
  }
}

创建额度只统计当前拥有的 active Organization。归档会释放槽位,Owner 转移会把槽位 移动给新 Owner。并发创建由数据库事务和唯一额度槽位保护。

POST update

Owner/Admin 修改展示名称:

{
  "organization_id": "org_01J...",
  "name": "New Name"
}

name 去除首尾空白后长度必须为 1–120。返回更新后的 Organization。

POST server/update

只有 Owner 可以修改 Server Origin:

{
  "organization_id": "org_01J...",
  "server_url": "https://new-city.example.com"
}

更新与 organization.server_url.changed Outbox Event 在同一事务中提交。Event 投递给 旧 Server;更新后签发的 Token 只绑定新 Server。相同 URL 是幂等操作,不创建 Event。

POST archive

{ "organization_id": "org_01J..." }

只有 Owner 可以归档。归档不可恢复,并会:

  • 停止所有 Organization 治理写入;
  • 停止 Join Request 与 Token 签发;
  • 释放 Owner 创建额度;
  • 写入 organization.archived 撤权事件;
  • 保留 Organization、Membership、Join Request 和审计记录。

返回更新后的 archived Organization。重复归档返回 410 ORGANIZATION_ARCHIVED

Membership API

GET membership/get

GET /v1/organizations/membership/get?organization_id=org_01J...
Authorization: Bearer ub_...

返回 { organization, membership },其中 Membership 一定属于当前用户且为 active。

GET members/list

GET /v1/organizations/members/list?organization_id=org_01J...
Authorization: Bearer ub_...

任意 active Member 可以调用。返回:

{ "items": [{ "membership_id": "mem_01J...", "role": "member", "state": "active" }] }

历史 removed Membership 不在结果中。

POST members/role

只有 Owner 可以把 active Member/Admin 设置为 adminmember

{
  "organization_id": "org_01J...",
  "membership_id": "mem_01J...",
  "role": "admin"
}

不能用这个 Action 设置 Owner;Owner 必须通过 owner/transfer 转移。

POST members/remove

{
  "organization_id": "org_01J...",
  "membership_id": "mem_01J..."
}
  • Owner 可以移除 Admin 或 Member;
  • Admin 只能移除 Member;
  • Owner 不能被移除,必须先转移;
  • 成功后返回 state: "removed" 的 Membership;
  • Membership 更新与 organization.membership.removed Event 原子提交。

POST members/leave

Admin/Member 主动退出:

{ "organization_id": "org_01J..." }

返回 removed Membership,并写入撤权 Event。Owner 调用会返回 409 OWNER_TRANSFER_REQUIRED

POST owner/transfer

当前 Owner 将唯一 Owner 身份转移给另一个 active Membership:

{
  "organization_id": "org_01J...",
  "membership_id": "mem_new_owner"
}

新 Owner 必须还有可用的 Organization 创建额度。成功后原 Owner 变为 Admin:

{
  "previous_owner": { "membership_id": "mem_old", "role": "admin" },
  "owner": { "membership_id": "mem_new_owner", "role": "owner" }
}

Join Request API

POST join-requests/create

用户通过已知的 organization_id 主动申请:

{ "organization_id": "org_01J..." }

Organizations Service 不提供公开搜索。产品应通过邀请链接、二维码或自己的业务流程 传递 organization_id

首次申请返回 pending Join Request。重复申请幂等返回同一条 pending 记录。如果用户 已经是 active Member,则返回:

{
  "state": "joined",
  "organization": { "organization_id": "org_01J..." },
  "membership": { "membership_id": "mem_01J...", "state": "active" }
}

POST join-requests/cancel

申请人取消自己的 pending Request:

{ "request_id": "join_01J..." }

返回 state: "canceled" 的 Join Request。已决定、已取消或属于其他用户的 Request 统一按不存在处理。

GET join-requests/list

GET /v1/organizations/join-requests/list?organization_id=org_01J...
Authorization: Bearer ub_...

Owner/Admin 可调用,只返回 pending Request:

{ "items": [{ "request_id": "join_01J...", "user_id": "user_2", "state": "pending" }] }

POST join-requests/decide

{
  "request_id": "join_01J...",
  "decision": "approved"
}

decision 只能为 approvedrejected。Owner/Admin 可以决定 pending Request。 批准会在同一事务中创建新的 Member Membership:

{
  "request": { "request_id": "join_01J...", "state": "approved" },
  "membership": { "membership_id": "mem_01J...", "role": "member", "state": "active" }
}

拒绝时只返回已更新的 requestmembership 不存在。rejected/canceled 后允许用户 重新申请,并生成新的 request_id

Organization Token API

POST token/create

active Member 使用当前 user_token 换取目标 City Server 的凭证:

{ "organization_id": "org_01J..." }
{
  "organization_token": "ot_eyJ...",
  "organization_id": "org_01J...",
  "server_url": "https://city.example.com",
  "expires_at": "2026-08-05T10:00:00.000Z"
}

Token 默认有效期为 7 天,aud 精确绑定返回的 server_url,包含 user_idcity_idorganization_idmembership_id。它不包含 Organization Role 或项目 资源权限。客户端只能把它发送给返回的 City Server,不能用它调用 Federation API。

archived Organization、removed Membership 或非成员调用都会失败。

Outbox 运维 API

POST events/deliver

仅 Federation Admin 使用:

POST /v1/organizations/events/deliver
Authorization: Bearer <federation-admin-secret>
Content-Type: application/json

{}
{ "delivered": 12, "pending": 2 }

每次最多尝试一批 pending Event。pending 是尝试后完整 Outbox 中仍未成功的数量。 生产运行时应优先配置 Queue Adapter 自动重试;这个 Action 用于运维重放和故障恢复。

错误响应

{
  "error": {
    "code": "ORGANIZATION_ROLE_DENIED",
    "message": "ORGANIZATION_ROLE_DENIED",
    "type": "server_error"
  }
}
HTTPCode含义
400ORGANIZATION_INPUT_INVALID名称、ID、角色或决策格式错误
400ORGANIZATION_SERVER_URL_INVALIDServer URL 不符合 Origin 规则
401AUTH_REQUIRED 或无 Code缺少、过期或无效的用户凭证
403ORGANIZATION_CITY_MISMATCHToken City 与 Organization 不一致
403NOT_AN_ORGANIZATION_MEMBER当前用户没有 active Membership
403ORGANIZATION_ROLE_DENIED当前治理角色不能执行操作
404ORGANIZATION_NOT_FOUNDOrganization 不存在
404ORGANIZATION_MEMBERSHIP_NOT_FOUND目标 Membership 不存在或不再 active
404JOIN_REQUEST_NOT_FOUNDJoin Request 不存在或不再 pending
409ORGANIZATION_LIMIT_REACHED当前用户的 Owner 创建额度已满
409OWNER_TRANSFER_REQUIRED操作前必须转移 Owner
410ORGANIZATION_ARCHIVEDOrganization 已归档,写入和 Token 签发停止

客户端应基于 error.code 处理业务分支,不要匹配人类可读的 message