@roll-agent/relay-protocol 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,8 @@ Browser-safe 的 Roll Companion Relay Wire 契约。
9
9
 
10
10
  | 使用方 | 是否直接安装 | 用途 |
11
11
  |---|---:|---|
12
- | Browser Web App | 是 | 构造、校验并投影 Relay frame |
12
+ | 普通 Browser Web App | 否 | 安装 `@roll-agent/relay-client`,不直接处理 frame |
13
+ | 自定义 Browser Transport | 是 | 实现 Relay frame、ACK/gap 与恢复 |
13
14
  | Cloud Relay Server | 是 | 校验、路由和持久化 Relay frame |
14
15
  | Local Companion Host | 是 | 与 Cloud 使用同一 Wire 契约 |
15
16
  | Local-only Desktop GUI | 否 | 直接使用 Runtime Protocol,不经过 Relay |
@@ -19,6 +20,39 @@ Browser-safe 的 Roll Companion Relay Wire 契约。
19
20
  pnpm add @roll-agent/relay-protocol
20
21
  ```
21
22
 
23
+ 普通第三方 Web App 应安装 `@roll-agent/relay-client`;本包面向 Cloud Relay、Companion 和确实
24
+ 需要自定义 Transport 的高级实现者。
25
+
26
+ ## Control 1.0 与方向合同
27
+
28
+ Browser Session 的 HTTP 返回和 WebSocket 控制面从独立 subpath 导入,不属于
29
+ `relayMessageSchemaV11` 数据面 union:
30
+
31
+ ```ts
32
+ import {
33
+ RELAY_BROWSER_WSS_MESSAGE_TYPES_BY_DIRECTION,
34
+ RELAY_COMPANION_WSS_MESSAGE_TYPES_BY_DIRECTION,
35
+ parseRelayBrowserFirstControlFrame,
36
+ relayBrowserControlMessageSchema,
37
+ relaySessionDescriptorSchema,
38
+ } from "@roll-agent/relay-protocol/control";
39
+
40
+ const session = relaySessionDescriptorSchema.parse(await response.json());
41
+ const ready = parseRelayBrowserFirstControlFrame(firstWebSocketFrame);
42
+ relayBrowserControlMessageSchema.parse(laterControlFrame);
43
+ ```
44
+
45
+ `session.ready` 必须是 Browser WebSocket 收到的第一帧,并固定声明 Control 1.0、Wire 1.1、
46
+ Relay 绑定的 Workspace 和在线状态。Browser 不能在请求中自行指定 `workspaceId`。
47
+ Ticket 无效、过期、重复消费或 Origin 不匹配时,Relay 必须在 HTTP upgrade 阶段拒绝连接;
48
+ 这些错误属于 `RELAY_BROWSER_HANDSHAKE_ERROR_CODES`,不能伪装成已认证连接中的
49
+ `session.error`。后者只承载 `session.ready` 之后的会话错误。
50
+
51
+ 方向 allowlist 固定为:Browser→Relay 只允许 Wire 1.1 `runtime.request` 与 `runtime.ack`;
52
+ Relay→Companion 同样只允许这两类。Relay→Browser 允许 Control 帧以及 response/event/gap/
53
+ Interaction 帧;Companion→Relay 还包含首帧 `device.connect`。`runtime.ack` 是 Wire 数据面,
54
+ 不是 Control ACK。P0 不提供 E2EE,因此这些 P0 WSS allowlist 明确不含 `runtime.encrypted`。
55
+
22
56
  ## 版本
23
57
 
24
58
  支持矩阵是 `["1.1", "1.0"]`。Wire 1.1 新增 typed Interaction;Wire 1.0 的 registry、
@@ -50,8 +84,10 @@ Wire 1.1 增加 `interaction.request`、`interaction.resolved`、
50
84
  `interaction.cancelled` 三种 Companion→Browser frame。Browser 通过 mutation
51
85
  `interaction.candidate` 提交候选;`approval.candidate` 只存在于冻结的 Wire 1.0。
52
86
 
53
- 远端候选不是 Runtime 的权威决议。Companion 仍持有 Runtime Handler、绝对 deadline 和
54
- 本地 Policy;Approval approve candidate 必须在本机重新经过 Policy。
87
+ 远端候选不是独立授权事实源。Runtime policy 先决定 auto/confirm/deny;只有 confirm 才产生
88
+ 可响应 Interaction,deny 不会被远端越过。官方 Companion 只再验证 request policy、Workspace、
89
+ responder 与绝对 deadline,不增加电脑端二次审批。低层自定义 Host 可以进一步收紧,但不能
90
+ 放宽 Runtime 决议。
55
91
 
56
92
  安全投影只允许:
57
93
 
@@ -64,6 +100,10 @@ Runtime JSON-RPC `id`、原始 Tool input/output、secret、完整 User Input Re
64
100
  以及 Authentication/File Picker projector 都不能进入 Companion→Browser 投影。Browser→Companion 的
65
101
  `interaction.candidate` 是 User Input Result 唯一允许的入站位置,并且必须关联原始表单重新校验。
66
102
 
103
+ Wire 1.1 Snapshot 不能重建一个仍可响应的 `interactionId`。Browser Session 重连时,Cloud
104
+ Relay 必须保留并重新投递尚未 resolved/cancelled 的 `interaction.request`;客户端不得从
105
+ `approvalId` 或 Snapshot 自行合成 Interaction identity。
106
+
67
107
  Wire 1.1 的 pull 查询遵守同一边界:`thread.open` / `thread.snapshot` 必须把 Approval preview
68
108
  投影为可选 explanation、移除本地 reason,并把 Operation display 与 outcome reason 脱敏;
69
109
  Host 对前两种结果调用 `projectRelayThreadSnapshotV11(value)`,对 `operation.get` 调用
@@ -104,6 +144,10 @@ context 必须由宿主注入。
104
144
 
105
145
  - `@roll-agent/relay-protocol/schema` 与 `/schema/v1.0`:冻结的 Wire 1.0 JSON Schema;
106
146
  - `@roll-agent/relay-protocol/schema/v1.1`:Wire 1.1 JSON Schema;
147
+ - `@roll-agent/relay-protocol/control`:Control 1.0、Browser Session 和 WSS 方向 Schema;
148
+ - `@roll-agent/relay-protocol/control/schema`:Control 1.0 JSON Schema;
149
+ - `@roll-agent/relay-protocol/control/session-schema`:Browser Session JSON Schema;
150
+ - `@roll-agent/relay-protocol/control/fixtures/*`:Control/Session golden fixtures;
107
151
  - `@roll-agent/relay-protocol/fixtures/v1/*`:字节冻结的 Wire 1.0 golden fixtures;
108
152
  - `@roll-agent/relay-protocol/fixtures/v1.1/*`:Wire 1.1 Interaction 与安全负例;
109
153
  - `@roll-agent/relay-protocol/conformance`:可复用的 N/N-1 suite。
@@ -0,0 +1,188 @@
1
+ import { z } from "zod/v4";
2
+ export declare const RELAY_CONTROL_VERSION: "1.0";
3
+ export declare const RELAY_BROWSER_PROTOCOL_VERSION: "1.1";
4
+ /**
5
+ * Authentication failures happen before the Browser WebSocket is accepted. They are HTTP upgrade
6
+ * errors and must never be sent as an authenticated Control frame.
7
+ */
8
+ export declare const RELAY_BROWSER_HANDSHAKE_ERROR_CODES: {
9
+ readonly sessionTicketInvalid: "SESSION_TICKET_INVALID";
10
+ readonly sessionTicketExpired: "SESSION_TICKET_EXPIRED";
11
+ readonly sessionTicketAlreadyUsed: "SESSION_TICKET_ALREADY_USED";
12
+ readonly originNotAllowed: "ORIGIN_NOT_ALLOWED";
13
+ };
14
+ export declare const RELAY_BROWSER_HANDSHAKE_ERROR_CODE_VALUES: readonly ["SESSION_TICKET_INVALID", "SESSION_TICKET_EXPIRED", "SESSION_TICKET_ALREADY_USED", "ORIGIN_NOT_ALLOWED"];
15
+ export declare const relayBrowserHandshakeErrorCodeSchema: z.ZodEnum<{
16
+ SESSION_TICKET_INVALID: "SESSION_TICKET_INVALID";
17
+ SESSION_TICKET_EXPIRED: "SESSION_TICKET_EXPIRED";
18
+ SESSION_TICKET_ALREADY_USED: "SESSION_TICKET_ALREADY_USED";
19
+ ORIGIN_NOT_ALLOWED: "ORIGIN_NOT_ALLOWED";
20
+ }>;
21
+ /** Errors observable only after the authenticated `session.ready` first frame. */
22
+ export declare const RELAY_CLIENT_ERROR_CODES: {
23
+ readonly workspaceNotFound: "WORKSPACE_NOT_FOUND";
24
+ readonly workspaceOffline: "WORKSPACE_OFFLINE";
25
+ readonly controllerConflict: "CONTROLLER_CONFLICT";
26
+ readonly protocolVersionUnsupported: "RELAY_PROTOCOL_VERSION_UNSUPPORTED";
27
+ readonly rateLimited: "RATE_LIMITED";
28
+ readonly internalError: "RELAY_INTERNAL_ERROR";
29
+ };
30
+ export declare const RELAY_CLIENT_ERROR_CODE_VALUES: readonly ["WORKSPACE_NOT_FOUND", "WORKSPACE_OFFLINE", "CONTROLLER_CONFLICT", "RELAY_PROTOCOL_VERSION_UNSUPPORTED", "RATE_LIMITED", "RELAY_INTERNAL_ERROR"];
31
+ export declare const relayClientErrorCodeSchema: z.ZodEnum<{
32
+ RELAY_PROTOCOL_VERSION_UNSUPPORTED: "RELAY_PROTOCOL_VERSION_UNSUPPORTED";
33
+ WORKSPACE_NOT_FOUND: "WORKSPACE_NOT_FOUND";
34
+ WORKSPACE_OFFLINE: "WORKSPACE_OFFLINE";
35
+ CONTROLLER_CONFLICT: "CONTROLLER_CONFLICT";
36
+ RATE_LIMITED: "RATE_LIMITED";
37
+ RELAY_INTERNAL_ERROR: "RELAY_INTERNAL_ERROR";
38
+ }>;
39
+ export declare const relayBrowserSessionIdSchema: z.core.$ZodBranded<z.ZodString, "RelayBrowserSessionId">;
40
+ export declare const relaySessionDescriptorSchema: z.ZodReadonly<z.ZodObject<{
41
+ connectUrl: z.ZodString;
42
+ expiresAt: z.ZodString;
43
+ }, z.core.$strict>>;
44
+ export declare const RELAY_WORKSPACE_STATUSES: readonly ["online", "offline"];
45
+ export declare const relayWorkspaceStatusSchema: z.ZodEnum<{
46
+ online: "online";
47
+ offline: "offline";
48
+ }>;
49
+ export declare const RELAY_BROWSER_CONTROL_MESSAGE_TYPES: {
50
+ readonly sessionReady: "session.ready";
51
+ readonly workspaceStatus: "workspace.status";
52
+ readonly sessionError: "session.error";
53
+ };
54
+ export declare const RELAY_BROWSER_CONTROL_MESSAGE_TYPE_VALUES: readonly ["session.ready", "workspace.status", "session.error"];
55
+ export declare const relaySessionReadyControlMessageSchema: z.ZodReadonly<z.ZodObject<{
56
+ type: z.ZodLiteral<"session.ready">;
57
+ controlVersion: z.ZodLiteral<"1.0">;
58
+ relayProtocolVersion: z.ZodLiteral<"1.1">;
59
+ sessionId: z.core.$ZodBranded<z.ZodString, "RelayBrowserSessionId">;
60
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
61
+ workspaceStatus: z.ZodEnum<{
62
+ online: "online";
63
+ offline: "offline";
64
+ }>;
65
+ }, z.core.$strict>>;
66
+ export declare const relayWorkspaceStatusControlMessageSchema: z.ZodReadonly<z.ZodObject<{
67
+ type: z.ZodLiteral<"workspace.status">;
68
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
69
+ status: z.ZodEnum<{
70
+ online: "online";
71
+ offline: "offline";
72
+ }>;
73
+ }, z.core.$strict>>;
74
+ export declare const relaySessionErrorControlMessageSchema: z.ZodReadonly<z.ZodObject<{
75
+ type: z.ZodLiteral<"session.error">;
76
+ code: z.ZodEnum<{
77
+ RELAY_PROTOCOL_VERSION_UNSUPPORTED: "RELAY_PROTOCOL_VERSION_UNSUPPORTED";
78
+ WORKSPACE_NOT_FOUND: "WORKSPACE_NOT_FOUND";
79
+ WORKSPACE_OFFLINE: "WORKSPACE_OFFLINE";
80
+ CONTROLLER_CONFLICT: "CONTROLLER_CONFLICT";
81
+ RATE_LIMITED: "RATE_LIMITED";
82
+ RELAY_INTERNAL_ERROR: "RELAY_INTERNAL_ERROR";
83
+ }>;
84
+ retryable: z.ZodBoolean;
85
+ }, z.core.$strict>>;
86
+ export declare const relayBrowserControlMessageSchema: z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
87
+ type: z.ZodLiteral<"session.ready">;
88
+ controlVersion: z.ZodLiteral<"1.0">;
89
+ relayProtocolVersion: z.ZodLiteral<"1.1">;
90
+ sessionId: z.core.$ZodBranded<z.ZodString, "RelayBrowserSessionId">;
91
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
92
+ workspaceStatus: z.ZodEnum<{
93
+ online: "online";
94
+ offline: "offline";
95
+ }>;
96
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
97
+ type: z.ZodLiteral<"workspace.status">;
98
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
99
+ status: z.ZodEnum<{
100
+ online: "online";
101
+ offline: "offline";
102
+ }>;
103
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
104
+ type: z.ZodLiteral<"session.error">;
105
+ code: z.ZodEnum<{
106
+ RELAY_PROTOCOL_VERSION_UNSUPPORTED: "RELAY_PROTOCOL_VERSION_UNSUPPORTED";
107
+ WORKSPACE_NOT_FOUND: "WORKSPACE_NOT_FOUND";
108
+ WORKSPACE_OFFLINE: "WORKSPACE_OFFLINE";
109
+ CONTROLLER_CONFLICT: "CONTROLLER_CONFLICT";
110
+ RATE_LIMITED: "RATE_LIMITED";
111
+ RELAY_INTERNAL_ERROR: "RELAY_INTERNAL_ERROR";
112
+ }>;
113
+ retryable: z.ZodBoolean;
114
+ }, z.core.$strict>>]>;
115
+ /** Browser WebSocket connections must validate their first received frame with this schema. */
116
+ export declare const relayBrowserFirstControlFrameSchema: z.ZodReadonly<z.ZodObject<{
117
+ type: z.ZodLiteral<"session.ready">;
118
+ controlVersion: z.ZodLiteral<"1.0">;
119
+ relayProtocolVersion: z.ZodLiteral<"1.1">;
120
+ sessionId: z.core.$ZodBranded<z.ZodString, "RelayBrowserSessionId">;
121
+ workspaceId: z.core.$ZodBranded<z.ZodString, "WorkspaceId">;
122
+ workspaceStatus: z.ZodEnum<{
123
+ online: "online";
124
+ offline: "offline";
125
+ }>;
126
+ }, z.core.$strict>>;
127
+ export declare function parseRelayBrowserFirstControlFrame(value: unknown): RelaySessionReadyControlMessage;
128
+ export declare const RELAY_CONTROL_DIRECTIONS: {
129
+ readonly relayToBrowser: "relay-to-browser";
130
+ readonly browserToRelay: "browser-to-relay";
131
+ readonly relayToCompanion: "relay-to-companion";
132
+ readonly companionToRelay: "companion-to-relay";
133
+ };
134
+ export declare const RELAY_CONTROL_DIRECTION_VALUES: readonly ["relay-to-browser", "browser-to-relay", "relay-to-companion", "companion-to-relay"];
135
+ export declare const relayControlDirectionSchema: z.ZodEnum<{
136
+ "relay-to-browser": "relay-to-browser";
137
+ "browser-to-relay": "browser-to-relay";
138
+ "relay-to-companion": "relay-to-companion";
139
+ "companion-to-relay": "companion-to-relay";
140
+ }>;
141
+ export type RelayControlDirection = z.infer<typeof relayControlDirectionSchema>;
142
+ export type RelayBrowserControlMessageType = (typeof RELAY_BROWSER_CONTROL_MESSAGE_TYPE_VALUES)[number];
143
+ /** Control 1.0 is Relay-to-Browser only. Data-plane frames remain governed by Relay Wire 1.1. */
144
+ export declare const RELAY_CONTROL_MESSAGE_TYPES_BY_DIRECTION: {
145
+ readonly "relay-to-browser": readonly ["session.ready", "workspace.status", "session.error"];
146
+ readonly "browser-to-relay": readonly [];
147
+ readonly "relay-to-companion": readonly [];
148
+ readonly "companion-to-relay": readonly [];
149
+ };
150
+ export declare function isRelayControlMessageTypeAllowed(direction: RelayControlDirection, messageType: string): messageType is RelayBrowserControlMessageType;
151
+ export declare const RELAY_WSS_DIRECTIONS: {
152
+ readonly relayToPeer: "relay-to-peer";
153
+ readonly peerToRelay: "peer-to-relay";
154
+ };
155
+ export declare const RELAY_WSS_DIRECTION_VALUES: readonly ["relay-to-peer", "peer-to-relay"];
156
+ export declare const relayWssDirectionSchema: z.ZodEnum<{
157
+ "relay-to-peer": "relay-to-peer";
158
+ "peer-to-relay": "peer-to-relay";
159
+ }>;
160
+ export type RelayWssDirection = z.infer<typeof relayWssDirectionSchema>;
161
+ /**
162
+ * P0 Browser WebSocket direction contract. `runtime.ack` is a Wire 1.1 data-plane frame,
163
+ * not a Control frame. `runtime.encrypted` is intentionally absent because P0 does not offer E2EE.
164
+ */
165
+ export declare const RELAY_BROWSER_WSS_MESSAGE_TYPES_BY_DIRECTION: {
166
+ readonly "relay-to-peer": readonly ["session.ready", "workspace.status", "session.error", "runtime.response", "runtime.event", "runtime.gap", "interaction.request", "interaction.resolved", "interaction.cancelled"];
167
+ readonly "peer-to-relay": readonly ["runtime.request", "runtime.ack"];
168
+ };
169
+ /**
170
+ * P0 Companion WebSocket direction contract. A Cloud Relay must additionally require
171
+ * `device.connect` as the Companion connection's first frame.
172
+ */
173
+ export declare const RELAY_COMPANION_WSS_MESSAGE_TYPES_BY_DIRECTION: {
174
+ readonly "relay-to-peer": readonly ["runtime.request", "runtime.ack"];
175
+ readonly "peer-to-relay": readonly ["device.connect", "runtime.response", "runtime.event", "runtime.gap", "interaction.request", "interaction.resolved", "interaction.cancelled"];
176
+ };
177
+ export declare function isRelayBrowserWssMessageTypeAllowed(direction: RelayWssDirection, messageType: string): boolean;
178
+ export declare function isRelayCompanionWssMessageTypeAllowed(direction: RelayWssDirection, messageType: string): boolean;
179
+ export type RelayClientErrorCode = z.infer<typeof relayClientErrorCodeSchema>;
180
+ export type RelayBrowserHandshakeErrorCode = z.infer<typeof relayBrowserHandshakeErrorCodeSchema>;
181
+ export type RelayBrowserSessionId = z.infer<typeof relayBrowserSessionIdSchema>;
182
+ export type RelaySessionDescriptor = z.infer<typeof relaySessionDescriptorSchema>;
183
+ export type RelayWorkspaceStatus = z.infer<typeof relayWorkspaceStatusSchema>;
184
+ export type RelaySessionReadyControlMessage = z.infer<typeof relaySessionReadyControlMessageSchema>;
185
+ export type RelayWorkspaceStatusControlMessage = z.infer<typeof relayWorkspaceStatusControlMessageSchema>;
186
+ export type RelaySessionErrorControlMessage = z.infer<typeof relaySessionErrorControlMessageSchema>;
187
+ export type RelayBrowserControlMessage = z.infer<typeof relayBrowserControlMessageSchema>;
188
+ export type RelayBrowserFirstControlFrame = z.infer<typeof relayBrowserFirstControlFrameSchema>;
@@ -0,0 +1,180 @@
1
+ import { z } from "zod/v4";
2
+ import { RELAY_MESSAGE_TYPES_V11, workspaceIdSchema } from "./index.js";
3
+ export const RELAY_CONTROL_VERSION = "1.0";
4
+ export const RELAY_BROWSER_PROTOCOL_VERSION = "1.1";
5
+ /**
6
+ * Authentication failures happen before the Browser WebSocket is accepted. They are HTTP upgrade
7
+ * errors and must never be sent as an authenticated Control frame.
8
+ */
9
+ export const RELAY_BROWSER_HANDSHAKE_ERROR_CODES = {
10
+ sessionTicketInvalid: "SESSION_TICKET_INVALID",
11
+ sessionTicketExpired: "SESSION_TICKET_EXPIRED",
12
+ sessionTicketAlreadyUsed: "SESSION_TICKET_ALREADY_USED",
13
+ originNotAllowed: "ORIGIN_NOT_ALLOWED",
14
+ };
15
+ export const RELAY_BROWSER_HANDSHAKE_ERROR_CODE_VALUES = [
16
+ RELAY_BROWSER_HANDSHAKE_ERROR_CODES.sessionTicketInvalid,
17
+ RELAY_BROWSER_HANDSHAKE_ERROR_CODES.sessionTicketExpired,
18
+ RELAY_BROWSER_HANDSHAKE_ERROR_CODES.sessionTicketAlreadyUsed,
19
+ RELAY_BROWSER_HANDSHAKE_ERROR_CODES.originNotAllowed,
20
+ ];
21
+ export const relayBrowserHandshakeErrorCodeSchema = z.enum(RELAY_BROWSER_HANDSHAKE_ERROR_CODE_VALUES);
22
+ /** Errors observable only after the authenticated `session.ready` first frame. */
23
+ export const RELAY_CLIENT_ERROR_CODES = {
24
+ workspaceNotFound: "WORKSPACE_NOT_FOUND",
25
+ workspaceOffline: "WORKSPACE_OFFLINE",
26
+ controllerConflict: "CONTROLLER_CONFLICT",
27
+ protocolVersionUnsupported: "RELAY_PROTOCOL_VERSION_UNSUPPORTED",
28
+ rateLimited: "RATE_LIMITED",
29
+ internalError: "RELAY_INTERNAL_ERROR",
30
+ };
31
+ export const RELAY_CLIENT_ERROR_CODE_VALUES = [
32
+ RELAY_CLIENT_ERROR_CODES.workspaceNotFound,
33
+ RELAY_CLIENT_ERROR_CODES.workspaceOffline,
34
+ RELAY_CLIENT_ERROR_CODES.controllerConflict,
35
+ RELAY_CLIENT_ERROR_CODES.protocolVersionUnsupported,
36
+ RELAY_CLIENT_ERROR_CODES.rateLimited,
37
+ RELAY_CLIENT_ERROR_CODES.internalError,
38
+ ];
39
+ export const relayClientErrorCodeSchema = z.enum(RELAY_CLIENT_ERROR_CODE_VALUES);
40
+ export const relayBrowserSessionIdSchema = z.string().min(1).brand();
41
+ const secureWebSocketUrlSchema = z
42
+ .string()
43
+ .regex(/^wss:\/\/\S+$/u, "connectUrl must use wss:// without whitespace")
44
+ .url();
45
+ export const relaySessionDescriptorSchema = z
46
+ .object({
47
+ connectUrl: secureWebSocketUrlSchema,
48
+ expiresAt: z.string().datetime({ offset: true }),
49
+ })
50
+ .strict()
51
+ .readonly();
52
+ export const RELAY_WORKSPACE_STATUSES = ["online", "offline"];
53
+ export const relayWorkspaceStatusSchema = z.enum(RELAY_WORKSPACE_STATUSES);
54
+ export const RELAY_BROWSER_CONTROL_MESSAGE_TYPES = {
55
+ sessionReady: "session.ready",
56
+ workspaceStatus: "workspace.status",
57
+ sessionError: "session.error",
58
+ };
59
+ export const RELAY_BROWSER_CONTROL_MESSAGE_TYPE_VALUES = [
60
+ RELAY_BROWSER_CONTROL_MESSAGE_TYPES.sessionReady,
61
+ RELAY_BROWSER_CONTROL_MESSAGE_TYPES.workspaceStatus,
62
+ RELAY_BROWSER_CONTROL_MESSAGE_TYPES.sessionError,
63
+ ];
64
+ export const relaySessionReadyControlMessageSchema = z
65
+ .object({
66
+ type: z.literal(RELAY_BROWSER_CONTROL_MESSAGE_TYPES.sessionReady),
67
+ controlVersion: z.literal(RELAY_CONTROL_VERSION),
68
+ relayProtocolVersion: z.literal(RELAY_BROWSER_PROTOCOL_VERSION),
69
+ sessionId: relayBrowserSessionIdSchema,
70
+ workspaceId: workspaceIdSchema,
71
+ workspaceStatus: relayWorkspaceStatusSchema,
72
+ })
73
+ .strict()
74
+ .readonly();
75
+ export const relayWorkspaceStatusControlMessageSchema = z
76
+ .object({
77
+ type: z.literal(RELAY_BROWSER_CONTROL_MESSAGE_TYPES.workspaceStatus),
78
+ workspaceId: workspaceIdSchema,
79
+ status: relayWorkspaceStatusSchema,
80
+ })
81
+ .strict()
82
+ .readonly();
83
+ export const relaySessionErrorControlMessageSchema = z
84
+ .object({
85
+ type: z.literal(RELAY_BROWSER_CONTROL_MESSAGE_TYPES.sessionError),
86
+ code: relayClientErrorCodeSchema,
87
+ retryable: z.boolean(),
88
+ })
89
+ .strict()
90
+ .readonly();
91
+ export const relayBrowserControlMessageSchema = z.discriminatedUnion("type", [
92
+ relaySessionReadyControlMessageSchema,
93
+ relayWorkspaceStatusControlMessageSchema,
94
+ relaySessionErrorControlMessageSchema,
95
+ ]);
96
+ /** Browser WebSocket connections must validate their first received frame with this schema. */
97
+ export const relayBrowserFirstControlFrameSchema = relaySessionReadyControlMessageSchema;
98
+ export function parseRelayBrowserFirstControlFrame(value) {
99
+ return relayBrowserFirstControlFrameSchema.parse(value);
100
+ }
101
+ export const RELAY_CONTROL_DIRECTIONS = {
102
+ relayToBrowser: "relay-to-browser",
103
+ browserToRelay: "browser-to-relay",
104
+ relayToCompanion: "relay-to-companion",
105
+ companionToRelay: "companion-to-relay",
106
+ };
107
+ export const RELAY_CONTROL_DIRECTION_VALUES = [
108
+ RELAY_CONTROL_DIRECTIONS.relayToBrowser,
109
+ RELAY_CONTROL_DIRECTIONS.browserToRelay,
110
+ RELAY_CONTROL_DIRECTIONS.relayToCompanion,
111
+ RELAY_CONTROL_DIRECTIONS.companionToRelay,
112
+ ];
113
+ export const relayControlDirectionSchema = z.enum(RELAY_CONTROL_DIRECTION_VALUES);
114
+ /** Control 1.0 is Relay-to-Browser only. Data-plane frames remain governed by Relay Wire 1.1. */
115
+ export const RELAY_CONTROL_MESSAGE_TYPES_BY_DIRECTION = {
116
+ [RELAY_CONTROL_DIRECTIONS.relayToBrowser]: RELAY_BROWSER_CONTROL_MESSAGE_TYPE_VALUES,
117
+ [RELAY_CONTROL_DIRECTIONS.browserToRelay]: [],
118
+ [RELAY_CONTROL_DIRECTIONS.relayToCompanion]: [],
119
+ [RELAY_CONTROL_DIRECTIONS.companionToRelay]: [],
120
+ };
121
+ export function isRelayControlMessageTypeAllowed(direction, messageType) {
122
+ const allowed = RELAY_CONTROL_MESSAGE_TYPES_BY_DIRECTION[direction];
123
+ return allowed.includes(messageType);
124
+ }
125
+ export const RELAY_WSS_DIRECTIONS = {
126
+ relayToPeer: "relay-to-peer",
127
+ peerToRelay: "peer-to-relay",
128
+ };
129
+ export const RELAY_WSS_DIRECTION_VALUES = [
130
+ RELAY_WSS_DIRECTIONS.relayToPeer,
131
+ RELAY_WSS_DIRECTIONS.peerToRelay,
132
+ ];
133
+ export const relayWssDirectionSchema = z.enum(RELAY_WSS_DIRECTION_VALUES);
134
+ /**
135
+ * P0 Browser WebSocket direction contract. `runtime.ack` is a Wire 1.1 data-plane frame,
136
+ * not a Control frame. `runtime.encrypted` is intentionally absent because P0 does not offer E2EE.
137
+ */
138
+ export const RELAY_BROWSER_WSS_MESSAGE_TYPES_BY_DIRECTION = {
139
+ [RELAY_WSS_DIRECTIONS.relayToPeer]: [
140
+ ...RELAY_BROWSER_CONTROL_MESSAGE_TYPE_VALUES,
141
+ RELAY_MESSAGE_TYPES_V11.runtimeResponse,
142
+ RELAY_MESSAGE_TYPES_V11.runtimeEvent,
143
+ RELAY_MESSAGE_TYPES_V11.runtimeGap,
144
+ RELAY_MESSAGE_TYPES_V11.interactionRequest,
145
+ RELAY_MESSAGE_TYPES_V11.interactionResolved,
146
+ RELAY_MESSAGE_TYPES_V11.interactionCancelled,
147
+ ],
148
+ [RELAY_WSS_DIRECTIONS.peerToRelay]: [
149
+ RELAY_MESSAGE_TYPES_V11.runtimeRequest,
150
+ RELAY_MESSAGE_TYPES_V11.runtimeAck,
151
+ ],
152
+ };
153
+ /**
154
+ * P0 Companion WebSocket direction contract. A Cloud Relay must additionally require
155
+ * `device.connect` as the Companion connection's first frame.
156
+ */
157
+ export const RELAY_COMPANION_WSS_MESSAGE_TYPES_BY_DIRECTION = {
158
+ [RELAY_WSS_DIRECTIONS.relayToPeer]: [
159
+ RELAY_MESSAGE_TYPES_V11.runtimeRequest,
160
+ RELAY_MESSAGE_TYPES_V11.runtimeAck,
161
+ ],
162
+ [RELAY_WSS_DIRECTIONS.peerToRelay]: [
163
+ RELAY_MESSAGE_TYPES_V11.deviceConnect,
164
+ RELAY_MESSAGE_TYPES_V11.runtimeResponse,
165
+ RELAY_MESSAGE_TYPES_V11.runtimeEvent,
166
+ RELAY_MESSAGE_TYPES_V11.runtimeGap,
167
+ RELAY_MESSAGE_TYPES_V11.interactionRequest,
168
+ RELAY_MESSAGE_TYPES_V11.interactionResolved,
169
+ RELAY_MESSAGE_TYPES_V11.interactionCancelled,
170
+ ],
171
+ };
172
+ function includesMessageType(allowedByDirection, direction, messageType) {
173
+ return allowedByDirection[direction].includes(messageType);
174
+ }
175
+ export function isRelayBrowserWssMessageTypeAllowed(direction, messageType) {
176
+ return includesMessageType(RELAY_BROWSER_WSS_MESSAGE_TYPES_BY_DIRECTION, direction, messageType);
177
+ }
178
+ export function isRelayCompanionWssMessageTypeAllowed(direction, messageType) {
179
+ return includesMessageType(RELAY_COMPANION_WSS_MESSAGE_TYPES_BY_DIRECTION, direction, messageType);
180
+ }
package/dist/index.d.ts CHANGED
@@ -182,6 +182,35 @@ export declare const RELAY_ERROR_RETRYABILITY: {
182
182
  readonly COMPANION_ERROR: false;
183
183
  };
184
184
  export declare function getRelayErrorRetryability(code: RelayErrorCode): boolean;
185
+ /** Wire 1.1 adds Host-level request authorization without mutating frozen Wire 1.0 exports. */
186
+ export declare const RELAY_ERROR_CODES_V11: {
187
+ readonly remoteRequestDenied: "REMOTE_REQUEST_DENIED";
188
+ readonly protocolVersionUnsupported: "RELAY_PROTOCOL_VERSION_UNSUPPORTED";
189
+ readonly invalidFrame: "RELAY_INVALID_FRAME";
190
+ readonly invalidParams: "INVALID_PARAMS";
191
+ readonly requestIdConflict: "RELAY_REQUEST_ID_CONFLICT";
192
+ readonly workspaceNotFound: "WORKSPACE_NOT_FOUND";
193
+ readonly encryptionRequired: "RELAY_ENCRYPTION_REQUIRED";
194
+ readonly encryptedPayloadInvalid: "ENCRYPTED_PAYLOAD_INVALID";
195
+ readonly localApprovalDenied: "LocalApprovalDeniedError";
196
+ readonly localConfirmationRequired: "LocalConfirmationRequiredError";
197
+ readonly companionError: "COMPANION_ERROR";
198
+ };
199
+ export type RelayErrorCodeV11 = (typeof RELAY_ERROR_CODES_V11)[keyof typeof RELAY_ERROR_CODES_V11];
200
+ export declare const RELAY_ERROR_RETRYABILITY_V11: {
201
+ readonly REMOTE_REQUEST_DENIED: false;
202
+ readonly RELAY_PROTOCOL_VERSION_UNSUPPORTED: false;
203
+ readonly RELAY_INVALID_FRAME: false;
204
+ readonly INVALID_PARAMS: false;
205
+ readonly RELAY_REQUEST_ID_CONFLICT: false;
206
+ readonly WORKSPACE_NOT_FOUND: false;
207
+ readonly RELAY_ENCRYPTION_REQUIRED: false;
208
+ readonly ENCRYPTED_PAYLOAD_INVALID: false;
209
+ readonly LocalApprovalDeniedError: false;
210
+ readonly LocalConfirmationRequiredError: false;
211
+ readonly COMPANION_ERROR: false;
212
+ };
213
+ export declare function getRelayErrorRetryabilityV11(code: RelayErrorCodeV11): boolean;
185
214
  export declare const relayProtocolVersionSchema: z.ZodEnum<{
186
215
  1.1: "1.1";
187
216
  "1.0": "1.0";
@@ -4704,6 +4733,7 @@ export type RelayTimelineEventV11 = z.output<typeof relayTimelineEventSchemaV11>
4704
4733
  export type RelayRuntimeEventEnvelopeV11 = z.output<typeof relayRuntimeEventEnvelopeSchemaV11>;
4705
4734
  export type RelayMessageV10 = z.output<typeof relayMessageSchemaV10>;
4706
4735
  export type RelayMessageV11 = z.output<typeof relayMessageSchemaV11>;
4736
+ export type RelayRequestMethodV11 = RelayRequestMethodForVersion<"1.1">;
4707
4737
  export type RelayRuntimeRequestV10 = z.output<typeof relayRuntimeRequestSchemaV10>;
4708
4738
  export type RelayRuntimeRequestV11 = z.output<typeof relayRuntimeRequestSchemaV11>;
4709
4739
  export type RelayRuntimeEventV10 = z.output<typeof relayRuntimeEventSchemaV10>;
package/dist/index.js CHANGED
@@ -197,6 +197,18 @@ export const RELAY_ERROR_RETRYABILITY = {
197
197
  export function getRelayErrorRetryability(code) {
198
198
  return RELAY_ERROR_RETRYABILITY[code];
199
199
  }
200
+ /** Wire 1.1 adds Host-level request authorization without mutating frozen Wire 1.0 exports. */
201
+ export const RELAY_ERROR_CODES_V11 = {
202
+ ...RELAY_ERROR_CODES,
203
+ remoteRequestDenied: "REMOTE_REQUEST_DENIED",
204
+ };
205
+ export const RELAY_ERROR_RETRYABILITY_V11 = {
206
+ ...RELAY_ERROR_RETRYABILITY,
207
+ [RELAY_ERROR_CODES_V11.remoteRequestDenied]: false,
208
+ };
209
+ export function getRelayErrorRetryabilityV11(code) {
210
+ return RELAY_ERROR_RETRYABILITY_V11[code];
211
+ }
200
212
  export const relayProtocolVersionSchema = z.enum(SUPPORTED_RELAY_PROTOCOL_VERSIONS);
201
213
  export const deviceIdSchema = z.string().uuid().brand();
202
214
  export const workspaceIdSchema = z.string().uuid().brand();
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "readOnly": true,
4
+ "type": "object",
5
+ "properties": {
6
+ "connectUrl": {
7
+ "type": "string",
8
+ "format": "uri",
9
+ "pattern": "^wss:\\/\\/\\S+$"
10
+ },
11
+ "expiresAt": {
12
+ "type": "string",
13
+ "format": "date-time",
14
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$"
15
+ }
16
+ },
17
+ "required": ["connectUrl", "expiresAt"],
18
+ "additionalProperties": false,
19
+ "$id": "urn:roll-agent:schema:relay-browser-session:1.0",
20
+ "title": "Roll Relay Browser Session Descriptor v1.0",
21
+ "description": "Short-lived single-use Browser WebSocket connection descriptor.",
22
+ "x-roll-control-version": "1.0"
23
+ }
@@ -0,0 +1,127 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "anyOf": [
4
+ {
5
+ "readOnly": true,
6
+ "type": "object",
7
+ "properties": {
8
+ "type": {
9
+ "type": "string",
10
+ "const": "session.ready"
11
+ },
12
+ "controlVersion": {
13
+ "type": "string",
14
+ "const": "1.0"
15
+ },
16
+ "relayProtocolVersion": {
17
+ "type": "string",
18
+ "const": "1.1"
19
+ },
20
+ "sessionId": {
21
+ "type": "string",
22
+ "minLength": 1
23
+ },
24
+ "workspaceId": {
25
+ "type": "string",
26
+ "format": "uuid",
27
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"
28
+ },
29
+ "workspaceStatus": {
30
+ "type": "string",
31
+ "enum": ["online", "offline"]
32
+ }
33
+ },
34
+ "required": [
35
+ "type",
36
+ "controlVersion",
37
+ "relayProtocolVersion",
38
+ "sessionId",
39
+ "workspaceId",
40
+ "workspaceStatus"
41
+ ],
42
+ "additionalProperties": false
43
+ },
44
+ {
45
+ "readOnly": true,
46
+ "type": "object",
47
+ "properties": {
48
+ "type": {
49
+ "type": "string",
50
+ "const": "workspace.status"
51
+ },
52
+ "workspaceId": {
53
+ "type": "string",
54
+ "format": "uuid",
55
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"
56
+ },
57
+ "status": {
58
+ "type": "string",
59
+ "enum": ["online", "offline"]
60
+ }
61
+ },
62
+ "required": ["type", "workspaceId", "status"],
63
+ "additionalProperties": false
64
+ },
65
+ {
66
+ "readOnly": true,
67
+ "type": "object",
68
+ "properties": {
69
+ "type": {
70
+ "type": "string",
71
+ "const": "session.error"
72
+ },
73
+ "code": {
74
+ "type": "string",
75
+ "enum": [
76
+ "WORKSPACE_NOT_FOUND",
77
+ "WORKSPACE_OFFLINE",
78
+ "CONTROLLER_CONFLICT",
79
+ "RELAY_PROTOCOL_VERSION_UNSUPPORTED",
80
+ "RATE_LIMITED",
81
+ "RELAY_INTERNAL_ERROR"
82
+ ]
83
+ },
84
+ "retryable": {
85
+ "type": "boolean"
86
+ }
87
+ },
88
+ "required": ["type", "code", "retryable"],
89
+ "additionalProperties": false
90
+ }
91
+ ],
92
+ "$id": "urn:roll-agent:schema:relay-control:1.0",
93
+ "title": "Roll Relay Browser Control v1.0",
94
+ "description": "Relay-to-Browser session and workspace control messages.",
95
+ "x-roll-control-version": "1.0",
96
+ "x-roll-browser-first-message-type": "session.ready",
97
+ "x-roll-browser-message-types-by-direction": {
98
+ "relay-to-peer": [
99
+ "session.ready",
100
+ "workspace.status",
101
+ "session.error",
102
+ "runtime.response",
103
+ "runtime.event",
104
+ "runtime.gap",
105
+ "interaction.request",
106
+ "interaction.resolved",
107
+ "interaction.cancelled"
108
+ ],
109
+ "peer-to-relay": ["runtime.request", "runtime.ack"]
110
+ },
111
+ "x-roll-companion-message-types-by-direction": {
112
+ "relay-to-peer": ["runtime.request", "runtime.ack"],
113
+ "peer-to-relay": [
114
+ "device.connect",
115
+ "runtime.response",
116
+ "runtime.event",
117
+ "runtime.gap",
118
+ "interaction.request",
119
+ "interaction.resolved",
120
+ "interaction.cancelled"
121
+ ]
122
+ },
123
+ "x-roll-wss-directions": {
124
+ "relayToPeer": "relay-to-peer",
125
+ "peerToRelay": "peer-to-relay"
126
+ }
127
+ }
@@ -1108,7 +1108,7 @@
1108
1108
  }
1109
1109
  ],
1110
1110
  "$schema": "https://json-schema.org/draft/2020-12/schema",
1111
- "$id": "https://roll-agent.dev/schemas/relay-protocol/1.1",
1111
+ "$id": "urn:roll-agent:schema:relay-protocol:1.1",
1112
1112
  "title": "Roll Relay Protocol v1.1",
1113
1113
  "description": "Versioned Companion Relay frames shared by Browser, Cloud Relay and Companion.",
1114
1114
  "$defs": {
@@ -718,7 +718,7 @@
718
718
  }
719
719
  ],
720
720
  "$schema": "https://json-schema.org/draft/2020-12/schema",
721
- "$id": "https://roll-agent.dev/schemas/relay-protocol/1.0",
721
+ "$id": "urn:roll-agent:schema:relay-protocol:1.0",
722
722
  "title": "Roll Relay Protocol v1.0",
723
723
  "description": "Versioned Companion Relay frames shared by Browser, Cloud Relay and Companion.",
724
724
  "$defs": {
@@ -0,0 +1,4 @@
1
+ {
2
+ "connectUrl": "wss://relay.example.test/v1/browser?ticket=single-use ",
3
+ "expiresAt": "2026-08-06T12:00:00.000Z"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "connectUrl": "WSS://relay.example.test/v1/browser?ticket=single-use",
3
+ "expiresAt": "2026-08-06T12:00:00.000Z"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "connectUrl": "wss://not a valid url",
3
+ "expiresAt": "2026-08-06T12:00:00.000Z"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "connectUrl": "ws://relay.example.test/v1/browser?ticket=single-use",
3
+ "expiresAt": "2026-08-06T12:00:00.000Z"
4
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "workspace.status",
3
+ "workspaceId": "00000000-0000-4000-8000-000000000302",
4
+ "status": "online",
5
+ "deviceId": "00000000-0000-4000-8000-000000000303"
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "type": "session.ready",
3
+ "controlVersion": "1.0",
4
+ "relayProtocolVersion": "1.0",
5
+ "sessionId": "browser-session-301",
6
+ "workspaceId": "00000000-0000-4000-8000-000000000302",
7
+ "workspaceStatus": "online"
8
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "controlVersion": "1.0",
3
+ "messages": [
4
+ {
5
+ "fixture": "valid-session-ready.json",
6
+ "valid": true,
7
+ "validFirstFrame": true
8
+ },
9
+ {
10
+ "fixture": "valid-workspace-status.json",
11
+ "valid": true,
12
+ "validFirstFrame": false
13
+ },
14
+ {
15
+ "fixture": "valid-session-error.json",
16
+ "valid": true,
17
+ "validFirstFrame": false
18
+ },
19
+ {
20
+ "fixture": "invalid-session-ready-wire-version.json",
21
+ "valid": false,
22
+ "validFirstFrame": false
23
+ },
24
+ {
25
+ "fixture": "invalid-control-extra-field.json",
26
+ "valid": false,
27
+ "validFirstFrame": false
28
+ }
29
+ ],
30
+ "sessions": [
31
+ {
32
+ "fixture": "valid-browser-session.json",
33
+ "valid": true
34
+ },
35
+ {
36
+ "fixture": "invalid-browser-session-ws.json",
37
+ "valid": false
38
+ },
39
+ {
40
+ "fixture": "invalid-browser-session-url.json",
41
+ "valid": false
42
+ },
43
+ {
44
+ "fixture": "invalid-browser-session-uppercase-scheme.json",
45
+ "valid": false
46
+ },
47
+ {
48
+ "fixture": "invalid-browser-session-trailing-whitespace.json",
49
+ "valid": false
50
+ }
51
+ ]
52
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "connectUrl": "wss://relay.example.test/v1/browser?ticket=single-use",
3
+ "expiresAt": "2026-08-06T12:00:00.000Z"
4
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "session.error",
3
+ "code": "CONTROLLER_CONFLICT",
4
+ "retryable": false
5
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "type": "session.ready",
3
+ "controlVersion": "1.0",
4
+ "relayProtocolVersion": "1.1",
5
+ "sessionId": "browser-session-301",
6
+ "workspaceId": "00000000-0000-4000-8000-000000000302",
7
+ "workspaceStatus": "online"
8
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "workspace.status",
3
+ "workspaceId": "00000000-0000-4000-8000-000000000302",
4
+ "status": "offline"
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/relay-protocol",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,6 +23,13 @@
23
23
  "types": "./dist/reference-adapter.d.ts",
24
24
  "default": "./dist/reference-adapter.js"
25
25
  },
26
+ "./control": {
27
+ "types": "./dist/control.d.ts",
28
+ "default": "./dist/control.js"
29
+ },
30
+ "./control/schema": "./dist/schema/roll-relay-control-v1.schema.json",
31
+ "./control/session-schema": "./dist/schema/roll-relay-browser-session-v1.schema.json",
32
+ "./control/fixtures/*": "./fixtures/control/v1.0/*",
26
33
  "./schema": "./dist/schema/roll-relay-protocol-v1.schema.json",
27
34
  "./schema/v1.0": "./dist/schema/roll-relay-protocol-v1.schema.json",
28
35
  "./schema/v1.1": "./dist/schema/roll-relay-protocol-v1.1.schema.json",
@@ -40,7 +47,7 @@
40
47
  },
41
48
  "dependencies": {
42
49
  "zod": "3.25.76",
43
- "@roll-agent/protocol": "0.4.0"
50
+ "@roll-agent/protocol": "0.5.0"
44
51
  },
45
52
  "devDependencies": {
46
53
  "@types/node": "^22.0.0",
@@ -49,7 +56,7 @@
49
56
  "prettier": "3.8.1"
50
57
  },
51
58
  "scripts": {
52
- "build": "rm -rf dist && tsc -p tsconfig.build.json && node --experimental-strip-types scripts/generate-schema.mjs",
59
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node --experimental-strip-types scripts/generate-schema.mjs && node --experimental-strip-types scripts/generate-control-schema.mjs",
53
60
  "typecheck": "tsc --noEmit",
54
61
  "test": "node --experimental-strip-types --test \"src/**/*.test.ts\"",
55
62
  "clean": "rm -rf dist"