Skip to content

RFC: 私有包访问控制 (Package Access Control) #1002

@elrrrrrrr

Description

@elrrrrrrr

RFC: cnpmcore 私有包访问控制

  • Status: Draft
  • Date: 2026-03-10

1. 背景与动机

cnpmcore 当前缺乏包级别的访问控制。现有 isPrivate 字段仅标记包来源(用户发布 vs 上游同步),所有 GET 端点无鉴权,任何人都可读取所有包。

企业场景中,需要将部分包限制为仅特定团队可访问。例如:

  • @mycompany/open-sdk — 公开包,所有人可读
  • @mycompany/internal-sdk — 仅 core 团队可访问
  • @mycompany/frontend-utils — 仅 frontend 团队可访问

这要求访问控制的粒度在 Team × Package 级别,而非简单的 Org 级别。

目标

  1. 每个包独立设置 access: public | restricted
  2. 引入 Organization → Team → Package 三层权限模型(对齐 npm 官方)
  3. restricted 包仅被授权的 Team 成员 / Admin / Maintainer 可读
  4. 未登录返回 401,已登录无权限返回 403
  5. public 包行为完全不变,零额外开销

非目标(本期不做)

  • 搜索结果过滤 restricted 包(后续 PR)
  • 写权限通过 Team 控制(现有 ensurePublishAccess 通过 Maintainer 机制已够用)
  • Team 级别的 read-write vs read-only 区分(本期 Team 只控制读权限,写权限仍由 Maintainer 控制)

业务约束

  1. 仅 registry 私有包支持发包

    • 只有 registry_id = 'self'(本地发布)的包才能由普通用户发布和管理 access
    • 从上游(如 npmjs.org)同步的包 registry_id 不是 self,这些包不可被普通用户发布或覆盖
  2. 公网同步包仅 Admin 可应急发布

    • 来自上游注册表的包(registry_id !== 'self')只有 Admin 可以应急发布新版本
    • 普通用户无法通过 npm publish 覆盖公网同步包
  3. 同 scope 下私有包与公网包共存

    • 例如 @cnpm/public-utils 已从 npmjs.org 同步(registry_id !== 'self'),同时 @cnpm 也是私有 Org 的 scope
    • 此时 @cnpm/public-utils 不受私有 access 控制影响,因为它的 registry_id 不是 self
    • 用户不能以私有包身份重新发布 @cnpm/public-utilsregistry_id 不匹配,拒绝覆盖)
    • 但用户可以发布新的私有包如 @cnpm/internal-sdkregistry_id = 'self'),并设为 restricted

2. 整体设计

2.1 数据模型

参照 npm 官方权限模型,采用 Org → Team → Package 三层结构:

┌─────────────────────────────────────────────────────────────┐
│                       Org ("mycompany")                        │
│                    ⟺ scope "@mycompany"                        │
│                                                             │
│  ┌──────────────────┐  ┌──────────────────┐                 │
│  │ Team: developers │  │ Team: core       │  ...            │
│  │ (默认团队)        │  │                  │                 │
│  │                  │  │                  │                 │
│  │ Members:         │  │ Members:         │                 │
│  │  - alice (owner) │  │  - alice         │                 │
│  │  - bob           │  │  - charlie       │                 │
│  │  - charlie       │  │                  │                 │
│  │                  │  │ Packages:        │                 │
│  │ Packages:        │  │  - internal-sdk  │                 │
│  │  - open-sdk      │  │  - core-lib      │                 │
│  │  - frontend-utils│  │                  │                 │
│  └──────────────────┘  └──────────────────┘                 │
└─────────────────────────────────────────────────────────────┘

Package: @mycompany/open-sdk        access=public     → 所有人可读
Package: @mycompany/frontend-utils  access=restricted  → developers team 成员可读
Package: @mycompany/internal-sdk    access=restricted  → core team 成员可读
Package: @mycompany/core-lib        access=restricted  → core team 成员可读

ER 关系:

Org ──1:N──▶ OrgMember ──▶ User       (谁属于这个组织)
Org ──1:N──▶ Team                      (组织下的团队)
Team ──1:N──▶ TeamMember ──▶ User      (谁属于这个团队,必须先是 OrgMember)
Team ──N:N──▶ TeamPackage ──▶ Package  (团队可以访问哪些包)

2.2 developers 默认团队

参照 npm 行为:

  1. 创建 Org 时自动创建 developers Team
  2. 新成员加入 Org 时自动加入 developers Team
  3. developers Team 不可删除
  4. 在 Org scope 下新发布 restricted 包时,自动授权给 developers Team

这样的默认行为保证了:

  • 简单场景(不需要细粒度控制)只需创建 Org,所有成员通过 developers team 自动获得访问权限
  • 需要细粒度控制时,创建额外的 Team,将特定包只授权给特定 Team

2.3 读取权限判断流程

+---------------------------+
| ensurePackageReadAccess() |
+---------------------------+
            |
            v
+------------------------+     +--------+
| pkg.access == "public"?|--Y->| PASS   |  <-- public 包,零开销
+------------------------+     +--------+
            | N
            v
+------------------------+     +-----------+
| Authorization header   |--N->| 401       |
| 有用户?                 |     | 未登录     |
+------------------------+     +-----------+
            | Y
            v
+------------------------+     +--------+
| user 是 Admin?          |--Y->| PASS   |
+------------------------+     +--------+
            | N
            v
+------------------------+     +--------+
| user 是 Maintainer?     |--Y->| PASS   |  <-- 查 maintainers 表
+------------------------+     +--------+
            | N
            v
+------------------------+
| pkg 有 scope?           |--N-+
+------------------------+     |
            | Y                |
            v                  |
+------------------------+     |
| 查 team_packages 表     |     |
| WHERE package_id = ?    |     |
| 得到 teamId 列表        |     |
+------------------------+     |
            |                  |
            v                  |
+---------------------------+  |
| 查 team_members 表        |  |
| WHERE team_id IN (...)    |  |
|   AND user_id = ?         |  |
+---------------------------+  |
            |                  |
            v                  |
+------------------------+    |   +--------+
| 找到匹配记录?           |--Y--->| PASS   |
+------------------------+    |   +--------+
            | N               |
            v                 |
       +----------+           |
       | 403      |<----------+
       | 无权限    |
       +----------+

注意:Team 权限查询不使用 JOIN,而是分两步单表查询:

  1. 先查 team_packages 拿到 teamId 列表
  2. 再查 team_members 判断用户是否属于其中任一 team

2.4 读取链路鉴权位置

权限检查在缓存之前。ShowPackageController 先查 DB 获取 Package 实体做权限判断,再走缓存逻辑。

                         public 包: 直接跳过(零开销)
                              |
+----------+   +--------------+-+   +----+   +----------+
| Request  |-->| findPackage()  |-->|Auth|-->| Cache/DB |-->  Response
+----------+   +----------------+   +----+   +----------+
                                      |
                                      | restricted 包:
                                      | 401 未登录
                                      | 403 无权限
                                      v
                                  Error Response

3. PR 拆分

PR 内容 依赖
PR1 Org + Team 数据层 + 管理 API
PR2 Package.access + Team 授权 + 读取链路鉴权 PR1
PR3 搜索结果过滤 restricted 包 PR2

4. PR1: Org + Team 数据层 + 管理 API

4.1 实体设计

Org

字段 类型 说明
orgId string 唯一标识
name string 组织名,对应 scope 去掉 @(如 mycompany
description string 描述

OrgMember(组织成员,控制"谁属于组织"及组织管理权限)

字段 类型 说明
orgMemberId string 唯一标识
orgId string 所属组织
userId string 用户 ID
role 'owner' | 'member' owner 可管理组织和团队

Team(团队,权限分配的核心单元)

字段 类型 说明
teamId string 唯一标识
orgId string 所属组织
name string 团队名(如 developers, core
description string 描述

TeamMember(团队成员,必须先是 OrgMember)

字段 类型 说明
teamMemberId string 唯一标识
teamId string 所属团队
userId string 用户 ID

TeamPackage(团队 ↔ 包授权关系)

字段 类型 说明
teamPackageId string 唯一标识
teamId string 团队 ID
packageId string 包 ID

关于 read-only / read-write:npm 官方区分 read-onlyread-write。本期简化为只控制读权限(TeamPackage 记录存在即表示该 Team 可读该 Package)。写权限仍通过已有的 Maintainer 机制控制。后续如需 Team 级别写权限控制,在 TeamPackage 上加 permissions 字段即可。

4.2 DDL

CREATE TABLE IF NOT EXISTS orgs (
  id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  gmt_create    DATETIME(3) NOT NULL,
  gmt_modified  DATETIME(3) NOT NULL,
  org_id        VARCHAR(24) NOT NULL,
  name          VARCHAR(214) NOT NULL,
  description   VARCHAR(10240) DEFAULT NULL,
  UNIQUE KEY uk_org_id (org_id),
  UNIQUE KEY uk_name (name)
);

CREATE TABLE IF NOT EXISTS org_members (
  id              BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  gmt_create      DATETIME(3) NOT NULL,
  gmt_modified    DATETIME(3) NOT NULL,
  org_member_id   VARCHAR(24) NOT NULL,
  org_id          VARCHAR(24) NOT NULL,
  user_id         VARCHAR(24) NOT NULL,
  role            VARCHAR(20) NOT NULL DEFAULT 'member',
  UNIQUE KEY uk_org_member_id (org_member_id),
  UNIQUE KEY uk_org_id_user_id (org_id, user_id),
  KEY idx_user_id (user_id)
);

CREATE TABLE IF NOT EXISTS teams (
  id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  gmt_create    DATETIME(3) NOT NULL,
  gmt_modified  DATETIME(3) NOT NULL,
  team_id       VARCHAR(24) NOT NULL,
  org_id        VARCHAR(24) NOT NULL,
  name          VARCHAR(214) NOT NULL,
  description   VARCHAR(10240) DEFAULT NULL,
  UNIQUE KEY uk_team_id (team_id),
  UNIQUE KEY uk_org_id_name (org_id, name),
  KEY idx_org_id (org_id)
);

CREATE TABLE IF NOT EXISTS team_members (
  id              BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  gmt_create      DATETIME(3) NOT NULL,
  gmt_modified    DATETIME(3) NOT NULL,
  team_member_id  VARCHAR(24) NOT NULL,
  team_id         VARCHAR(24) NOT NULL,
  user_id         VARCHAR(24) NOT NULL,
  UNIQUE KEY uk_team_member_id (team_member_id),
  UNIQUE KEY uk_team_id_user_id (team_id, user_id),
  KEY idx_user_id (user_id)
);

CREATE TABLE IF NOT EXISTS team_packages (
  id                BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  gmt_create        DATETIME(3) NOT NULL,
  gmt_modified      DATETIME(3) NOT NULL,
  team_package_id   VARCHAR(24) NOT NULL,
  team_id           VARCHAR(24) NOT NULL,
  package_id        VARCHAR(24) NOT NULL,
  UNIQUE KEY uk_team_package_id (team_package_id),
  UNIQUE KEY uk_team_id_package_id (team_id, package_id),
  KEY idx_package_id (package_id)
);

-- Package 表新增列
ALTER TABLE packages ADD COLUMN access VARCHAR(20) DEFAULT NULL
  COMMENT 'package access level: public or restricted';

4.3 Repository 层

OrgRepository

方法 说明
findOrgByName(name) scope → org 查找
saveOrg(org) / removeOrg(orgId) CRUD
saveMember(member) Upsert(已存在则更新 role)
removeMember(orgId, userId) 删除成员(同时清理其所有 TeamMember)
listMembers(orgId) JOIN User 表

TeamRepository

方法 说明
findTeam(orgId, teamName) 查找团队
listTeamsByOrgId(orgId) 列出 org 下所有团队
saveTeam(team) / removeTeam(teamId) CRUD(删除时级联清理 members + packages)
addMember(teamId, userId) / removeMember(teamId, userId) 管理团队成员
listMembers(teamId) JOIN User 表
addPackage(teamId, packageId) / removePackage(teamId, packageId) 管理团队可访问的包
listPackages(teamId) 列出团队可访问的包
hasPackageAccess(packageId, userId) 读取链路核心查询

hasPackageAccess 实现思路(分步单表查询,不使用 JOIN):

async hasPackageAccess(packageId: string, userId: string): Promise<boolean> {
  // Step 1: 查包被授权给了哪些 team
  const teamPackages = await TeamPackage.find({ packageId });
  if (teamPackages.length === 0) return false;

  // Step 2: 查用户是否在其中任一 team
  const teamIds = teamPackages.map(tp => tp.teamId);
  const member = await TeamMember.findOne({
    teamId: teamIds,  // WHERE team_id IN (...)
    userId,
  });
  return !!member;
}

4.4 Service 层

OrgService

方法 说明
createOrg(cmd) 创建 org + 自动创建 developers Team + 创建者设为 owner 并加入 developers
removeOrg(orgId) 删除 org + 级联删除所有 team / member / team_package
addMember(orgId, userId, role) 加入 org + 自动加入 developers Team
removeMember(orgId, userId) 退出 org + 自动从所有 Team 移除

TeamService

方法 说明
createTeam(orgId, name) 创建团队(校验 org 存在)
removeTeam(teamId) 删除团队(developers 不可删除)
addMember(teamId, userId) 加入团队(校验用户是 OrgMember)
removeMember(teamId, userId) 从团队移除
grantPackageAccess(teamId, packageId) 授权团队访问包
revokePackageAccess(teamId, packageId) 撤销授权

4.5 管理 API

标注 🔌 npm CLI 的端点与 npm CLI 命令兼容,可直接通过 npm 客户端调用。
其余为 cnpmcore 扩展 API,需通过 HTTP 客户端(如 curl)调用。

Org 管理 — 🔌 npm CLI 兼容(npm org

端点 Method 权限 npm CLI 说明
/-/org PUT Admin 创建 org(自动创建 developers team)
/-/org/:orgName GET 登录用户 查看 org 信息
/-/org/:orgName DELETE Admin 删除 org
/-/org/:orgName/member GET 登录用户 npm org ls <orgName> 列出 org 成员
/-/org/:orgName/member PUT Admin 或 org owner npm org set <orgName> <user> [owner] 添加成员(自动加入 developers)
/-/org/:orgName/member/:username DELETE Admin 或 org owner npm org rm <orgName> <user> 移除成员(自动从所有 team 移除)

Team 管理 — 🔌 npm CLI 兼容(npm team

端点 Method 权限 npm CLI 说明
/-/org/:orgName/team PUT Admin 或 org owner npm team create <scope:team> 创建团队
/-/org/:orgName/team GET 登录用户 npm team ls <scope> 列出 org 下所有团队
/-/org/:orgName/team/:teamName GET 登录用户 查看团队详情
/-/org/:orgName/team/:teamName DELETE Admin 或 org owner npm team destroy <scope:team> 删除团队(developers 不可删)
/-/org/:orgName/team/:teamName/member GET 登录用户 npm team ls <scope:team> 列出团队成员
/-/org/:orgName/team/:teamName/member PUT Admin 或 org owner 添加团队成员
/-/org/:orgName/team/:teamName/member/:username DELETE Admin 或 org owner 移除团队成员

Team ↔ Package 授权 — 🔌 npm CLI 部分兼容(npm access

端点 Method 权限 npm CLI 说明
/-/org/:orgName/team/:teamName/package GET 登录用户 npm access ls-packages <scope:team> 列出团队可访问的包
/-/org/:orgName/team/:teamName/package PUT Admin 或 org owner npm access grant read-only <scope:team> <pkg> 授权团队访问包
/-/org/:orgName/team/:teamName/package/:fullname DELETE Admin 或 org owner npm access revoke <scope:team> <pkg> 撤销授权

5. PR2: Package.access + 读取链路鉴权

5.1 Package.access 字段

// app/common/constants.ts
export enum PackageAccess {
  public = 'public',
  restricted = 'restricted',
}

Package 实体新增 access 字段,默认 'public'。已有包 access 为 NULL,实体层默认为 public,无需数据迁移。

5.2 PackageAccessService

核心方法 ensureReadAccess,判断流程见 2.3 流程图。

class PackageAccessService {
  // 读权限检查(详细流程见 2.3)
  async ensureReadAccess(pkg: Package, user?: User): Promise<void>;

  // 设置包的 access 级别(仅 registry_id=self 的包可操作)
  async setPackageAccess(pkg: Package, access: PackageAccess): Promise<void>;
}

Team 权限查询不使用 JOINhasPackageAccess 分两步单表查询(见 4.3),restricted 包访问量不高,单包查询性能可接受。

5.3 发布约束与自动授权

发布流程:

npm publish --access restricted
            |
            v
+---------------------------+
| 解析 scope & name          |
+---------------------------+
            |
            v
+---------------------------+     +----------------------------+
| 包已存在?                  |--Y->| registry_id == "self"?     |
+---------------------------+     +----------------------------+
            | N                        | Y              | N
            v                          v                v
+---------------------------+   +-----------+   +----------------+
| 新包: registry_id = "self" |   | 正常发布   |   | user 是 Admin?  |
| access = cmd.access        |   +-----------+   +----------------+
+---------------------------+                     | Y         | N
            |                                     v           v
            v                              +-----------+ +--------+
+---------------------------+              | 应急发布   | | 403    |
| access == "restricted"    |              +-----------+ | 拒绝   |
| && scope 有对应 Org?       |                            +--------+
+---------------------------+
            | Y
            v
+---------------------------+
| 自动授权给 developers Team  |
| TeamPackage.create(        |
|   developersTeam, pkg)     |
+---------------------------+

发布约束:

  • 只有 registry_id = 'self' 的包可由普通用户发布(npm publish
  • 公网同步包(registry_id !== 'self')仅 Admin 可应急发布新版本
  • 如果一个包名已从上游同步(registry_id !== 'self'),普通用户不能以私有包身份覆盖发布

自动授权: 当在 Org scope 下发布 restricted 包时,自动将包授权给 developers Team。这保证了简单场景下(只有一个 developers team)的开箱即用。

5.4 加鉴权的端点(8 个 Controller)

Controller 方法 鉴权时机
ShowPackageController show() 缓存之前 findPackage + 检查
ShowPackageVersionController show() showPackageVersionManifest 返回 pkg 后
DownloadPackageVersionTar download() SyncMode.all NFS 快速路径之前 + 正常流程中
PackageVersionFileController #getPackageVersion() 覆盖 listFiles()raw()
PackageTagController showTags() getPackageEntityByFullname
AccessController listCollaborators() findPackage
DownloadController showPackageDownloads() findPackage
PackageBlockController listPackageBlocks() getPackageEntityByFullname

5.5 新增 API — 🔌 npm CLI 兼容(npm access

端点 Method 权限 Body npm CLI 说明
/-/package/:fullname/access PUT Maintainer 或 Admin { access: "public" | "restricted" } npm access public/restricted <pkg> 设置包访问级别
/-/package/:fullname/collaborators GET 见 5.4 npm access ls-collaborators <pkg> 列出包协作者

注意: 只有 registry_id = 'self' 的包才能修改 access。公网同步包不允许设置 access。


6. 典型使用场景

场景 1:简单模式(只用 developers team)

# 1. Admin 创建 org(HTTP API)
curl -X PUT https://registry.example.com/-/org \
  -H "Authorization: Bearer <admin-token>" \
  -d '{ "name": "mycompany" }'
# → 自动创建 developers team

# 2. 添加成员(npm CLI 兼容)
npm org set mycompany alice --registry=https://registry.example.com
npm org set mycompany bob --registry=https://registry.example.com
# → alice, bob 自动加入 developers team

# 3. 发布 restricted 包(npm CLI 兼容)
npm publish --access restricted --registry=https://registry.example.com
# → @mycompany/sdk (registry_id=self) 自动授权给 developers team

# 4. alice 和 bob 可以 install,其他人 403
npm install @mycompany/sdk  # ✅ alice/bob (在 developers team 中)
npm install @mycompany/sdk  # ❌ charlie → 403

场景 2:细粒度控制(多个 team)

# 1. 创建额外 team(npm CLI 兼容)
npm team create @mycompany:core --registry=https://registry.example.com
npm team create @mycompany:frontend --registry=https://registry.example.com

# 2. 分配成员到不同 team(HTTP API)
curl -X PUT https://registry.example.com/-/org/mycompany/team/core/member \
  -d '{ "user": "alice" }'
curl -X PUT https://registry.example.com/-/org/mycompany/team/frontend/member \
  -d '{ "user": "bob" }'

# 3. 设置包为 restricted 并调整 team 授权
npm access restricted @mycompany/internal-sdk --registry=https://registry.example.com

# 将 developers team 的默认授权移除,只给 core team
npm access revoke @mycompany:developers @mycompany/internal-sdk --registry=https://registry.example.com
npm access grant read-only @mycompany:core @mycompany/internal-sdk --registry=https://registry.example.com

# 结果:
# @mycompany/internal-sdk → 只有 core team (alice) 可读
# @mycompany/public-utils → developers team (alice + bob) 可读

场景 3:同 scope 下私有包与公网同步包共存

# 背景:@cnpm/public-utils 已从 npmjs.org 同步到私有 registry
#       registry_id = "https://registry.npmjs.org"(非 self)
#       同时 @cnpm 也是私有 Org 的 scope

# ❌ 尝试以私有包发布 @cnpm/public-utils — 被拒绝
npm publish  # 当前目录的 package.json: { "name": "@cnpm/public-utils" }
# → 403: 该包已从上游同步,registry_id 不是 self,不允许覆盖

# ✅ 发布全新的私有包 @cnpm/internal-sdk — 成功
npm publish --access restricted  # package.json: { "name": "@cnpm/internal-sdk" }
# → registry_id = self,正常发布为 restricted 包

# 两个包在同一 scope 下共存:
# @cnpm/public-utils  → registry_id 非 self,public,任何人可读
# @cnpm/internal-sdk  → registry_id = self,restricted,仅授权 team 可读

7. 测试方案

7.1 Org + Team Service 测试

# 场景 预期
1 创建 org 自动创建 developers team developers team 存在
2 添加 org member 自动加入 developers team 新成员在 developers 中
3 移除 org member 自动从所有 team 移除 team 中不再有该用户
4 developers team 不可删除 抛出 ForbiddenError
5 创建/删除自定义 team 正常 CRUD
6 team member 必须先是 org member 非 org 成员加入 team 抛错

7.2 PackageAccessService 测试

# 场景 预期
1 public 包,无用户 通过
2 public 包,任意用户 通过
3 restricted 包,无用户 401
4 restricted 包,admin 通过
5 restricted 包,maintainer 通过
6 restricted 包,属于有权 team 的成员 通过
7 restricted 包,属于无权 team 的成员 403
8 restricted 包,org member 但不在有权 team 中 403
9 restricted 包,非 org 成员 403

注意 case 8:Org member 不等于有权限。必须属于被授权的 Team 才行。

7.3 Controller 集成测试

6 个端点 × 4 场景(public 200 / restricted 401 / team member 200 / stranger 403)

7.4 自动授权测试

# 场景 预期
1 在 org scope 下发布 restricted 包 developers team 自动获得授权
2 发布 public 包 不创建 team_package 记录

8. 关键设计决策

8.1 三层模型 Org → Team → Package(非 Org → Package)

Org 成员不直接获得包访问权限,必须通过 Team 中转。

原因: 同 scope 下的包可能属于不同团队。@mycompany/internal-sdk 只给 core team,@mycompany/frontend-utils 只给 frontend team。Org 级别的成员关系粒度太粗。

简单场景不受影响: developers 默认 team + 自动授权机制保证了不需要细粒度控制时零配置开箱即用。

8.2 Org 与 scope 约定关联,不加外键

org.name = "mycompany" 对应 scope = "@mycompany",通过命名约定关联。

原因: 解耦。已有 Scope 模型只做注册表映射,不适合耦合权限语义。

8.3 access 是包维度

同 scope 不同包可以有不同 access。@mycompany/open-sdk 可以 public,@mycompany/internal-sdk 可以 restricted。

8.4 NULL access = public

已有包 access 为 NULL,实体层默认为 'public'。无需数据迁移,向后完全兼容。

8.5 public 包零开销

ensurePackageReadAccess() 第一行判断 access 字段,public 包不做任何认证查询。

8.6 权限检查在缓存之前

风险: 每次请求多一次 findPackage DB 查询。
缓解: public 包第一行就返回。restricted 包访问量低,多一次查询可接受。

8.7 401 vs 403

状态码 含义 场景
401 未认证 访问 restricted 包但未提供 Authorization header
403 无权限 已登录但不在有权限的 team 中

8.8 本期不区分 read-only / read-write

npm 官方 TeamPackage 有 read-onlyread-write 两种权限。本期简化为 TeamPackage 记录存在即可读。写权限仍通过已有的 Maintainer 机制控制。

扩展方式: 后续在 team_packages 表加 permissions VARCHAR(20) 字段,默认 'read-only',即可支持。

8.9 access 控制仅作用于 registry 私有包

只有 registry_id = 'self' 的包才参与 access 控制。上游同步的包(registry_id !== 'self')始终视为 public,不受 restricted 限制。

原因: 同步包来自公网,其可见性不应受私有 Org 权限控制。同时防止用户通过设置 restricted 来"劫持"公网包名。

8.10 公网同步包仅 Admin 可应急发布

已从上游同步的包(registry_id !== 'self'),普通用户不能通过 npm publish 覆盖。仅 Admin 可应急发布。

原因: 防止普通用户意外或恶意覆盖公网包内容。Admin 应急发布用于修复紧急安全问题等场景。

8.11 OrgMember role 设计

Role 权限
owner 管理 org 和 team(增删成员、创建/删除 team、授权/撤销包访问)
member 通过 team 读取 restricted 包

Admin 是全局角色,不在 OrgMember 中表达。


9. 向后兼容性

项目 影响
已有包 access = NULL → 视为 public,行为不变
已有 API 所有 GET 端点行为不变(public 包无额外开销)
公网同步包 registry_id !== 'self' 的包不受 access 控制影响,始终可读
发布行为 公网同步包仍不允许普通用户覆盖(已有行为),仅新增 Admin 应急发布能力
数据库 新增 5 张表 + 1 个 ALTER TABLE,无破坏性变更
npm 客户端 核心操作兼容 npm org / npm team / npm access 命令

10. 文件清单

新增文件

文件 说明
app/core/entity/Org.ts Org 实体
app/core/entity/OrgMember.ts OrgMember 实体
app/core/entity/Team.ts Team 实体
app/core/entity/TeamMember.ts TeamMember 实体
app/core/entity/TeamPackage.ts TeamPackage 实体
app/repository/model/Org.ts Org ORM Model
app/repository/model/OrgMember.ts OrgMember ORM Model
app/repository/model/Team.ts Team ORM Model
app/repository/model/TeamMember.ts TeamMember ORM Model
app/repository/model/TeamPackage.ts TeamPackage ORM Model
app/repository/OrgRepository.ts Org 数据访问层
app/repository/TeamRepository.ts Team 数据访问层
app/core/service/OrgService.ts Org 业务逻辑
app/core/service/TeamService.ts Team 业务逻辑
app/core/service/PackageAccessService.ts 访问控制核心逻辑
app/port/controller/OrgController.ts Org 管理 API
app/port/controller/TeamController.ts Team 管理 API

修改文件

文件 变更
app/common/constants.ts 新增 PackageAccess 枚举
app/core/entity/Package.ts 加 access 字段
app/core/service/PackageManagerService.ts publish() 支持 access + 自动授权 developers team
app/repository/model/Package.ts 加 access 列
app/repository/PackageRepository.ts 加 isPackageMaintainer()
app/port/controller/AbstractController.ts 加 ensurePackageReadAccess()
app/port/controller/AccessController.ts 加鉴权 + setAccess API
app/port/controller/DownloadController.ts 加鉴权
app/port/controller/PackageBlockController.ts 加鉴权
app/port/controller/PackageTagController.ts 加鉴权
app/port/controller/PackageVersionFileController.ts 加鉴权
app/port/controller/package/DownloadPackageVersionTar.ts 加鉴权
app/port/controller/package/SavePackageVersionController.ts 传 access
app/port/controller/package/ShowPackageController.ts 加鉴权(缓存前)
app/port/controller/package/ShowPackageVersionController.ts 加鉴权
sql/ddl_mysql.sql 加表 + 列
sql/ddl_postgresql.sql 加表 + 列
sql/mysql/3.77.0.sql 迁移文件

11. 已决策事项

# 问题 决策
1 Org 创建权限 仅 Admin 可创建
2 access 默认值 默认 public,不支持 Org 级别的 defaultAccess,除非用户发布时手动传 --access restricted
3 CDN 缓存 仅私有部署场景,不考虑 CDN 缓存问题
4 npm CLI 兼容 核心 API(npm org, npm team, npm access)按 npm CLI 协议设计,cnpmcore 同时支持管理员维度的配置
5 已有包迁移 不在本次 RFC 范围内
6 性能 不使用 JOIN,改为分步单表查询(见 4.3)。restricted 包访问量不高,单包查询性能可接受
7 同步包 access 保护 只在 Service 层校验,不加 DB 约束

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions