文档

为服务器和 CI 任务分配专属访问权限

服务客户端是使用自身凭据在单个项目上工作的服务器、脚本或 CI 任务。它使用私钥或 secret 登录,接收 15 分钟访问令牌,无需人员批准每次运行。

服务客户端是什么

  • 归项目所有。 当创建该项目的人员或智能体离开时,项目保留客户端。其请求作为智能体请求计费,计入相同的包含请求。
  • 只读、提议或管理。 创建时设置,类似已登录的智能体。只读仅列出和读取;提议还可上传媒体并提议播放列表和屏幕更改供人员审核;管理还可更改、发布和消费。当项目要求智能体更改需审核时,客户端最多以提议身份工作。详见 只读、提议与管理。
  • 能力。 包含创建者自身的任何 screens、content、playlists、advertising、reports 和 project。
  • 受限。 服务客户端永远不会创建或管理服务客户端、成员、邀请或智能体登录。
  • 两种登录方式。 注册的公钥(private_key_jwt,推荐方式)或密钥(client_secret_basic)。客户端最多持有两者各两个,方便无停机轮换。

组织跨其所有项目最多持有 50 个实时服务客户端。客户端 ID 以 scl_ 开头,密钥以 sr_cs_ 开头。

创建密钥

在运行客户端的机器上生成 Ed25519 密钥对。私钥永远不会离开该机器;screenRIG 仅保留公钥部分。

openssl genpkey -algorithm ed25519 -out screenrig-client.pem
chmod 600 screenrig-client.pem

EC P-256 密钥和至少 2048 位的 RSA 密钥也可用。

创建客户端

在控制台中。打开访问并选择新建服务客户端。命名,选择只读、提议或管理,勾选其权限,并以 JWK 格式粘贴公钥,或选择生成客户端密钥。使用 Node 打印公钥 JWK:

node -e 'const c=require("node:crypto");const k=c.createPublicKey(require("node:fs").readFileSync("screenrig-client.pem"));console.log(JSON.stringify(k.export({format:"jwk"})))'

使用 CLI。 智能体需要带有 identity 作用域的身份管理权限、自身在目标项目中获授的包含 project 能力的管理权限,以及一名已验证且管理该项目所属组织的负责人。

对于新注册的智能体,受邀人员必须先接受注册邀请。仅有项目管理权限不足以授权更改凭据。

--key-file 接受 JWK 或 PEM 文件,仅发送其公钥部分:

screenrig service-client create --name "Nightly report" --access read \
  --capability reports --key-file screenrig-client.pem

要改用密钥,请添加 --secret-file PATH。CLI 会以模式 0600 将密钥写入该新文件;只显示一次,之后不再显示。

screenrig service-client show scl_ID 列出客户端的密钥 ID(kid)和密钥密文。没有自己的 kid 的密钥通过其 RFC 7638 指纹识别。

上传者客户端

代理机构的智能体会获得一个仅限所选播放列表和媒体文件夹的客户端:将--uploader --folder-id mfd_FOLDER[,mfd_FOLDER] --playlist-id pl_PLAYLIST[,pl_PLAYLIST]添加到service-client create。上传者客户端仅拥有content和playlists权限,除非--access另行指定,否则以提议级别运行。凭其令牌,它可以上传到自己的文件夹(screenrig media upload FILE --folder mfd_FOLDER),并添加、替换和移除自己播放列表的页面(screenrig playlist page add|replace|remove);其他所有请求都会返回outside_upload_scope,其upload_scope列出它可以使用的内容。项目所有者之后可更改客户端的文件夹和播放列表;更改会应用于已签发的令牌。请参阅上传者。

获取访问令牌

向令牌端点请求带有 client_credentials 授权的令牌。响应包含 access_token、token_type: Bearer、expires_in(900 秒)和 scope。无刷新令牌:过期时重新请求。

使用密钥时,使用 HTTP Basic 认证。请求体中的密钥将被拒绝:

curl -s https://api.screenrig.ai/oauth/token \
  -u "$SCREENRIG_CLIENT_ID:$SCREENRIG_CLIENT_SECRET" \
  -d grant_type=client_credentials

使用密钥时,发送签名的客户端断言代替密钥:

curl -s https://api.screenrig.ai/oauth/token \
  -d grant_type=client_credentials \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  -d client_assertion="$ASSERTION"

断言是用你的私钥签名的 JWT:

字段值
标题 algEdDSA,ES256 或 RS256,匹配密钥
标题 kid已注册密钥的 ID
iss 和 sub客户端 ID
audhttps://api.screenrig.ai 或 https://api.screenrig.ai/oauth/token,作为一个字符串
iat, exp最多相隔 5 分钟
jti每次断言使用一个新的随机值;重复的值将被拒绝

拒绝使用标题 jku、x5u、jwk 和 x5c。

请求减少

两个可选参数缩小令牌范围:

  • scope:access:read 或 access:propose 赋予令牌比客户端更低的权限级别,且能力名称仅保留那些能力,例如 scope=access:read reports。超出客户端范围的是 invalid_scope。
  • resource:https://api.screenrig.ai/api,默认,用于 REST API,或 https://api.screenrig.ai/mcp 用于 远程服务器。

调用 API

将令牌作为承载令牌发送。它作用于客户端自身项目:

curl -s https://api.screenrig.ai/api/screens \
  -H "Authorization: Bearer $TOKEN"

节点:带密钥的缓存令牌

此助手使用 Node 内置的 crypto 签署断言,将令牌保存在内存中,并在令牌过期前一分钟请求新令牌。将CLIENT_KEY_ID设置为密钥的kid,来自service-client show:

import { createPrivateKey, randomUUID, sign } from "node:crypto";
import { readFileSync } from "node:fs";

const ISSUER = "https://api.screenrig.ai";
const { SCREENRIG_CLIENT_ID: clientId, SCREENRIG_CLIENT_KEY_FILE: keyFile, CLIENT_KEY_ID: kid } = process.env;
const key = createPrivateKey(readFileSync(keyFile));
const part = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
let cached;

function assertion() {
  const now = Math.floor(Date.now() / 1000);
  const input = `${part({ alg: "EdDSA", typ: "JWT", kid })}.${part({ iss: clientId, sub: clientId, aud: ISSUER, iat: now, exp: now + 60, jti: randomUUID() })}`;
  return `${input}.${sign(null, Buffer.from(input), key).toString("base64url")}`;
}

export async function accessToken() {
  if (cached && cached.expiresAt - 60_000 > Date.now()) return cached.token;
  const response = await fetch(`${ISSUER}/oauth/token`, {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      client_assertion: assertion(),
    }),
  });
  const body = await response.json();
  if (!response.ok) throw new Error(`screenRIG token request refused: ${body.error}: ${body.error_description ?? ""}`);
  cached = { token: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 };
  return cached.token;
}

Python:带密钥的缓存令牌

import base64, json, os, time, urllib.parse, urllib.request

ISSUER = "https://api.screenrig.ai"
_cached = {"token": None, "expires_at": 0.0}


def access_token() -> str:
    if _cached["token"] and _cached["expires_at"] - 60 > time.time():
        return _cached["token"]
    pair = f'{os.environ["SCREENRIG_CLIENT_ID"]}:{os.environ["SCREENRIG_CLIENT_SECRET"]}'
    request = urllib.request.Request(
        f"{ISSUER}/oauth/token",
        data=urllib.parse.urlencode({"grant_type": "client_credentials"}).encode(),
        headers={"Authorization": "Basic " + base64.b64encode(pair.encode()).decode()},
    )
    with urllib.request.urlopen(request, timeout=10) as response:
        body = json.load(response)
    _cached.update(token=body["access_token"], expires_at=time.time() + body["expires_in"])
    return body["access_token"]

以服务客户端身份运行 CLI

以该客户端身份让 CLI 登录一次,使用其专属配置。配置会记录客户端以及保存其私钥(推荐)或密钥的文件,绝不记录私钥或密钥本身:

screenrig --config /run/screenrig/ci.json login --client-id scl_ID \
  --client-key-file /run/secrets/screenrig-client.pem
screenrig --config /run/screenrig/ci.json screen list

如需使用密钥,请用 --client-secret-file FILE 代替 --client-key-file。使用该配置的每个命令都以该客户端身份运行,无需登录步骤:每次都会读取私钥或密钥文件(因此轮换后的文件无需重新登录),将令牌保存在内存中,且不会向配置写入其他内容。screenrig logout 会从配置中移除该客户端。每个命令请求自己的令牌,因此每小时运行数百条命令的作业更适合通过 API 重用一个令牌。

轮换并撤销

至控制台(访问,客户端)CLI
添加第二个密钥添加密钥screenrig service-client add-key scl_ID --key-file new.pem
移除密钥移除密钥screenrig service-client remove-key scl_ID --kid KID
添加第二个秘密生成密钥screenrig service-client add-secret scl_ID --secret-file new-secret
移除密钥秘密移除密钥screenrig service-client remove-secret scl_ID --secret-id css_ID
更改访问权限或功能访问,然后 保存访问使用控制台
结束客户端撤销screenrig service-client revoke scl_ID --yes

要无停机旋转,先添加新密钥或密钥,部署后再移除旧密钥。使用已移除密钥或密钥签发的令牌会在几秒内失效,客户端至少保留一个密钥或密钥。

缩小客户端的访问权限或功能会使其当前令牌失效;其下一个令牌将携带新的访问权限。撤销是永久性的:密钥和秘密被删除,客户端的令牌在几秒内停止工作,记录会保留在访问页面以供审计。删除项目会撤销其客户端。

客户端仅在其项目处于活动状态时获取令牌。已取消或已删除的项目不发放令牌。余额为零的项目仍发放令牌,其付费请求与智能体相同返回 402 payment_required。

错误和限制

响应原因修复
401 invalid_client错误或已移除的密钥,错误或重复的断言,未知 kid,吊销的客户端,或已取消或删除的项目。检查凭据与 service-client show;每次发送新的 jti
400 invalid_scope超出客户端访问权限或能力的范围请求减少,或更换客户端
400 invalid_target除列出的两者之外的 resource使用默认或远程服务器的
429 rate_limited每客户端每小时超过 600 个令牌,或每地址每小时超过 1200 个令牌请求重复使用每个令牌直到其过期
503 temporarily_unavailable请稍后再试指数退避重试
401 来自 /api令牌已过期或被撤销请求一次新令牌,然后停止
管理客户端时返回 403 forbidden缺少身份权限、具有 project 能力的当前项目成员资格,或负责人的组织管理员角色请受邀人员接受注册邀请;确认身份管理权限、智能体自身获授的项目管理权限,以及负责人的组织管理员角色
403 insufficient_access 来自 /api令牌的访问级别无法进行该更改,或项目要求代理更改需审核使用管理客户端;项目需审核时,播放列表和屏幕变更进入审核。
402 payment_required 来自 /api项目无付费请求额度。为项目添加额度