@roll-agent/relay-protocol 0.0.0

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/dist/index.js ADDED
@@ -0,0 +1,343 @@
1
+ import { approvalIdSchema, jsonValueSchema, runtimeEventEnvelopeSchema, runtimeMethodSchemas, threadIdSchema, turnIdSchema, } from "@roll-agent/protocol";
2
+ import { z } from "zod/v4";
3
+ export const SUPPORTED_RELAY_PROTOCOL_VERSIONS = ["1.0"];
4
+ export const RELAY_PROTOCOL_VERSION = SUPPORTED_RELAY_PROTOCOL_VERSIONS[0];
5
+ /** @deprecated Use `RELAY_PROTOCOL_VERSION`. */
6
+ export const COMPANION_RELAY_PROTOCOL_VERSION = RELAY_PROTOCOL_VERSION;
7
+ export const RELAY_MESSAGE_TYPES = {
8
+ deviceConnect: "device.connect",
9
+ runtimeRequest: "runtime.request",
10
+ runtimeResponse: "runtime.response",
11
+ runtimeEvent: "runtime.event",
12
+ runtimeAck: "runtime.ack",
13
+ runtimeGap: "runtime.gap",
14
+ runtimeEncrypted: "runtime.encrypted",
15
+ };
16
+ export const RELAY_MESSAGE_TYPE_VALUES = [
17
+ RELAY_MESSAGE_TYPES.deviceConnect,
18
+ RELAY_MESSAGE_TYPES.runtimeRequest,
19
+ RELAY_MESSAGE_TYPES.runtimeResponse,
20
+ RELAY_MESSAGE_TYPES.runtimeEvent,
21
+ RELAY_MESSAGE_TYPES.runtimeAck,
22
+ RELAY_MESSAGE_TYPES.runtimeGap,
23
+ RELAY_MESSAGE_TYPES.runtimeEncrypted,
24
+ ];
25
+ /**
26
+ * Relay Wire 1.0 is intentionally independent from the latest Runtime registry.
27
+ * New Runtime methods never become remotely callable without a Relay version change.
28
+ */
29
+ export const RELAY_REQUEST_METHODS = {
30
+ initialize: "initialize",
31
+ threadList: "thread.list",
32
+ threadCreate: "thread.create",
33
+ threadOpen: "thread.open",
34
+ threadSnapshot: "thread.snapshot",
35
+ threadRename: "thread.rename",
36
+ threadDelete: "thread.delete",
37
+ threadDetach: "thread.detach",
38
+ threadCapabilities: "thread.capabilities",
39
+ turnStart: "turn.start",
40
+ turnCancel: "turn.cancel",
41
+ approvalRespond: "approval.respond",
42
+ operationGet: "operation.get",
43
+ approvalCandidate: "approval.candidate",
44
+ };
45
+ export const RELAY_REQUEST_METHOD_VALUES = [
46
+ RELAY_REQUEST_METHODS.initialize,
47
+ RELAY_REQUEST_METHODS.threadList,
48
+ RELAY_REQUEST_METHODS.threadCreate,
49
+ RELAY_REQUEST_METHODS.threadOpen,
50
+ RELAY_REQUEST_METHODS.threadSnapshot,
51
+ RELAY_REQUEST_METHODS.threadRename,
52
+ RELAY_REQUEST_METHODS.threadDelete,
53
+ RELAY_REQUEST_METHODS.threadDetach,
54
+ RELAY_REQUEST_METHODS.threadCapabilities,
55
+ RELAY_REQUEST_METHODS.turnStart,
56
+ RELAY_REQUEST_METHODS.turnCancel,
57
+ RELAY_REQUEST_METHODS.approvalRespond,
58
+ RELAY_REQUEST_METHODS.operationGet,
59
+ RELAY_REQUEST_METHODS.approvalCandidate,
60
+ ];
61
+ export const RELAY_REQUEST_METHOD_DISPOSITIONS = {
62
+ [RELAY_REQUEST_METHODS.initialize]: "local-only",
63
+ [RELAY_REQUEST_METHODS.threadList]: "query",
64
+ [RELAY_REQUEST_METHODS.threadCreate]: "mutation",
65
+ [RELAY_REQUEST_METHODS.threadOpen]: "query",
66
+ [RELAY_REQUEST_METHODS.threadSnapshot]: "query",
67
+ [RELAY_REQUEST_METHODS.threadRename]: "mutation",
68
+ [RELAY_REQUEST_METHODS.threadDelete]: "mutation",
69
+ [RELAY_REQUEST_METHODS.threadDetach]: "mutation",
70
+ [RELAY_REQUEST_METHODS.threadCapabilities]: "query",
71
+ [RELAY_REQUEST_METHODS.turnStart]: "mutation",
72
+ [RELAY_REQUEST_METHODS.turnCancel]: "mutation",
73
+ [RELAY_REQUEST_METHODS.approvalRespond]: "mutation",
74
+ [RELAY_REQUEST_METHODS.operationGet]: "query",
75
+ [RELAY_REQUEST_METHODS.approvalCandidate]: "mutation",
76
+ };
77
+ export const RELAY_MUTATION_REQUEST_METHODS = [
78
+ RELAY_REQUEST_METHODS.threadCreate,
79
+ RELAY_REQUEST_METHODS.threadRename,
80
+ RELAY_REQUEST_METHODS.threadDelete,
81
+ RELAY_REQUEST_METHODS.threadDetach,
82
+ RELAY_REQUEST_METHODS.turnStart,
83
+ RELAY_REQUEST_METHODS.turnCancel,
84
+ RELAY_REQUEST_METHODS.approvalRespond,
85
+ RELAY_REQUEST_METHODS.approvalCandidate,
86
+ ];
87
+ export const RELAY_LOCAL_ONLY_REQUEST_METHODS = [
88
+ RELAY_REQUEST_METHODS.initialize,
89
+ ];
90
+ export const RELAY_ERROR_CODES = {
91
+ protocolVersionUnsupported: "RELAY_PROTOCOL_VERSION_UNSUPPORTED",
92
+ invalidFrame: "RELAY_INVALID_FRAME",
93
+ invalidParams: "INVALID_PARAMS",
94
+ requestIdConflict: "RELAY_REQUEST_ID_CONFLICT",
95
+ workspaceNotFound: "WORKSPACE_NOT_FOUND",
96
+ encryptionRequired: "RELAY_ENCRYPTION_REQUIRED",
97
+ encryptedPayloadInvalid: "ENCRYPTED_PAYLOAD_INVALID",
98
+ localApprovalDenied: "LocalApprovalDeniedError",
99
+ localConfirmationRequired: "LocalConfirmationRequiredError",
100
+ companionError: "COMPANION_ERROR",
101
+ };
102
+ export const RELAY_ERROR_RETRYABILITY = {
103
+ [RELAY_ERROR_CODES.protocolVersionUnsupported]: false,
104
+ [RELAY_ERROR_CODES.invalidFrame]: false,
105
+ [RELAY_ERROR_CODES.invalidParams]: false,
106
+ [RELAY_ERROR_CODES.requestIdConflict]: false,
107
+ [RELAY_ERROR_CODES.workspaceNotFound]: false,
108
+ [RELAY_ERROR_CODES.encryptionRequired]: false,
109
+ [RELAY_ERROR_CODES.encryptedPayloadInvalid]: false,
110
+ [RELAY_ERROR_CODES.localApprovalDenied]: false,
111
+ [RELAY_ERROR_CODES.localConfirmationRequired]: false,
112
+ [RELAY_ERROR_CODES.companionError]: false,
113
+ };
114
+ export function getRelayErrorRetryability(code) {
115
+ return RELAY_ERROR_RETRYABILITY[code];
116
+ }
117
+ export const relayProtocolVersionSchema = z.enum(SUPPORTED_RELAY_PROTOCOL_VERSIONS);
118
+ export const deviceIdSchema = z.string().uuid().brand();
119
+ export const workspaceIdSchema = z.string().uuid().brand();
120
+ export const relayRequestIdSchema = z.string().uuid().brand();
121
+ export const relayEnvelopeIdSchema = z.string().uuid();
122
+ export const relayApprovalCandidateParamsSchema = z
123
+ .object({
124
+ threadId: threadIdSchema,
125
+ turnId: turnIdSchema,
126
+ approvalId: approvalIdSchema,
127
+ decision: z.enum(["approve", "reject"]),
128
+ reason: z.string().min(1).optional(),
129
+ })
130
+ .strict()
131
+ .readonly();
132
+ export const relayApprovalCandidateResultSchema = z
133
+ .object({
134
+ accepted: z.literal(true),
135
+ })
136
+ .strict()
137
+ .readonly();
138
+ export const relayRequestMethodSchemas = {
139
+ [RELAY_REQUEST_METHODS.initialize]: runtimeMethodSchemas["initialize"],
140
+ [RELAY_REQUEST_METHODS.threadList]: runtimeMethodSchemas["thread.list"],
141
+ [RELAY_REQUEST_METHODS.threadCreate]: runtimeMethodSchemas["thread.create"],
142
+ [RELAY_REQUEST_METHODS.threadOpen]: runtimeMethodSchemas["thread.open"],
143
+ [RELAY_REQUEST_METHODS.threadSnapshot]: runtimeMethodSchemas["thread.snapshot"],
144
+ [RELAY_REQUEST_METHODS.threadRename]: runtimeMethodSchemas["thread.rename"],
145
+ [RELAY_REQUEST_METHODS.threadDelete]: runtimeMethodSchemas["thread.delete"],
146
+ [RELAY_REQUEST_METHODS.threadDetach]: runtimeMethodSchemas["thread.detach"],
147
+ [RELAY_REQUEST_METHODS.threadCapabilities]: runtimeMethodSchemas["thread.capabilities"],
148
+ [RELAY_REQUEST_METHODS.turnStart]: runtimeMethodSchemas["turn.start"],
149
+ [RELAY_REQUEST_METHODS.turnCancel]: runtimeMethodSchemas["turn.cancel"],
150
+ [RELAY_REQUEST_METHODS.approvalRespond]: runtimeMethodSchemas["approval.respond"],
151
+ [RELAY_REQUEST_METHODS.operationGet]: runtimeMethodSchemas["operation.get"],
152
+ [RELAY_REQUEST_METHODS.approvalCandidate]: {
153
+ params: relayApprovalCandidateParamsSchema,
154
+ result: relayApprovalCandidateResultSchema,
155
+ },
156
+ };
157
+ export function isRelayRequestMethod(value) {
158
+ return Object.hasOwn(relayRequestMethodSchemas, value);
159
+ }
160
+ export function getRelayRequestMethodDisposition(value) {
161
+ return isRelayRequestMethod(value) ? RELAY_REQUEST_METHOD_DISPOSITIONS[value] : undefined;
162
+ }
163
+ export function isRelayMutationRequestMethod(method) {
164
+ return RELAY_REQUEST_METHOD_DISPOSITIONS[method] === "mutation";
165
+ }
166
+ export function parseRelayRequestParams(method, value) {
167
+ return relayRequestMethodSchemas[method].params.parse(value);
168
+ }
169
+ export function parseRelayRequestResult(method, value) {
170
+ return relayRequestMethodSchemas[method].result.parse(value);
171
+ }
172
+ export const relayDeviceConnectSchema = z
173
+ .object({
174
+ type: z.literal(RELAY_MESSAGE_TYPES.deviceConnect),
175
+ protocolVersion: z.literal(RELAY_PROTOCOL_VERSION),
176
+ deviceId: deviceIdSchema,
177
+ pairingToken: z.string().min(16),
178
+ })
179
+ .strict()
180
+ .readonly();
181
+ export const relayRuntimeRequestSchema = z
182
+ .object({
183
+ type: z.literal(RELAY_MESSAGE_TYPES.runtimeRequest),
184
+ requestId: relayRequestIdSchema,
185
+ workspaceId: workspaceIdSchema,
186
+ method: z.enum(RELAY_REQUEST_METHOD_VALUES),
187
+ params: jsonValueSchema,
188
+ })
189
+ .strict()
190
+ .readonly();
191
+ export const relayErrorSchema = z
192
+ .object({
193
+ code: z.string().min(1),
194
+ message: z.string(),
195
+ retryable: z.boolean(),
196
+ })
197
+ .strict()
198
+ .readonly();
199
+ /**
200
+ * This preserves the pre-freeze v1.0 shape: result/error are independently optional.
201
+ * A strict exactly-one response belongs in a later Relay Wire version.
202
+ */
203
+ export const relayRuntimeResponseSchema = z
204
+ .object({
205
+ type: z.literal(RELAY_MESSAGE_TYPES.runtimeResponse),
206
+ requestId: relayRequestIdSchema,
207
+ workspaceId: workspaceIdSchema,
208
+ result: jsonValueSchema.optional(),
209
+ error: relayErrorSchema.optional(),
210
+ })
211
+ .strict()
212
+ .readonly();
213
+ export const relayRuntimeEventSchema = z
214
+ .object({
215
+ type: z.literal(RELAY_MESSAGE_TYPES.runtimeEvent),
216
+ workspaceId: workspaceIdSchema,
217
+ relaySequence: z.number().int().nonnegative(),
218
+ event: runtimeEventEnvelopeSchema,
219
+ })
220
+ .strict()
221
+ .readonly();
222
+ export const relayAckSchema = z
223
+ .object({
224
+ type: z.literal(RELAY_MESSAGE_TYPES.runtimeAck),
225
+ workspaceId: workspaceIdSchema,
226
+ throughRelaySequence: z.number().int().min(-1),
227
+ })
228
+ .strict()
229
+ .readonly();
230
+ export const relayGapSchema = z
231
+ .object({
232
+ type: z.literal(RELAY_MESSAGE_TYPES.runtimeGap),
233
+ workspaceId: workspaceIdSchema,
234
+ fromRelaySequence: z.number().int().nonnegative(),
235
+ throughRelaySequence: z.number().int().nonnegative(),
236
+ recovery: z.literal("thread.snapshot"),
237
+ })
238
+ .strict()
239
+ .readonly();
240
+ /**
241
+ * This preserves the pre-freeze v1.0 metadata shape. Payload-kind-specific required
242
+ * fields must be introduced under a later Relay Wire version.
243
+ */
244
+ export const relayEncryptedMessageSchema = z
245
+ .object({
246
+ type: z.literal(RELAY_MESSAGE_TYPES.runtimeEncrypted),
247
+ workspaceId: workspaceIdSchema,
248
+ envelopeId: relayEnvelopeIdSchema,
249
+ payloadKind: z.enum(["request", "response", "event"]),
250
+ requestId: relayRequestIdSchema.optional(),
251
+ relaySequence: z.number().int().nonnegative().optional(),
252
+ algorithm: z.string().min(1),
253
+ nonce: z.string().min(1),
254
+ ciphertext: z.string().min(1),
255
+ })
256
+ .strict()
257
+ .readonly();
258
+ export const relayMessageSchemas = {
259
+ [RELAY_MESSAGE_TYPES.deviceConnect]: relayDeviceConnectSchema,
260
+ [RELAY_MESSAGE_TYPES.runtimeRequest]: relayRuntimeRequestSchema,
261
+ [RELAY_MESSAGE_TYPES.runtimeResponse]: relayRuntimeResponseSchema,
262
+ [RELAY_MESSAGE_TYPES.runtimeEvent]: relayRuntimeEventSchema,
263
+ [RELAY_MESSAGE_TYPES.runtimeAck]: relayAckSchema,
264
+ [RELAY_MESSAGE_TYPES.runtimeGap]: relayGapSchema,
265
+ [RELAY_MESSAGE_TYPES.runtimeEncrypted]: relayEncryptedMessageSchema,
266
+ };
267
+ export const relayMessageSchema = z.discriminatedUnion("type", [
268
+ relayDeviceConnectSchema,
269
+ relayRuntimeRequestSchema,
270
+ relayRuntimeResponseSchema,
271
+ relayRuntimeEventSchema,
272
+ relayAckSchema,
273
+ relayGapSchema,
274
+ relayEncryptedMessageSchema,
275
+ ]);
276
+ export const RELAY_PROTOCOL_REGISTRY = {
277
+ "1.0": {
278
+ messageTypes: RELAY_MESSAGE_TYPE_VALUES,
279
+ requestMethods: RELAY_REQUEST_METHOD_VALUES,
280
+ mutationMethods: RELAY_MUTATION_REQUEST_METHODS,
281
+ localOnlyMethods: RELAY_LOCAL_ONLY_REQUEST_METHODS,
282
+ },
283
+ };
284
+ export function getRelayProtocolRegistry(version) {
285
+ return RELAY_PROTOCOL_REGISTRY[version];
286
+ }
287
+ export function negotiateRelayProtocolVersion(peerVersions) {
288
+ return SUPPORTED_RELAY_PROTOCOL_VERSIONS.find((version) => peerVersions.includes(version));
289
+ }
290
+ export function canonicalizeRelayJson(value) {
291
+ if (Array.isArray(value)) {
292
+ return `[${value.map((entry) => canonicalizeRelayJson(entry)).join(",")}]`;
293
+ }
294
+ if (value !== null && typeof value === "object") {
295
+ return `{${Object.entries(value)
296
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
297
+ .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalizeRelayJson(entry)}`)
298
+ .join(",")}}`;
299
+ }
300
+ const encoded = JSON.stringify(value);
301
+ if (encoded === undefined) {
302
+ throw new Error("Relay canonical JSON received a non-JSON value");
303
+ }
304
+ return encoded;
305
+ }
306
+ export const RELAY_REQUEST_REPLAY_DISPOSITIONS = {
307
+ new: "new",
308
+ replay: "replay",
309
+ conflict: "conflict",
310
+ };
311
+ export function classifyRelayRequestReplay(existing, candidate) {
312
+ if (existing.workspaceId !== candidate.workspaceId ||
313
+ existing.requestId !== candidate.requestId) {
314
+ return { disposition: RELAY_REQUEST_REPLAY_DISPOSITIONS.new };
315
+ }
316
+ const existingIdentity = {
317
+ method: existing.method,
318
+ params: existing.params,
319
+ };
320
+ const candidateIdentity = {
321
+ method: candidate.method,
322
+ params: candidate.params,
323
+ };
324
+ if (canonicalizeRelayJson(existingIdentity) === canonicalizeRelayJson(candidateIdentity)) {
325
+ return { disposition: RELAY_REQUEST_REPLAY_DISPOSITIONS.replay };
326
+ }
327
+ return {
328
+ disposition: RELAY_REQUEST_REPLAY_DISPOSITIONS.conflict,
329
+ errorCode: RELAY_ERROR_CODES.requestIdConflict,
330
+ };
331
+ }
332
+ export function classifyRelayAck(state) {
333
+ if (!Number.isInteger(state.acknowledgedThrough) ||
334
+ !Number.isInteger(state.advertisedThrough) ||
335
+ !Number.isInteger(state.incomingThrough) ||
336
+ state.acknowledgedThrough < -1 ||
337
+ state.advertisedThrough < -1 ||
338
+ state.incomingThrough < -1 ||
339
+ state.incomingThrough > state.advertisedThrough) {
340
+ return "invalid";
341
+ }
342
+ return state.incomingThrough > state.acknowledgedThrough ? "advance" : "duplicate";
343
+ }