能力插件
每种传输和认证形式都是某个能力的简写:能力是发布四种服务之一的普通 Graft 插件。在写简写的位置填上你自己的能力即可。签名见 API 参考中的
Transport、Auth
和整个包。
connect: transport: { name: my-remote-connector, config: { address: example } } auth: { name: my-remote-credentials, config: { account: billing } }| 服务 | 所在侧 | 职责 | 导入自(只含定义) |
|---|---|---|---|
Connector |
宿主 | 打开一条通往 runner 的字节通道。 | @graftland/plugin-remote/Transport |
Credentials |
宿主 | 读取宿主要出示的内容:None、Authorization 或 Certificate,每次连接一次。 |
@graftland/plugin-remote/Auth |
Listener |
runner | 接受来自宿主的字节通道。 | @graftland/plugin-remote/Transport |
Authenticator |
runner | 把宿主出示的内容转换为稳定的身份,而不是会话 id。 | @graftland/plugin-remote/Auth |
在 runner 上,transport 和 auth 写在 listen
元素中。名字的解析方式与其他插件名称相同。Remote 为每个条目单独启动一份能力,因此不同条目可以用不同配置使用同一个能力,能力也仍然可以注入应用的服务。
| 规则 | 说明 |
|---|---|
| 导出 | 插件是模块的默认导出。 |
| 配置 | 用插件的 config schema 解码。路径相对于 loader 的 ConfigDirectory 解析。 |
| I/O | 只在 apply 中进行,绝不在导入时进行。 |
| 发布 | 激活后没有发布服务,会以 CapabilityContractError 失败。等待某个服务时停留在 Pending。 |
| Cause | 以 TransportError 或 CredentialsError 报告失败,原始失败放在 cause 中。 |
| 密钥 | 绝不把 Authorization 的值写进日志。 |
| 信任 | 能力代码及其对自身的声明,与其他插件一样被信任。 |
connector 和 listener 声明一组属性。Remote 照单全收,用它们拒绝组合并决定记录哪些警告。
| 属性 | 含义 | Remote 的要求 |
|---|---|---|
protocol |
简短的名字,如 wss 或 unix。 |
mTLS 要求 wss。stdio 必须是唯一的 listener。stdio 和 command 不接受显式 auth。 |
encrypted |
流量是否保密。 | mTLS 要求为 true。不加密的网络传输会记录警告。 |
network |
是否通过网络可达,而非仅限本地。 | 网络传输在不加密或无认证时记录警告。 |
carries |
握手携带的凭据:header、certificate。 |
bearer 和 JWT 要求 header,mTLS 要求 certificate。为空时只允许 none。 |
通道双向承载有序、可靠的字节。块的边界没有意义,通道结束也不能证明插件进程已经退出。
listener 如何运行
Section titled “listener 如何运行”- 绑定,使用 authenticator 给出的 TLS 策略;只有 mTLS 有这样的策略。TLS 设置(包括请求客户端证书)都来自这个策略。
- 服务,调用
serve(accept)。Remote 等待ready(表示 accept 循环已经在运行),同时监视serve是否失败。所有 listener 都就绪后 runner 才算启动;之后的失败只影响那一个 listener。 - 接受每条连接:在任何 HTTP 升级之前,用宿主出示的内容调用
accept。遇到AuthenticationFailed时,HTTP listener 返回 401,并把错误的challenge作为WWW-Authenticate。 - 运行返回的会话,交给它这条通道。身份由 Remote 附加。
- 每个会话的 fiber 和资源都要归属于某个 scope。
stdio式的 listener 在输入结束时结束graft start;网络 listener 会一直接受连接,直到被中断。AuthenticationFailed.reason是"missing"或"invalid"。内置的 bearer 和 JWT 使用的 challenge 分别是Bearer和Bearer error="invalid_token"。- 只有基于签发方的 authenticator 才声明
issuer;runner 会拒绝签发方不同的多个 listener。 - WebSocket 适配器要拒绝文本消息、执行帧大小限制、每条二进制消息只接受一个完整的协议帧,并重新拼接分块输出。
一个用于可信本地通道的 authenticator,只发放一个身份,和 none
一样什么都不检查:
import { Deferred, Effect, Schema } from "effect"
import { Graft, Plugin } from "@graftland/graft"import { Authenticator, type AuthenticatorApi,} from "@graftland/plugin-remote/Auth"
const fixedLocalIdentity = Plugin.make({ name: "app/fixed-local-identity", config: Schema.Struct({ identity: Schema.NonEmptyString }), apply: ({ identity }) => Graft.provide(Authenticator, { _tag: "None", identity }),})export default fixedLocalIdentity
const published = await Effect.runPromise( Effect.gen(function* () { const found = yield* Deferred.make<AuthenticatorApi>() yield* Graft.plugin(fixedLocalIdentity, { identity: "local" }) yield* Graft.plugin( Plugin.make({ name: "app/reader", inject: [Authenticator], apply: () => Effect.gen(function* () { const authenticator = yield* Authenticator yield* Deferred.succeed(found, authenticator) }), }), ) return yield* Deferred.await(found) }).pipe(Effect.scoped, Effect.provide(Graft.layer)),)published // => { _tag: "None", identity: "local" }@graftland/plugin-remote/Testing 包含内置能力通过的检查,不依赖任何测试框架。
import type { Effect, Scope } from "effect"import { layer } from "@effect/vitest"
import { Graft } from "@graftland/graft"import * as Testing from "@graftland/plugin-remote/Testing"import type { Channel } from "@graftland/plugin-remote/Transport"
declare const plugins: Testing.Capabilitiesdeclare const openEchoSession: ( plugins: Testing.Capabilities,) => Effect.Effect<Channel, never, Graft.Graft | Scope.Scope>
const checks = Testing.transport(plugins, openEchoSession)
layer(Graft.layer, { excludeTestServices: true })((it) => { it.effect("preserves bytes", () => checks.chunks)})| 检查 | 验证内容 |
|---|---|
Testing.transport(plugins, fixture) |
接收 connector、listener、credentials 和 authenticator;fixture 在 Graft 中连同一个使用方真实启动它们,并返回一条连到回显会话的通道。 |
chunks |
帧头被拆开、多个帧被合并时,字节仍按顺序到达。 |
eof |
关闭通道能干净地结束它。 |
Testing.rejection(httpResponse, challenge?) |
被拒绝的凭证得到 401、没有升级,并带有预期的 challenge。 |
失败时报
ConformanceError。另外还要测试错误的凭证、cause 是否保留、scope 取消、在
bind 时应用 TLS、serve 在 ready
之前失败,以及两份配置不同的副本同时运行,可参考内置的测试 fixture。
| 角色 | @graftland/plugin-remote/ 下的模块 |
|---|---|
| 宿主执行 | host/local、host/worker |
| 宿主 connector | host/websocket、host/unix、host/command |
| 宿主凭证 | host/auth/mtls、host/auth/bearer、host/auth/jwt、host/auth/none |
| runner listener | runner/websocket、runner/unix、runner/stdio |
| runner authenticator | runner/auth/mtls、runner/auth/bearer、runner/auth/jwt、runner/auth/none |
/internal/child、/internal/worker 和 /internal/watchdog
供 Remote 启动自己的运行时,不是公开 API。