@ccmsg/protocol 1.7.0 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/protocol",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Wire contract (schema + types + op attribute table) shared by the ccmsg daemon and web UI",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
@@ -14,7 +14,8 @@
14
14
  "type": "module",
15
15
  "main": "./src/index.ts",
16
16
  "exports": {
17
- ".": "./src/index.ts"
17
+ ".": "./src/index.ts",
18
+ "./fixtures": "./src/fixtures/index.ts"
18
19
  },
19
20
  "scripts": {
20
21
  "typecheck": "tsc --noEmit",
@@ -306,7 +306,21 @@ export const AuthResolveResponse = response("auth_resolve", AuthResolveResult);
306
306
  * parallel would merge by last write and lose a generation, which reads exactly
307
307
  * like a stolen token being replayed — so the rotation is forwarded rather than
308
308
  * done where the request landed. */
309
- export const AuthRotateArgs = Type.Object({ refresh_token: Base64Url });
309
+ export const AuthRotateArgs = Type.Object({
310
+ refresh_token: Base64Url,
311
+ /** What the receiving instance observed of the caller, carried to the issuer
312
+ * for `last_refresh`. The person is at the other end of the receiver's
313
+ * connection, not the issuer's, so these are only knowable there; forwarded
314
+ * without them, a rotation would be remembered as a time and nothing else.
315
+ *
316
+ * Stated by the receiver and never checked by the issuer — the same standing
317
+ * as the values on a rotation that was not forwarded, which the client and
318
+ * its connection are equally the only source of. Nothing may be decided by
319
+ * them. */
320
+ reason: Type.Optional(AuthRefreshReason),
321
+ ip: Type.Optional(Type.String({ minLength: 1, maxLength: 45 })),
322
+ user_agent: Type.Optional(Type.String({ maxLength: 512 })),
323
+ });
310
324
  export type AuthRotateArgs = Static<typeof AuthRotateArgs>;
311
325
 
312
326
  /** Both halves, unlike the person-facing ops: the instance that asked for the
@@ -0,0 +1,305 @@
1
+ import type { Static } from "@sinclair/typebox";
2
+ import type {
3
+ AuthAssertRequest,
4
+ AuthAssertResponse,
5
+ AuthChallengeRequest,
6
+ AuthChallengeResponse,
7
+ AuthRefreshRequest,
8
+ AuthRefreshResponse,
9
+ AuthRefreshTokenRequest,
10
+ AuthRefreshTokenResponse,
11
+ AuthRegisterRequest,
12
+ AuthRegisterResponse,
13
+ AuthResolveRequest,
14
+ AuthResolveResponse,
15
+ AuthRotateRequest,
16
+ AuthRotateResponse,
17
+ } from "../common/auth.ts";
18
+ import type { HelloRequest, HelloResponse } from "../common/hello.ts";
19
+ import type { InstancePingRequest, InstancePingResponse } from "../common/ping.ts";
20
+ import type {
21
+ InstanceShutdownRequest,
22
+ InstanceShutdownResponse,
23
+ SessionStoppingRequest,
24
+ SessionStoppingResponse,
25
+ } from "../common/shutdown.ts";
26
+ import type {
27
+ TopicSubscribeRequest,
28
+ TopicSubscribeResponse,
29
+ TopicUnsubscribeRequest,
30
+ TopicUnsubscribeResponse,
31
+ } from "../common/topics.ts";
32
+ import { FIXTURE_IDS, FIXTURE_NOW } from "./ids.ts";
33
+
34
+ const { sid, instance, other_instance, endpoint, other_endpoint, request_id } = FIXTURE_IDS;
35
+
36
+ export const HELLO_REQUEST: Static<typeof HelloRequest> = {
37
+ request_id,
38
+ op: "hello",
39
+ role: "session",
40
+ protocol_version: 3,
41
+ sid,
42
+ client_version: "0.1.0",
43
+ repo: "ccmsg-protocol",
44
+ ws: "main",
45
+ cwd: "/repos/kawaz/ccmsg-protocol/main",
46
+ repo_root: "/repos/kawaz/ccmsg-protocol",
47
+ branch: "main",
48
+ transcript_path: "/transcripts/6f1a2b3c.jsonl",
49
+ title: "contract fixtures",
50
+ model: "claude-opus-5",
51
+ effort: "high",
52
+ };
53
+
54
+ /** The other side of `hello`: an instance greeting a peer, which carries the
55
+ * mesh claim in place of a session's meta. */
56
+ export const HELLO_MESH_REQUEST: Static<typeof HelloRequest> = {
57
+ request_id,
58
+ op: "hello",
59
+ role: "instance",
60
+ protocol_version: 3,
61
+ mesh: {
62
+ ver: 1,
63
+ iss: other_endpoint,
64
+ aud: endpoint,
65
+ id: other_instance,
66
+ kid: "9f2c7a5e1b4d8036af51c9e27d604b18",
67
+ },
68
+ };
69
+
70
+ export const HELLO_RESPONSE: Static<typeof HelloResponse> = {
71
+ ok: true,
72
+ request_id,
73
+ protocol_version: 3,
74
+ instance,
75
+ endpoint,
76
+ auth_expires_at: FIXTURE_NOW + 10_000_000,
77
+ instances: [
78
+ { id: instance, endpoint, host: "mba", reachable: true },
79
+ { id: other_instance, endpoint: other_endpoint, host: "nuc", reachable: false },
80
+ ],
81
+ capabilities: ["fork", "launcher", "terminal"],
82
+ version: "0.1.0",
83
+ started_at: FIXTURE_NOW - 3_600_000,
84
+ };
85
+
86
+ export const INSTANCE_PING_REQUEST: Static<typeof InstancePingRequest> = {
87
+ request_id,
88
+ op: "instance_ping",
89
+ };
90
+
91
+ export const INSTANCE_PING_RESPONSE: Static<typeof InstancePingResponse> = {
92
+ ok: true,
93
+ request_id,
94
+ instance,
95
+ version: "0.1.0",
96
+ pid: 4821,
97
+ started_at: FIXTURE_NOW - 3_600_000,
98
+ clients: 3,
99
+ http: ["127.0.0.1:8787"],
100
+ network: "online",
101
+ };
102
+
103
+ export const INSTANCE_SHUTDOWN_REQUEST: Static<typeof InstanceShutdownRequest> = {
104
+ request_id,
105
+ op: "instance_shutdown",
106
+ };
107
+
108
+ export const INSTANCE_SHUTDOWN_RESPONSE: Static<typeof InstanceShutdownResponse> = {
109
+ ok: true,
110
+ request_id,
111
+ };
112
+
113
+ export const SESSION_STOPPING_REQUEST: Static<typeof SessionStoppingRequest> = {
114
+ request_id,
115
+ op: "session_stopping",
116
+ reason: "prompt_input_exit",
117
+ };
118
+
119
+ export const SESSION_STOPPING_RESPONSE: Static<typeof SessionStoppingResponse> = {
120
+ ok: true,
121
+ request_id,
122
+ stopped_at: FIXTURE_NOW - 1_000_000,
123
+ };
124
+
125
+ export const TOPIC_SUBSCRIBE_REQUEST: Static<typeof TopicSubscribeRequest> = {
126
+ request_id,
127
+ op: "topic_subscribe",
128
+ topic: "peers",
129
+ };
130
+
131
+ export const TOPIC_SUBSCRIBE_RESPONSE: Static<typeof TopicSubscribeResponse> = {
132
+ ok: true,
133
+ request_id,
134
+ topic: "peers",
135
+ };
136
+
137
+ /** A subscription naming one session, which is how the session-scoped topics
138
+ * are spelled. */
139
+ export const TOPIC_SUBSCRIBE_SESSION_REQUEST: Static<typeof TopicSubscribeRequest> = {
140
+ request_id,
141
+ op: "topic_subscribe",
142
+ topic: `transcript:${sid}`,
143
+ };
144
+
145
+ export const TOPIC_UNSUBSCRIBE_REQUEST: Static<typeof TopicUnsubscribeRequest> = {
146
+ request_id,
147
+ op: "topic_unsubscribe",
148
+ topic: "peers",
149
+ };
150
+
151
+ export const TOPIC_UNSUBSCRIBE_RESPONSE: Static<typeof TopicUnsubscribeResponse> = {
152
+ ok: true,
153
+ request_id,
154
+ topic: "peers",
155
+ };
156
+
157
+ const CHALLENGE = {
158
+ challenge: "Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA",
159
+ issuer: instance,
160
+ expires_at: FIXTURE_NOW + 300_000,
161
+ };
162
+
163
+ const ACCESS = { value: "YWNjZXNzLXRva2Vu", expires_at: FIXTURE_NOW + 10_000_000 };
164
+ const REFRESH = { value: "cmVmcmVzaA", expires_at: FIXTURE_NOW + 600_000_000 };
165
+ const REGISTER_TOKEN = "eyJhbGciOiJIUzI1NiJ9.e30.c2ln";
166
+ const REGISTER_CODE = "048213";
167
+ const SUBJECT = "personal-1";
168
+
169
+ export const AUTH_CHALLENGE_REQUEST: Static<typeof AuthChallengeRequest> = {
170
+ request_id,
171
+ op: "auth_challenge",
172
+ };
173
+
174
+ export const AUTH_CHALLENGE_RESPONSE: Static<typeof AuthChallengeResponse> = {
175
+ ok: true,
176
+ request_id,
177
+ ...CHALLENGE,
178
+ };
179
+
180
+ export const AUTH_REGISTER_REQUEST: Static<typeof AuthRegisterRequest> = {
181
+ request_id,
182
+ op: "auth_register",
183
+ token: REGISTER_TOKEN,
184
+ code: REGISTER_CODE,
185
+ device_label: "work laptop",
186
+ challenge: CHALLENGE,
187
+ credential: {
188
+ id: "Y3JlZC1pZA",
189
+ raw_id: "Y3JlZC1pZA",
190
+ client_data_json: "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0",
191
+ attestation_object: "o2NmbXRkbm9uZQ",
192
+ },
193
+ };
194
+
195
+ export const AUTH_REGISTER_RESPONSE: Static<typeof AuthRegisterResponse> = {
196
+ ok: true,
197
+ request_id,
198
+ sub: SUBJECT,
199
+ access: ACCESS,
200
+ };
201
+
202
+ export const AUTH_ASSERT_REQUEST: Static<typeof AuthAssertRequest> = {
203
+ request_id,
204
+ op: "auth_assert",
205
+ challenge: CHALLENGE,
206
+ credential: {
207
+ raw_id: "Y3JlZC1pZA",
208
+ client_data_json: "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0",
209
+ authenticator_data: "YXV0aC1kYXRh",
210
+ signature: "c2lnbmF0dXJl",
211
+ user_handle: "dXNlci1oYW5kbGU",
212
+ },
213
+ };
214
+
215
+ export const AUTH_ASSERT_RESPONSE: Static<typeof AuthAssertResponse> = {
216
+ ok: true,
217
+ request_id,
218
+ sub: SUBJECT,
219
+ access: ACCESS,
220
+ };
221
+
222
+ export const AUTH_REFRESH_TOKEN_REQUEST: Static<typeof AuthRefreshTokenRequest> = {
223
+ request_id,
224
+ op: "auth_refresh_token",
225
+ reason: "reload",
226
+ };
227
+
228
+ export const AUTH_REFRESH_TOKEN_RESPONSE: Static<typeof AuthRefreshTokenResponse> = {
229
+ ok: true,
230
+ request_id,
231
+ sub: SUBJECT,
232
+ access: ACCESS,
233
+ };
234
+
235
+ export const AUTH_REFRESH_REQUEST: Static<typeof AuthRefreshRequest> = {
236
+ request_id,
237
+ op: "auth_refresh",
238
+ access_token: ACCESS.value,
239
+ };
240
+
241
+ export const AUTH_REFRESH_RESPONSE: Static<typeof AuthRefreshResponse> = {
242
+ ok: true,
243
+ request_id,
244
+ auth_expires_at: FIXTURE_NOW + 20_000_000,
245
+ };
246
+
247
+ export const AUTH_RESOLVE_REQUEST = {
248
+ request_id,
249
+ op: "auth_resolve",
250
+ to_instance: instance,
251
+ kind: "register",
252
+ token: REGISTER_TOKEN,
253
+ code: REGISTER_CODE,
254
+ } satisfies Static<typeof AuthResolveRequest>;
255
+
256
+ export const AUTH_RESOLVE_RESPONSE = {
257
+ ok: true,
258
+ request_id,
259
+ kind: "register",
260
+ claims: {
261
+ iss: instance,
262
+ sub: SUBJECT,
263
+ unit: "personal",
264
+ endpoint,
265
+ rp_id: "mba.example.ts.net",
266
+ expires_at: FIXTURE_NOW + 600_000,
267
+ jti: "01J9Z3W2Q",
268
+ user_id: "dXNlci1oYW5kbGU",
269
+ issued_label: "for kawaz",
270
+ },
271
+ } satisfies Static<typeof AuthResolveResponse>;
272
+
273
+ /** The other half of the resolve union: spending a challenge, which answers
274
+ * nothing beyond having spent it. */
275
+ export const AUTH_RESOLVE_CHALLENGE_REQUEST = {
276
+ request_id,
277
+ op: "auth_resolve",
278
+ to_instance: instance,
279
+ kind: "challenge",
280
+ challenge: CHALLENGE.challenge,
281
+ } satisfies Static<typeof AuthResolveRequest>;
282
+
283
+ export const AUTH_RESOLVE_CHALLENGE_RESPONSE = {
284
+ ok: true,
285
+ request_id,
286
+ kind: "challenge",
287
+ } satisfies Static<typeof AuthResolveResponse>;
288
+
289
+ export const AUTH_ROTATE_REQUEST: Static<typeof AuthRotateRequest> = {
290
+ request_id,
291
+ op: "auth_rotate",
292
+ to_instance: instance,
293
+ refresh_token: REFRESH.value,
294
+ reason: "reconnect",
295
+ ip: "203.0.113.7",
296
+ user_agent: "Mozilla/5.0",
297
+ };
298
+
299
+ export const AUTH_ROTATE_RESPONSE: Static<typeof AuthRotateResponse> = {
300
+ ok: true,
301
+ request_id,
302
+ sub: SUBJECT,
303
+ access: ACCESS,
304
+ refresh: REFRESH,
305
+ };