@kangentic/protocol 0.3.0 → 0.6.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.
@@ -9,7 +9,7 @@
9
9
  * wire a handler, and a device's roster entry still has to include it in
10
10
  * its CapabilitySet.
11
11
  */
12
- export declare const CAPABILITY_VERBS: readonly ["read-stream", "read-board", "read-diff", "send-user-message", "move-task", "answer-permission-prompt", "interactive-terminal", "board-tool-read", "board-tool-write"];
12
+ export declare const CAPABILITY_VERBS: readonly ["read-stream", "read-board", "read-diff", "send-user-message", "move-task", "answer-permission-prompt", "interactive-terminal", "board-tool-read", "board-tool-write", "register-push"];
13
13
  export type CapabilityVerb = (typeof CAPABILITY_VERBS)[number];
14
14
  export declare function isCapabilityVerb(value: string): value is CapabilityVerb;
15
15
  /** Deny-by-default: a verb absent from the set is not authorized. */
@@ -0,0 +1,38 @@
1
+ export declare const PUSH_CATEGORIES: readonly ["permission-needed", "agent-question", "turn-complete", "session-failed"];
2
+ export type PushCategory = (typeof PUSH_CATEGORIES)[number];
3
+ export declare function isPushCategory(value: unknown): value is PushCategory;
4
+ /** Reject an opened envelope whose sentAt is older than this (stale replay). */
5
+ export declare const PUSH_ENVELOPE_MAX_AGE_MS: number;
6
+ /** Tolerated forward clock skew; anything claiming to be further in the future is rejected. */
7
+ export declare const PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS: number;
8
+ /**
9
+ * The real notification content, visible only on the sealing desktop and
10
+ * the opening device. Everything the OS-visible push payload carries
11
+ * instead is a static placeholder.
12
+ */
13
+ export interface PushEnvelopePlaintext {
14
+ category: PushCategory;
15
+ projectId: string;
16
+ taskId: string;
17
+ sessionId: string;
18
+ taskTitle: string;
19
+ detail: string;
20
+ /** Wall-clock ms at seal time; openPushEnvelope enforces the freshness window from it. */
21
+ sentAt: number;
22
+ }
23
+ /** Narrows a decrypted envelope's JSON to PushEnvelopePlaintext. Throws on any malformed field. */
24
+ export declare function parsePushEnvelopePlaintext(value: unknown): PushEnvelopePlaintext;
25
+ /**
26
+ * Seals a notification into an opaque base64url blob:
27
+ * base64url(nonce[24] || XChaCha20-Poly1305(pushKey, nonce, aad=recipientStaticPublicKey, JSON(plaintext))).
28
+ */
29
+ export declare function sealPushEnvelope(pushKey: Uint8Array, recipientStaticPublicKey: Uint8Array, plaintext: PushEnvelopePlaintext): string;
30
+ /**
31
+ * Opens a sealed push blob. Throws on: a malformed blob, a tampered
32
+ * ciphertext, the wrong key, the wrong recipient key (AAD mismatch),
33
+ * non-JSON or malformed plaintext, an unknown category, or a sentAt
34
+ * outside the freshness window (older than 24h, or more than 5min in the
35
+ * future). The caller (the device-side notification handler) catches and
36
+ * shows the generic placeholder.
37
+ */
38
+ export declare function openPushEnvelope(pushKey: Uint8Array, recipientStaticPublicKey: Uint8Array, blob: string): PushEnvelopePlaintext;
@@ -2,7 +2,7 @@
2
2
  * The event contract the phone consumes, pushed as EventMessage.event over
3
3
  * an established BridgeSession once Phase 2's capability handlers (desktop
4
4
  * bridge module) subscribe SessionManager/transcript-service/DiffService to
5
- * a session's live state. Five kinds:
5
+ * a session's live state. Six kinds:
6
6
  *
7
7
  * - transcript: a revision-gated delta from resolveTaskTranscript, for the
8
8
  * phone's transcript-styled conversation view.
@@ -13,6 +13,9 @@
13
13
  * - terminal: raw PTY output from the unfiltered data-tap, for the phone's
14
14
  * raw-terminal-mirror view (a distinct consumer from the parsed
15
15
  * transcript).
16
+ * - terminal-resize: the PTY grid changed (desktop pane refit, phone fit
17
+ * request, or restore-on-release); the phone re-sizes its renderer to
18
+ * match before the TUI's repaint bytes arrive on the terminal stream.
16
19
  * - board: a board-mutation notification, filterable by project and
17
20
  * (optionally) task.
18
21
  * - diff: a payload-less "something under this task's worktree changed,
@@ -24,7 +27,7 @@
24
27
  * event before its router dispatches on `kind`.
25
28
  */
26
29
  import type { JsonValue } from '../wire/messages';
27
- import { type ActivityReasonWire, type ActivityStateWire, type SessionEventWire, type SessionUsageWire, type TranscriptEventPayload } from './payloads';
30
+ import { type ActivityReasonWire, type ActivityStateWire, type SessionEventWire, type SessionUsageWire, type TerminalDimensionsWire, type TranscriptEventPayload } from './payloads';
28
31
  export interface TranscriptEvent {
29
32
  kind: 'transcript';
30
33
  sessionId: string;
@@ -42,10 +45,30 @@ export type ActivityEventPayload = {
42
45
  } | {
43
46
  type: 'event';
44
47
  event: SessionEventWire;
45
- } | {
48
+ }
49
+ /**
50
+ * `options`, when present on a pending push, carries the prompt dialog's
51
+ * numbered option labels in keystroke order (options[0] is answered with
52
+ * "1\r"). Absent from pre-0.6.0 desktops and whenever no numbered dialog
53
+ * could be parsed from the PTY frame; the phone then falls back to its
54
+ * blind approve/deny keystrokes.
55
+ */
56
+ | {
46
57
  type: 'permission';
47
58
  promptId: string;
48
59
  pending: boolean;
60
+ options?: string[];
61
+ }
62
+ /**
63
+ * The streamed session's PTY exited. Pushed once by the desktop's
64
+ * read-stream subscription right before it tears itself down, so the
65
+ * phone learns "this session is over" from the feed itself instead of
66
+ * inferring it from silence. `intentional` distinguishes a deliberate
67
+ * stop (desktop Stop button, suspend, shutdown) from a crash.
68
+ */
69
+ | {
70
+ type: 'session-ended';
71
+ intentional: boolean;
49
72
  };
50
73
  export interface ActivityEvent {
51
74
  kind: 'activity';
@@ -61,6 +84,13 @@ export interface TerminalEvent {
61
84
  data: string;
62
85
  };
63
86
  }
87
+ export interface TerminalResizeEvent {
88
+ kind: 'terminal-resize';
89
+ sessionId: string;
90
+ taskId: string;
91
+ /** The PTY's new grid; every terminal byte sent after this event is laid out for it. */
92
+ payload: TerminalDimensionsWire;
93
+ }
64
94
  export interface BoardEventPayload {
65
95
  change: 'task-created' | 'task-updated' | 'task-deleted' | 'swimlane-updated' | 'backlog-changed';
66
96
  ids: string[];
@@ -76,7 +106,7 @@ export interface DiffEvent {
76
106
  taskId: string;
77
107
  payload: null;
78
108
  }
79
- export type BridgeEvent = TranscriptEvent | ActivityEvent | TerminalEvent | BoardEvent | DiffEvent;
109
+ export type BridgeEvent = TranscriptEvent | ActivityEvent | TerminalEvent | TerminalResizeEvent | BoardEvent | DiffEvent;
80
110
  /**
81
111
  * Narrows an activity event's payload to its typed union. Throws on a
82
112
  * malformed required field so the caller can drop the event cleanly.
@@ -102,6 +102,17 @@ export type TranscriptEventPayload = {
102
102
  revision: number;
103
103
  totalEntries: number;
104
104
  };
105
+ /**
106
+ * PTY grid dimensions, shared by the read-stream snapshot (`ptyDimensions`),
107
+ * the `terminal-resize` event, and the interactive-terminal `resize` action.
108
+ * The bytes on the terminal stream are laid out for exactly this grid; a
109
+ * renderer that uses any other geometry misplaces every cursor-addressed
110
+ * frame the fullscreen TUI draws.
111
+ */
112
+ export interface TerminalDimensionsWire {
113
+ cols: number;
114
+ rows: number;
115
+ }
105
116
  /** Mirrors the desktop's ActivityState. 'permission' means the agent paused for user approval (incl. AskUserQuestion / ExitPlanMode pauses). */
106
117
  export type ActivityStateWire = 'thinking' | 'idle' | 'permission';
107
118
  /** Mirrors the desktop's ActivityReason discriminated union. */
@@ -236,6 +247,12 @@ export interface DiffFileContentWire {
236
247
  export declare function parseTranscriptEntriesWire(payload: JsonValue): TranscriptEntryWire[];
237
248
  /** Narrows a transcript event's delta/reset payload. Throws on a malformed required field so the caller can drop the push. */
238
249
  export declare function parseTranscriptEventPayload(payload: JsonValue): TranscriptEventPayload;
250
+ /**
251
+ * Narrows a PTY dimensions payload. Bounds are sanity caps, not policy:
252
+ * the desktop's own resize clamp (cols >= 2, rows >= 1) is the floor, and
253
+ * 1000 on each axis rejects garbage without constraining any real device.
254
+ */
255
+ export declare function parseTerminalDimensionsWire(payload: JsonValue): TerminalDimensionsWire;
239
256
  /** True for a well-formed ActivityStateWire value. */
240
257
  export declare function isActivityStateWire(value: unknown): value is ActivityStateWire;
241
258
  /** True for a well-formed ActivityReasonWire value. */
package/dist/index.cjs CHANGED
@@ -34,6 +34,9 @@ __export(index_exports, {
34
34
  MAX_FRAME_LENGTH: () => MAX_FRAME_LENGTH,
35
35
  PAIRING_URI_SCHEME: () => PAIRING_URI_SCHEME,
36
36
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
37
+ PUSH_CATEGORIES: () => PUSH_CATEGORIES,
38
+ PUSH_ENVELOPE_MAX_AGE_MS: () => PUSH_ENVELOPE_MAX_AGE_MS,
39
+ PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS: () => PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS,
37
40
  SecretstreamState: () => SecretstreamState,
38
41
  SessionFrameKind: () => SessionFrameKind,
39
42
  X25519_KEY_LENGTH: () => X25519_KEY_LENGTH,
@@ -64,8 +67,10 @@ __export(index_exports, {
64
67
  isBridgeEvent: () => isBridgeEvent,
65
68
  isCapabilityVerb: () => isCapabilityVerb,
66
69
  isJsonValue: () => isJsonValue,
70
+ isPushCategory: () => isPushCategory,
67
71
  isRecord: () => isRecord,
68
72
  isRosterEntryExpired: () => isRosterEntryExpired,
73
+ openPushEnvelope: () => openPushEnvelope,
69
74
  parseActivityEventPayload: () => parseActivityEventPayload,
70
75
  parseBacklogItemWire: () => parseBacklogItemWire,
71
76
  parseBoardColumnWire: () => parseBoardColumnWire,
@@ -73,16 +78,20 @@ __export(index_exports, {
73
78
  parseCapabilityRequestPayload: () => parseCapabilityRequestPayload,
74
79
  parseDiffFileContentWire: () => parseDiffFileContentWire,
75
80
  parseDiffFileListWire: () => parseDiffFileListWire,
81
+ parsePushEnvelopePlaintext: () => parsePushEnvelopePlaintext,
76
82
  parseReadBoardResponsePayload: () => parseReadBoardResponsePayload,
77
83
  parseReadDiffResponsePayload: () => parseReadDiffResponsePayload,
78
84
  parseReadStreamResponsePayload: () => parseReadStreamResponsePayload,
85
+ parseRegisterPushRequestPayload: () => parseRegisterPushRequestPayload,
79
86
  parseSessionEventWire: () => parseSessionEventWire,
80
87
  parseSessionUsageWire: () => parseSessionUsageWire,
88
+ parseTerminalDimensionsWire: () => parseTerminalDimensionsWire,
81
89
  parseTranscriptEntriesWire: () => parseTranscriptEntriesWire,
82
90
  parseTranscriptEventPayload: () => parseTranscriptEventPayload,
83
91
  parseTranscriptWindowResponsePayload: () => parseTranscriptWindowResponsePayload,
84
92
  randomBytes: () => randomBytes4,
85
93
  rosterDeviceCapabilitySet: () => rosterDeviceCapabilitySet,
94
+ sealPushEnvelope: () => sealPushEnvelope,
86
95
  signRosterEntry: () => signRosterEntry,
87
96
  unwrapSessionFrame: () => unwrapSessionFrame,
88
97
  verifyRosterEntry: () => verifyRosterEntry,
@@ -4975,7 +4984,8 @@ var CAPABILITY_VERBS = [
4975
4984
  "answer-permission-prompt",
4976
4985
  "interactive-terminal",
4977
4986
  "board-tool-read",
4978
- "board-tool-write"
4987
+ "board-tool-write",
4988
+ "register-push"
4979
4989
  ];
4980
4990
  function isCapabilityVerb(value) {
4981
4991
  return CAPABILITY_VERBS.includes(value);
@@ -5061,7 +5071,8 @@ function validateEvent(event) {
5061
5071
  switch (event.kind) {
5062
5072
  case "transcript":
5063
5073
  case "activity":
5064
- case "terminal": {
5074
+ case "terminal":
5075
+ case "terminal-resize": {
5065
5076
  if (typeof event.sessionId !== "string") throw new Error(`"${event.kind}" event is missing "sessionId"`);
5066
5077
  if (typeof event.taskId !== "string") throw new Error(`"${event.kind}" event is missing "taskId"`);
5067
5078
  return event;
@@ -5134,6 +5145,47 @@ function unwrapSessionFrame(frame) {
5134
5145
  return { kind, payload: frame.subarray(1) };
5135
5146
  }
5136
5147
 
5148
+ // src/wire/base64url.ts
5149
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5150
+ var DECODE_MAP = {};
5151
+ for (let i = 0; i < ALPHABET.length; i++) DECODE_MAP[ALPHABET[i]] = i;
5152
+ function base64UrlEncode(bytes) {
5153
+ let result = "";
5154
+ let i = 0;
5155
+ for (; i + 2 < bytes.length; i += 3) {
5156
+ const chunk = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
5157
+ result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63] + ALPHABET[chunk >> 6 & 63] + ALPHABET[chunk & 63];
5158
+ }
5159
+ const remaining = bytes.length - i;
5160
+ if (remaining === 1) {
5161
+ const chunk = bytes[i] << 16;
5162
+ result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63];
5163
+ } else if (remaining === 2) {
5164
+ const chunk = bytes[i] << 16 | bytes[i + 1] << 8;
5165
+ result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63] + ALPHABET[chunk >> 6 & 63];
5166
+ }
5167
+ return result;
5168
+ }
5169
+ function base64UrlDecode(encoded) {
5170
+ const byteLength = Math.floor(encoded.length * 6 / 8);
5171
+ const bytes = new Uint8Array(byteLength);
5172
+ let bitBuffer = 0;
5173
+ let bitCount = 0;
5174
+ let outputIndex = 0;
5175
+ for (const char of encoded) {
5176
+ const value = DECODE_MAP[char];
5177
+ if (value === void 0) throw new Error(`Invalid base64url character: "${char}"`);
5178
+ bitBuffer = bitBuffer << 6 | value;
5179
+ bitCount += 6;
5180
+ if (bitCount >= 8) {
5181
+ bitCount -= 8;
5182
+ bytes[outputIndex] = bitBuffer >> bitCount & 255;
5183
+ outputIndex += 1;
5184
+ }
5185
+ }
5186
+ return bytes;
5187
+ }
5188
+
5137
5189
  // src/events/payloads.ts
5138
5190
  var DIFF_FILE_STATUSES = ["A", "M", "D", "R", "C", "U"];
5139
5191
  var TRANSCRIPT_SYSTEM_SUBTYPES = ["compaction", "command", "command_output", "session_boundary"];
@@ -5199,6 +5251,14 @@ function parseTranscriptEventPayload(payload) {
5199
5251
  });
5200
5252
  return { mode: "delta", revision, totalEntries, upserts };
5201
5253
  }
5254
+ function parseTerminalDimensionsWire(payload) {
5255
+ if (!isRecord(payload)) throw new Error("terminal dimensions must be an object");
5256
+ const cols = requireNumber(payload, "cols", "terminal dimensions");
5257
+ const rows = requireNumber(payload, "rows", "terminal dimensions");
5258
+ if (!Number.isInteger(cols) || cols < 2 || cols > 1e3) throw new Error('terminal dimensions have an invalid "cols"');
5259
+ if (!Number.isInteger(rows) || rows < 1 || rows > 1e3) throw new Error('terminal dimensions have an invalid "rows"');
5260
+ return { cols, rows };
5261
+ }
5202
5262
  function isActivityStateWire(value) {
5203
5263
  return value === "thinking" || value === "idle" || value === "permission";
5204
5264
  }
@@ -5359,6 +5419,7 @@ function parseDiffFileContentWire(payload) {
5359
5419
  }
5360
5420
 
5361
5421
  // src/wire/payloads.ts
5422
+ var READ_STREAM_SESSION_STATUSES = ["running", "queued", "suspended", "exited"];
5362
5423
  function parseReadStreamResponsePayload(payload) {
5363
5424
  if (!isRecord(payload)) throw new Error("read-stream response must be an object");
5364
5425
  if (typeof payload.scrollback !== "string") throw new Error('read-stream response is missing "scrollback"');
@@ -5370,12 +5431,31 @@ function parseReadStreamResponsePayload(payload) {
5370
5431
  if (payload.awaitedPromptId !== null && typeof payload.awaitedPromptId !== "string") {
5371
5432
  throw new Error('read-stream response has an invalid "awaitedPromptId"');
5372
5433
  }
5373
- return {
5434
+ const response = {
5374
5435
  scrollback: payload.scrollback,
5375
5436
  activity: { state: state ?? null, reason: reason ?? null },
5376
5437
  usage: payload.usage === null || payload.usage === void 0 ? null : parseSessionUsageWire(payload.usage),
5377
5438
  awaitedPromptId: payload.awaitedPromptId ?? null
5378
5439
  };
5440
+ if (payload.awaitedPromptOptions !== void 0) {
5441
+ if (payload.awaitedPromptOptions === null) {
5442
+ response.awaitedPromptOptions = null;
5443
+ } else if (Array.isArray(payload.awaitedPromptOptions) && payload.awaitedPromptOptions.every((option) => typeof option === "string")) {
5444
+ response.awaitedPromptOptions = payload.awaitedPromptOptions;
5445
+ } else {
5446
+ throw new Error('read-stream response has an invalid "awaitedPromptOptions"');
5447
+ }
5448
+ }
5449
+ if (payload.ptyDimensions !== void 0) {
5450
+ response.ptyDimensions = parseTerminalDimensionsWire(payload.ptyDimensions);
5451
+ }
5452
+ if (payload.sessionStatus !== void 0) {
5453
+ if (typeof payload.sessionStatus !== "string" || !READ_STREAM_SESSION_STATUSES.includes(payload.sessionStatus)) {
5454
+ throw new Error('read-stream response has an invalid "sessionStatus"');
5455
+ }
5456
+ response.sessionStatus = payload.sessionStatus;
5457
+ }
5458
+ return response;
5379
5459
  }
5380
5460
  function parseTranscriptWindowResponsePayload(payload) {
5381
5461
  if (!isRecord(payload)) throw new Error("transcript-window response must be an object");
@@ -5417,6 +5497,13 @@ function parseReadStreamRequestPayload(payload) {
5417
5497
  }
5418
5498
  return request;
5419
5499
  }
5500
+ var ACCENT_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
5501
+ function parseAccentColor(value, context) {
5502
+ if (typeof value !== "string" || !ACCENT_COLOR_PATTERN.test(value)) {
5503
+ throw new Error(`${context} has an invalid accent color (expected "#rrggbb")`);
5504
+ }
5505
+ return value;
5506
+ }
5420
5507
  function parseReadBoardResponsePayload(payload) {
5421
5508
  if (!isRecord(payload)) throw new Error("read-board response must be an object");
5422
5509
  if (Array.isArray(payload.projects)) {
@@ -5424,7 +5511,11 @@ function parseReadBoardResponsePayload(payload) {
5424
5511
  if (!isRecord(project) || typeof project.id !== "string" || typeof project.name !== "string") {
5425
5512
  throw new Error(`read-board project ${index} is malformed`);
5426
5513
  }
5427
- return { id: project.id, name: project.name };
5514
+ return {
5515
+ id: project.id,
5516
+ name: project.name,
5517
+ ...project.color !== void 0 ? { color: parseAccentColor(project.color, `read-board project ${index}`) } : {}
5518
+ };
5428
5519
  });
5429
5520
  return { projects };
5430
5521
  }
@@ -5432,11 +5523,16 @@ function parseReadBoardResponsePayload(payload) {
5432
5523
  if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
5433
5524
  if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
5434
5525
  if (!Array.isArray(payload.backlog)) throw new Error('read-board response is missing "backlog"');
5526
+ if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
5527
+ throw new Error('read-board response has a non-boolean "showTicketNumbers"');
5528
+ }
5435
5529
  return {
5436
5530
  projectId: payload.projectId,
5437
5531
  columns: payload.columns.map((column) => parseBoardColumnWire(column)),
5438
5532
  tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
5439
- backlog: payload.backlog.map((item) => parseBacklogItemWire(item))
5533
+ backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
5534
+ ...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
5535
+ ...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
5440
5536
  };
5441
5537
  }
5442
5538
  function parseReadBoardRequestPayload(payload) {
@@ -5503,8 +5599,21 @@ function parseAnswerPermissionPromptRequestPayload(payload) {
5503
5599
  function parseInteractiveTerminalRequestPayload(payload) {
5504
5600
  if (!isRecord(payload)) throw new Error("interactive-terminal payload must be an object");
5505
5601
  if (typeof payload.sessionId !== "string") throw new Error('interactive-terminal payload missing "sessionId"');
5506
- if (typeof payload.data !== "string") throw new Error('interactive-terminal payload missing "data"');
5507
- return { sessionId: payload.sessionId, data: payload.data };
5602
+ if (payload.action === void 0 || payload.action === "write") {
5603
+ if (typeof payload.data !== "string") throw new Error('interactive-terminal payload missing "data"');
5604
+ return { sessionId: payload.sessionId, action: "write", data: payload.data };
5605
+ }
5606
+ if (payload.action === "resize") {
5607
+ return {
5608
+ sessionId: payload.sessionId,
5609
+ action: "resize",
5610
+ dimensions: parseTerminalDimensionsWire(payload.dimensions)
5611
+ };
5612
+ }
5613
+ if (payload.action === "release-size") {
5614
+ return { sessionId: payload.sessionId, action: "release-size" };
5615
+ }
5616
+ throw new Error('interactive-terminal payload has an invalid "action"');
5508
5617
  }
5509
5618
  function parseBoardToolRequestPayload(payload) {
5510
5619
  if (!isRecord(payload)) throw new Error("board-tool payload must be an object");
@@ -5512,6 +5621,42 @@ function parseBoardToolRequestPayload(payload) {
5512
5621
  if (payload.params === void 0 || !isJsonValue(payload.params)) throw new Error('board-tool payload missing a JSON "params"');
5513
5622
  return { tool: payload.tool, params: payload.params };
5514
5623
  }
5624
+ var PUSH_KEY_LENGTH = 32;
5625
+ function parseRegisterPushRequestPayload(payload) {
5626
+ if (!isRecord(payload)) throw new Error("register-push payload must be an object");
5627
+ if (payload.action !== "register" && payload.action !== "unregister") {
5628
+ throw new Error('register-push payload has an invalid "action"');
5629
+ }
5630
+ const request = { action: payload.action };
5631
+ if (payload.expoPushToken !== void 0) {
5632
+ if (typeof payload.expoPushToken !== "string") throw new Error('register-push payload has a non-string "expoPushToken"');
5633
+ request.expoPushToken = payload.expoPushToken;
5634
+ }
5635
+ if (payload.pushKeyBase64 !== void 0) {
5636
+ if (typeof payload.pushKeyBase64 !== "string") throw new Error('register-push payload has a non-string "pushKeyBase64"');
5637
+ let decodedKey;
5638
+ try {
5639
+ decodedKey = base64UrlDecode(payload.pushKeyBase64);
5640
+ } catch {
5641
+ throw new Error('register-push payload has a malformed "pushKeyBase64"');
5642
+ }
5643
+ if (decodedKey.length !== PUSH_KEY_LENGTH) {
5644
+ throw new Error(`register-push payload "pushKeyBase64" must decode to ${PUSH_KEY_LENGTH} bytes`);
5645
+ }
5646
+ request.pushKeyBase64 = payload.pushKeyBase64;
5647
+ }
5648
+ if (payload.platform !== void 0) {
5649
+ if (payload.platform !== "android" && payload.platform !== "ios") {
5650
+ throw new Error('register-push payload has an invalid "platform"');
5651
+ }
5652
+ request.platform = payload.platform;
5653
+ }
5654
+ if (request.action === "register") {
5655
+ if (request.expoPushToken === void 0) throw new Error('register-push register payload missing "expoPushToken"');
5656
+ if (request.pushKeyBase64 === void 0) throw new Error('register-push register payload missing "pushKeyBase64"');
5657
+ }
5658
+ return request;
5659
+ }
5515
5660
  function parseCapabilityRequestPayload(verb, payload) {
5516
5661
  switch (verb) {
5517
5662
  case "read-stream":
@@ -5531,6 +5676,8 @@ function parseCapabilityRequestPayload(verb, payload) {
5531
5676
  case "board-tool-read":
5532
5677
  case "board-tool-write":
5533
5678
  return parseBoardToolRequestPayload(payload);
5679
+ case "register-push":
5680
+ return parseRegisterPushRequestPayload(payload);
5534
5681
  default: {
5535
5682
  const exhaustiveCheck = verb;
5536
5683
  throw new Error(`Unknown capability verb: ${String(exhaustiveCheck)}`);
@@ -5538,45 +5685,54 @@ function parseCapabilityRequestPayload(verb, payload) {
5538
5685
  }
5539
5686
  }
5540
5687
 
5541
- // src/wire/base64url.ts
5542
- var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5543
- var DECODE_MAP = {};
5544
- for (let i = 0; i < ALPHABET.length; i++) DECODE_MAP[ALPHABET[i]] = i;
5545
- function base64UrlEncode(bytes) {
5546
- let result = "";
5547
- let i = 0;
5548
- for (; i + 2 < bytes.length; i += 3) {
5549
- const chunk = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
5550
- result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63] + ALPHABET[chunk >> 6 & 63] + ALPHABET[chunk & 63];
5551
- }
5552
- const remaining = bytes.length - i;
5553
- if (remaining === 1) {
5554
- const chunk = bytes[i] << 16;
5555
- result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63];
5556
- } else if (remaining === 2) {
5557
- const chunk = bytes[i] << 16 | bytes[i + 1] << 8;
5558
- result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63] + ALPHABET[chunk >> 6 & 63];
5559
- }
5560
- return result;
5688
+ // src/crypto/push-envelope.ts
5689
+ var PUSH_CATEGORIES = ["permission-needed", "agent-question", "turn-complete", "session-failed"];
5690
+ function isPushCategory(value) {
5691
+ return typeof value === "string" && PUSH_CATEGORIES.includes(value);
5692
+ }
5693
+ var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5694
+ var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5695
+ function requireEnvelopeString(record, field) {
5696
+ const value = record[field];
5697
+ if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
5698
+ return value;
5561
5699
  }
5562
- function base64UrlDecode(encoded) {
5563
- const byteLength = Math.floor(encoded.length * 6 / 8);
5564
- const bytes = new Uint8Array(byteLength);
5565
- let bitBuffer = 0;
5566
- let bitCount = 0;
5567
- let outputIndex = 0;
5568
- for (const char of encoded) {
5569
- const value = DECODE_MAP[char];
5570
- if (value === void 0) throw new Error(`Invalid base64url character: "${char}"`);
5571
- bitBuffer = bitBuffer << 6 | value;
5572
- bitCount += 6;
5573
- if (bitCount >= 8) {
5574
- bitCount -= 8;
5575
- bytes[outputIndex] = bitBuffer >> bitCount & 255;
5576
- outputIndex += 1;
5577
- }
5700
+ function parsePushEnvelopePlaintext(value) {
5701
+ if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
5702
+ if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
5703
+ if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
5704
+ throw new Error('push envelope plaintext has an invalid "sentAt"');
5578
5705
  }
5579
- return bytes;
5706
+ return {
5707
+ category: value.category,
5708
+ projectId: requireEnvelopeString(value, "projectId"),
5709
+ taskId: requireEnvelopeString(value, "taskId"),
5710
+ sessionId: requireEnvelopeString(value, "sessionId"),
5711
+ taskTitle: requireEnvelopeString(value, "taskTitle"),
5712
+ detail: requireEnvelopeString(value, "detail"),
5713
+ sentAt: value.sentAt
5714
+ };
5715
+ }
5716
+ function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
5717
+ const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
5718
+ const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
5719
+ const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
5720
+ return base64UrlEncode(concatBytes4(nonce, ciphertext));
5721
+ }
5722
+ function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
5723
+ const bytes = base64UrlDecode(blob);
5724
+ if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
5725
+ throw new Error("push envelope blob is too short");
5726
+ }
5727
+ const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
5728
+ const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
5729
+ const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
5730
+ const decoded = JSON.parse(new TextDecoder().decode(decrypted));
5731
+ const plaintext = parsePushEnvelopePlaintext(decoded);
5732
+ const now = Date.now();
5733
+ if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
5734
+ if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
5735
+ return plaintext;
5580
5736
  }
5581
5737
 
5582
5738
  // src/pairing/qr.ts
@@ -5752,7 +5908,17 @@ function parseActivityEventPayload(payload) {
5752
5908
  case "permission": {
5753
5909
  if (typeof payload.promptId !== "string") throw new Error('permission payload is missing "promptId"');
5754
5910
  if (typeof payload.pending !== "boolean") throw new Error('permission payload is missing "pending"');
5755
- return { type: "permission", promptId: payload.promptId, pending: payload.pending };
5911
+ if (payload.options === void 0) {
5912
+ return { type: "permission", promptId: payload.promptId, pending: payload.pending };
5913
+ }
5914
+ if (!Array.isArray(payload.options) || !payload.options.every((option) => typeof option === "string")) {
5915
+ throw new Error('permission payload has an invalid "options"');
5916
+ }
5917
+ return { type: "permission", promptId: payload.promptId, pending: payload.pending, options: payload.options };
5918
+ }
5919
+ case "session-ended": {
5920
+ if (typeof payload.intentional !== "boolean") throw new Error('session-ended payload is missing "intentional"');
5921
+ return { type: "session-ended", intentional: payload.intentional };
5756
5922
  }
5757
5923
  default:
5758
5924
  throw new Error('activity payload has an unknown "type"');
@@ -5783,6 +5949,15 @@ function isBridgeEvent(value) {
5783
5949
  if (typeof value.sessionId !== "string" || typeof value.taskId !== "string") return false;
5784
5950
  return isRecord(value.payload) && typeof value.payload.data === "string";
5785
5951
  }
5952
+ case "terminal-resize": {
5953
+ if (typeof value.sessionId !== "string" || typeof value.taskId !== "string") return false;
5954
+ try {
5955
+ parseTerminalDimensionsWire(value.payload);
5956
+ return true;
5957
+ } catch {
5958
+ return false;
5959
+ }
5960
+ }
5786
5961
  case "board": {
5787
5962
  if (typeof value.projectId !== "string") return false;
5788
5963
  if (value.taskId !== void 0 && typeof value.taskId !== "string") return false;
@@ -5813,6 +5988,9 @@ function isBridgeEvent(value) {
5813
5988
  MAX_FRAME_LENGTH,
5814
5989
  PAIRING_URI_SCHEME,
5815
5990
  PROTOCOL_VERSION,
5991
+ PUSH_CATEGORIES,
5992
+ PUSH_ENVELOPE_MAX_AGE_MS,
5993
+ PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS,
5816
5994
  SecretstreamState,
5817
5995
  SessionFrameKind,
5818
5996
  X25519_KEY_LENGTH,
@@ -5843,8 +6021,10 @@ function isBridgeEvent(value) {
5843
6021
  isBridgeEvent,
5844
6022
  isCapabilityVerb,
5845
6023
  isJsonValue,
6024
+ isPushCategory,
5846
6025
  isRecord,
5847
6026
  isRosterEntryExpired,
6027
+ openPushEnvelope,
5848
6028
  parseActivityEventPayload,
5849
6029
  parseBacklogItemWire,
5850
6030
  parseBoardColumnWire,
@@ -5852,16 +6032,20 @@ function isBridgeEvent(value) {
5852
6032
  parseCapabilityRequestPayload,
5853
6033
  parseDiffFileContentWire,
5854
6034
  parseDiffFileListWire,
6035
+ parsePushEnvelopePlaintext,
5855
6036
  parseReadBoardResponsePayload,
5856
6037
  parseReadDiffResponsePayload,
5857
6038
  parseReadStreamResponsePayload,
6039
+ parseRegisterPushRequestPayload,
5858
6040
  parseSessionEventWire,
5859
6041
  parseSessionUsageWire,
6042
+ parseTerminalDimensionsWire,
5860
6043
  parseTranscriptEntriesWire,
5861
6044
  parseTranscriptEventPayload,
5862
6045
  parseTranscriptWindowResponsePayload,
5863
6046
  randomBytes,
5864
6047
  rosterDeviceCapabilitySet,
6048
+ sealPushEnvelope,
5865
6049
  signRosterEntry,
5866
6050
  unwrapSessionFrame,
5867
6051
  verifyRosterEntry,
package/dist/index.d.ts CHANGED
@@ -12,11 +12,12 @@ export type { JsonValue, BridgeMessage, HeartbeatMessage, CapabilityRequestMessa
12
12
  export { encodeMessage, decodeMessage, MAX_FRAME_LENGTH, MAX_DECODED_LENGTH, COMPRESSION_THRESHOLD } from './wire/framing';
13
13
  export { isJsonValue, isRecord } from './wire/json-value';
14
14
  export { SessionFrameKind, wrapSessionFrame, unwrapSessionFrame } from './wire/session-frame';
15
- export { parseCapabilityRequestPayload, parseReadStreamResponsePayload, parseReadBoardResponsePayload, parseReadDiffResponsePayload, parseTranscriptWindowResponsePayload, type CapabilityRequestPayloadMap, type CapabilityResponsePayloadMap, type ReadStreamRequestPayload, type ReadStreamResponsePayload, type TranscriptWindowResponsePayload, type ReadBoardRequestPayload, type ReadBoardProjectSummary, type ReadBoardProjectListResponsePayload, type ReadBoardSnapshotResponsePayload, type ReadBoardResponsePayload, type ReadDiffScope, type ReadDiffRequestPayload, type ReadDiffResponsePayload, type SendUserMessageRequestPayload, type SendUserMessageResponsePayload, type MoveTaskRequestPayload, type MoveTaskResponsePayload, type AnswerPermissionPromptRequestPayload, type AnswerPermissionPromptResponsePayload, type InteractiveTerminalRequestPayload, type InteractiveTerminalResponsePayload, type BoardToolRequestPayload, type BoardToolResponsePayload, } from './wire/payloads';
15
+ export { parseCapabilityRequestPayload, parseReadStreamResponsePayload, parseReadBoardResponsePayload, parseReadDiffResponsePayload, parseTranscriptWindowResponsePayload, parseRegisterPushRequestPayload, type CapabilityRequestPayloadMap, type CapabilityResponsePayloadMap, type ReadStreamRequestPayload, type ReadStreamResponsePayload, type ReadStreamSessionStatusWire, type TranscriptWindowResponsePayload, type ReadBoardRequestPayload, type ReadBoardProjectSummary, type ReadBoardProjectListResponsePayload, type ReadBoardSnapshotResponsePayload, type ReadBoardResponsePayload, type ReadDiffScope, type ReadDiffRequestPayload, type ReadDiffResponsePayload, type SendUserMessageRequestPayload, type SendUserMessageResponsePayload, type MoveTaskRequestPayload, type MoveTaskResponsePayload, type AnswerPermissionPromptRequestPayload, type AnswerPermissionPromptResponsePayload, type InteractiveTerminalRequestPayload, type InteractiveTerminalResponsePayload, type BoardToolRequestPayload, type BoardToolResponsePayload, type RegisterPushRequestPayload, type RegisterPushResponsePayload, } from './wire/payloads';
16
+ export { PUSH_CATEGORIES, isPushCategory, parsePushEnvelopePlaintext, sealPushEnvelope, openPushEnvelope, PUSH_ENVELOPE_MAX_AGE_MS, PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS, type PushCategory, type PushEnvelopePlaintext, } from './crypto/push-envelope';
16
17
  export { PAIRING_URI_SCHEME, encodePairingQrPayload, decodePairingQrPayload, type PairingQrPayload, } from './pairing/qr';
17
18
  export { signRosterEntry, verifyRosterEntry, encodeRosterEntryForSigning, isRosterEntryExpired, findRosterDevice, rosterDeviceCapabilitySet, capabilitySetToRosterCapabilities, type RosterDeviceEntry, type DeviceRoster, } from './roster/roster';
18
19
  export { CAPABILITY_VERBS, isCapabilityVerb, capabilitySetFromArray, capabilitySetToArray, type CapabilityVerb, type CapabilitySet, } from './capabilities/verbs';
19
20
  export { BOARD_TOOL_READ_NAMES, BOARD_TOOL_WRITE_NAMES, isBoardToolReadName, isBoardToolWriteName, type BoardToolReadName, type BoardToolWriteName, type BoardToolName, } from './capabilities/board-tools';
20
- export { isBridgeEvent, parseActivityEventPayload, type BridgeEvent, type TranscriptEvent, type ActivityEvent, type ActivityEventPayload, type TerminalEvent, type BoardEvent, type BoardEventPayload, type DiffEvent, } from './events/event';
21
- export { parseTranscriptEntriesWire, parseTranscriptEventPayload, type TranscriptEventPayload, type TranscriptUpsertWire, isActivityStateWire, isActivityReasonWire, parseSessionUsageWire, parseSessionEventWire, parseBoardColumnWire, parseBoardTaskWire, parseBacklogItemWire, parseDiffFileListWire, parseDiffFileContentWire, type TranscriptBlockWire, type TranscriptEntryWire, type TranscriptTurnUsageWire, type TranscriptSystemSubtypeWire, type ActivityStateWire, type ActivityReasonWire, type SessionUsageWire, type SessionEventWire, type BoardColumnWire, type BoardTaskWire, type BacklogItemWire, type DiffFileStatusWire, type DiffFileWire, type DiffFileListWire, type DiffFileContentWire, } from './events/payloads';
21
+ export { isBridgeEvent, parseActivityEventPayload, type BridgeEvent, type TranscriptEvent, type ActivityEvent, type ActivityEventPayload, type TerminalEvent, type TerminalResizeEvent, type BoardEvent, type BoardEventPayload, type DiffEvent, } from './events/event';
22
+ export { parseTranscriptEntriesWire, parseTranscriptEventPayload, type TranscriptEventPayload, type TranscriptUpsertWire, isActivityStateWire, isActivityReasonWire, parseSessionUsageWire, parseSessionEventWire, parseTerminalDimensionsWire, parseBoardColumnWire, parseBoardTaskWire, parseBacklogItemWire, parseDiffFileListWire, parseDiffFileContentWire, type TranscriptBlockWire, type TranscriptEntryWire, type TranscriptTurnUsageWire, type TranscriptSystemSubtypeWire, type ActivityStateWire, type ActivityReasonWire, type SessionUsageWire, type SessionEventWire, type TerminalDimensionsWire, type BoardColumnWire, type BoardTaskWire, type BacklogItemWire, type DiffFileStatusWire, type DiffFileWire, type DiffFileListWire, type DiffFileContentWire, } from './events/payloads';
22
23
  export type { Transport, TransportState, Unsubscribe } from './transport/transport';
package/dist/index.mjs CHANGED
@@ -4873,7 +4873,8 @@ var CAPABILITY_VERBS = [
4873
4873
  "answer-permission-prompt",
4874
4874
  "interactive-terminal",
4875
4875
  "board-tool-read",
4876
- "board-tool-write"
4876
+ "board-tool-write",
4877
+ "register-push"
4877
4878
  ];
4878
4879
  function isCapabilityVerb(value) {
4879
4880
  return CAPABILITY_VERBS.includes(value);
@@ -4959,7 +4960,8 @@ function validateEvent(event) {
4959
4960
  switch (event.kind) {
4960
4961
  case "transcript":
4961
4962
  case "activity":
4962
- case "terminal": {
4963
+ case "terminal":
4964
+ case "terminal-resize": {
4963
4965
  if (typeof event.sessionId !== "string") throw new Error(`"${event.kind}" event is missing "sessionId"`);
4964
4966
  if (typeof event.taskId !== "string") throw new Error(`"${event.kind}" event is missing "taskId"`);
4965
4967
  return event;
@@ -5032,6 +5034,47 @@ function unwrapSessionFrame(frame) {
5032
5034
  return { kind, payload: frame.subarray(1) };
5033
5035
  }
5034
5036
 
5037
+ // src/wire/base64url.ts
5038
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5039
+ var DECODE_MAP = {};
5040
+ for (let i = 0; i < ALPHABET.length; i++) DECODE_MAP[ALPHABET[i]] = i;
5041
+ function base64UrlEncode(bytes) {
5042
+ let result = "";
5043
+ let i = 0;
5044
+ for (; i + 2 < bytes.length; i += 3) {
5045
+ const chunk = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
5046
+ result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63] + ALPHABET[chunk >> 6 & 63] + ALPHABET[chunk & 63];
5047
+ }
5048
+ const remaining = bytes.length - i;
5049
+ if (remaining === 1) {
5050
+ const chunk = bytes[i] << 16;
5051
+ result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63];
5052
+ } else if (remaining === 2) {
5053
+ const chunk = bytes[i] << 16 | bytes[i + 1] << 8;
5054
+ result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63] + ALPHABET[chunk >> 6 & 63];
5055
+ }
5056
+ return result;
5057
+ }
5058
+ function base64UrlDecode(encoded) {
5059
+ const byteLength = Math.floor(encoded.length * 6 / 8);
5060
+ const bytes = new Uint8Array(byteLength);
5061
+ let bitBuffer = 0;
5062
+ let bitCount = 0;
5063
+ let outputIndex = 0;
5064
+ for (const char of encoded) {
5065
+ const value = DECODE_MAP[char];
5066
+ if (value === void 0) throw new Error(`Invalid base64url character: "${char}"`);
5067
+ bitBuffer = bitBuffer << 6 | value;
5068
+ bitCount += 6;
5069
+ if (bitCount >= 8) {
5070
+ bitCount -= 8;
5071
+ bytes[outputIndex] = bitBuffer >> bitCount & 255;
5072
+ outputIndex += 1;
5073
+ }
5074
+ }
5075
+ return bytes;
5076
+ }
5077
+
5035
5078
  // src/events/payloads.ts
5036
5079
  var DIFF_FILE_STATUSES = ["A", "M", "D", "R", "C", "U"];
5037
5080
  var TRANSCRIPT_SYSTEM_SUBTYPES = ["compaction", "command", "command_output", "session_boundary"];
@@ -5097,6 +5140,14 @@ function parseTranscriptEventPayload(payload) {
5097
5140
  });
5098
5141
  return { mode: "delta", revision, totalEntries, upserts };
5099
5142
  }
5143
+ function parseTerminalDimensionsWire(payload) {
5144
+ if (!isRecord(payload)) throw new Error("terminal dimensions must be an object");
5145
+ const cols = requireNumber(payload, "cols", "terminal dimensions");
5146
+ const rows = requireNumber(payload, "rows", "terminal dimensions");
5147
+ if (!Number.isInteger(cols) || cols < 2 || cols > 1e3) throw new Error('terminal dimensions have an invalid "cols"');
5148
+ if (!Number.isInteger(rows) || rows < 1 || rows > 1e3) throw new Error('terminal dimensions have an invalid "rows"');
5149
+ return { cols, rows };
5150
+ }
5100
5151
  function isActivityStateWire(value) {
5101
5152
  return value === "thinking" || value === "idle" || value === "permission";
5102
5153
  }
@@ -5257,6 +5308,7 @@ function parseDiffFileContentWire(payload) {
5257
5308
  }
5258
5309
 
5259
5310
  // src/wire/payloads.ts
5311
+ var READ_STREAM_SESSION_STATUSES = ["running", "queued", "suspended", "exited"];
5260
5312
  function parseReadStreamResponsePayload(payload) {
5261
5313
  if (!isRecord(payload)) throw new Error("read-stream response must be an object");
5262
5314
  if (typeof payload.scrollback !== "string") throw new Error('read-stream response is missing "scrollback"');
@@ -5268,12 +5320,31 @@ function parseReadStreamResponsePayload(payload) {
5268
5320
  if (payload.awaitedPromptId !== null && typeof payload.awaitedPromptId !== "string") {
5269
5321
  throw new Error('read-stream response has an invalid "awaitedPromptId"');
5270
5322
  }
5271
- return {
5323
+ const response = {
5272
5324
  scrollback: payload.scrollback,
5273
5325
  activity: { state: state ?? null, reason: reason ?? null },
5274
5326
  usage: payload.usage === null || payload.usage === void 0 ? null : parseSessionUsageWire(payload.usage),
5275
5327
  awaitedPromptId: payload.awaitedPromptId ?? null
5276
5328
  };
5329
+ if (payload.awaitedPromptOptions !== void 0) {
5330
+ if (payload.awaitedPromptOptions === null) {
5331
+ response.awaitedPromptOptions = null;
5332
+ } else if (Array.isArray(payload.awaitedPromptOptions) && payload.awaitedPromptOptions.every((option) => typeof option === "string")) {
5333
+ response.awaitedPromptOptions = payload.awaitedPromptOptions;
5334
+ } else {
5335
+ throw new Error('read-stream response has an invalid "awaitedPromptOptions"');
5336
+ }
5337
+ }
5338
+ if (payload.ptyDimensions !== void 0) {
5339
+ response.ptyDimensions = parseTerminalDimensionsWire(payload.ptyDimensions);
5340
+ }
5341
+ if (payload.sessionStatus !== void 0) {
5342
+ if (typeof payload.sessionStatus !== "string" || !READ_STREAM_SESSION_STATUSES.includes(payload.sessionStatus)) {
5343
+ throw new Error('read-stream response has an invalid "sessionStatus"');
5344
+ }
5345
+ response.sessionStatus = payload.sessionStatus;
5346
+ }
5347
+ return response;
5277
5348
  }
5278
5349
  function parseTranscriptWindowResponsePayload(payload) {
5279
5350
  if (!isRecord(payload)) throw new Error("transcript-window response must be an object");
@@ -5315,6 +5386,13 @@ function parseReadStreamRequestPayload(payload) {
5315
5386
  }
5316
5387
  return request;
5317
5388
  }
5389
+ var ACCENT_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
5390
+ function parseAccentColor(value, context) {
5391
+ if (typeof value !== "string" || !ACCENT_COLOR_PATTERN.test(value)) {
5392
+ throw new Error(`${context} has an invalid accent color (expected "#rrggbb")`);
5393
+ }
5394
+ return value;
5395
+ }
5318
5396
  function parseReadBoardResponsePayload(payload) {
5319
5397
  if (!isRecord(payload)) throw new Error("read-board response must be an object");
5320
5398
  if (Array.isArray(payload.projects)) {
@@ -5322,7 +5400,11 @@ function parseReadBoardResponsePayload(payload) {
5322
5400
  if (!isRecord(project) || typeof project.id !== "string" || typeof project.name !== "string") {
5323
5401
  throw new Error(`read-board project ${index} is malformed`);
5324
5402
  }
5325
- return { id: project.id, name: project.name };
5403
+ return {
5404
+ id: project.id,
5405
+ name: project.name,
5406
+ ...project.color !== void 0 ? { color: parseAccentColor(project.color, `read-board project ${index}`) } : {}
5407
+ };
5326
5408
  });
5327
5409
  return { projects };
5328
5410
  }
@@ -5330,11 +5412,16 @@ function parseReadBoardResponsePayload(payload) {
5330
5412
  if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
5331
5413
  if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
5332
5414
  if (!Array.isArray(payload.backlog)) throw new Error('read-board response is missing "backlog"');
5415
+ if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
5416
+ throw new Error('read-board response has a non-boolean "showTicketNumbers"');
5417
+ }
5333
5418
  return {
5334
5419
  projectId: payload.projectId,
5335
5420
  columns: payload.columns.map((column) => parseBoardColumnWire(column)),
5336
5421
  tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
5337
- backlog: payload.backlog.map((item) => parseBacklogItemWire(item))
5422
+ backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
5423
+ ...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
5424
+ ...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
5338
5425
  };
5339
5426
  }
5340
5427
  function parseReadBoardRequestPayload(payload) {
@@ -5401,8 +5488,21 @@ function parseAnswerPermissionPromptRequestPayload(payload) {
5401
5488
  function parseInteractiveTerminalRequestPayload(payload) {
5402
5489
  if (!isRecord(payload)) throw new Error("interactive-terminal payload must be an object");
5403
5490
  if (typeof payload.sessionId !== "string") throw new Error('interactive-terminal payload missing "sessionId"');
5404
- if (typeof payload.data !== "string") throw new Error('interactive-terminal payload missing "data"');
5405
- return { sessionId: payload.sessionId, data: payload.data };
5491
+ if (payload.action === void 0 || payload.action === "write") {
5492
+ if (typeof payload.data !== "string") throw new Error('interactive-terminal payload missing "data"');
5493
+ return { sessionId: payload.sessionId, action: "write", data: payload.data };
5494
+ }
5495
+ if (payload.action === "resize") {
5496
+ return {
5497
+ sessionId: payload.sessionId,
5498
+ action: "resize",
5499
+ dimensions: parseTerminalDimensionsWire(payload.dimensions)
5500
+ };
5501
+ }
5502
+ if (payload.action === "release-size") {
5503
+ return { sessionId: payload.sessionId, action: "release-size" };
5504
+ }
5505
+ throw new Error('interactive-terminal payload has an invalid "action"');
5406
5506
  }
5407
5507
  function parseBoardToolRequestPayload(payload) {
5408
5508
  if (!isRecord(payload)) throw new Error("board-tool payload must be an object");
@@ -5410,6 +5510,42 @@ function parseBoardToolRequestPayload(payload) {
5410
5510
  if (payload.params === void 0 || !isJsonValue(payload.params)) throw new Error('board-tool payload missing a JSON "params"');
5411
5511
  return { tool: payload.tool, params: payload.params };
5412
5512
  }
5513
+ var PUSH_KEY_LENGTH = 32;
5514
+ function parseRegisterPushRequestPayload(payload) {
5515
+ if (!isRecord(payload)) throw new Error("register-push payload must be an object");
5516
+ if (payload.action !== "register" && payload.action !== "unregister") {
5517
+ throw new Error('register-push payload has an invalid "action"');
5518
+ }
5519
+ const request = { action: payload.action };
5520
+ if (payload.expoPushToken !== void 0) {
5521
+ if (typeof payload.expoPushToken !== "string") throw new Error('register-push payload has a non-string "expoPushToken"');
5522
+ request.expoPushToken = payload.expoPushToken;
5523
+ }
5524
+ if (payload.pushKeyBase64 !== void 0) {
5525
+ if (typeof payload.pushKeyBase64 !== "string") throw new Error('register-push payload has a non-string "pushKeyBase64"');
5526
+ let decodedKey;
5527
+ try {
5528
+ decodedKey = base64UrlDecode(payload.pushKeyBase64);
5529
+ } catch {
5530
+ throw new Error('register-push payload has a malformed "pushKeyBase64"');
5531
+ }
5532
+ if (decodedKey.length !== PUSH_KEY_LENGTH) {
5533
+ throw new Error(`register-push payload "pushKeyBase64" must decode to ${PUSH_KEY_LENGTH} bytes`);
5534
+ }
5535
+ request.pushKeyBase64 = payload.pushKeyBase64;
5536
+ }
5537
+ if (payload.platform !== void 0) {
5538
+ if (payload.platform !== "android" && payload.platform !== "ios") {
5539
+ throw new Error('register-push payload has an invalid "platform"');
5540
+ }
5541
+ request.platform = payload.platform;
5542
+ }
5543
+ if (request.action === "register") {
5544
+ if (request.expoPushToken === void 0) throw new Error('register-push register payload missing "expoPushToken"');
5545
+ if (request.pushKeyBase64 === void 0) throw new Error('register-push register payload missing "pushKeyBase64"');
5546
+ }
5547
+ return request;
5548
+ }
5413
5549
  function parseCapabilityRequestPayload(verb, payload) {
5414
5550
  switch (verb) {
5415
5551
  case "read-stream":
@@ -5429,6 +5565,8 @@ function parseCapabilityRequestPayload(verb, payload) {
5429
5565
  case "board-tool-read":
5430
5566
  case "board-tool-write":
5431
5567
  return parseBoardToolRequestPayload(payload);
5568
+ case "register-push":
5569
+ return parseRegisterPushRequestPayload(payload);
5432
5570
  default: {
5433
5571
  const exhaustiveCheck = verb;
5434
5572
  throw new Error(`Unknown capability verb: ${String(exhaustiveCheck)}`);
@@ -5436,45 +5574,54 @@ function parseCapabilityRequestPayload(verb, payload) {
5436
5574
  }
5437
5575
  }
5438
5576
 
5439
- // src/wire/base64url.ts
5440
- var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5441
- var DECODE_MAP = {};
5442
- for (let i = 0; i < ALPHABET.length; i++) DECODE_MAP[ALPHABET[i]] = i;
5443
- function base64UrlEncode(bytes) {
5444
- let result = "";
5445
- let i = 0;
5446
- for (; i + 2 < bytes.length; i += 3) {
5447
- const chunk = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
5448
- result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63] + ALPHABET[chunk >> 6 & 63] + ALPHABET[chunk & 63];
5449
- }
5450
- const remaining = bytes.length - i;
5451
- if (remaining === 1) {
5452
- const chunk = bytes[i] << 16;
5453
- result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63];
5454
- } else if (remaining === 2) {
5455
- const chunk = bytes[i] << 16 | bytes[i + 1] << 8;
5456
- result += ALPHABET[chunk >> 18 & 63] + ALPHABET[chunk >> 12 & 63] + ALPHABET[chunk >> 6 & 63];
5457
- }
5458
- return result;
5577
+ // src/crypto/push-envelope.ts
5578
+ var PUSH_CATEGORIES = ["permission-needed", "agent-question", "turn-complete", "session-failed"];
5579
+ function isPushCategory(value) {
5580
+ return typeof value === "string" && PUSH_CATEGORIES.includes(value);
5581
+ }
5582
+ var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5583
+ var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5584
+ function requireEnvelopeString(record, field) {
5585
+ const value = record[field];
5586
+ if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
5587
+ return value;
5459
5588
  }
5460
- function base64UrlDecode(encoded) {
5461
- const byteLength = Math.floor(encoded.length * 6 / 8);
5462
- const bytes = new Uint8Array(byteLength);
5463
- let bitBuffer = 0;
5464
- let bitCount = 0;
5465
- let outputIndex = 0;
5466
- for (const char of encoded) {
5467
- const value = DECODE_MAP[char];
5468
- if (value === void 0) throw new Error(`Invalid base64url character: "${char}"`);
5469
- bitBuffer = bitBuffer << 6 | value;
5470
- bitCount += 6;
5471
- if (bitCount >= 8) {
5472
- bitCount -= 8;
5473
- bytes[outputIndex] = bitBuffer >> bitCount & 255;
5474
- outputIndex += 1;
5475
- }
5589
+ function parsePushEnvelopePlaintext(value) {
5590
+ if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
5591
+ if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
5592
+ if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
5593
+ throw new Error('push envelope plaintext has an invalid "sentAt"');
5476
5594
  }
5477
- return bytes;
5595
+ return {
5596
+ category: value.category,
5597
+ projectId: requireEnvelopeString(value, "projectId"),
5598
+ taskId: requireEnvelopeString(value, "taskId"),
5599
+ sessionId: requireEnvelopeString(value, "sessionId"),
5600
+ taskTitle: requireEnvelopeString(value, "taskTitle"),
5601
+ detail: requireEnvelopeString(value, "detail"),
5602
+ sentAt: value.sentAt
5603
+ };
5604
+ }
5605
+ function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
5606
+ const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
5607
+ const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
5608
+ const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
5609
+ return base64UrlEncode(concatBytes4(nonce, ciphertext));
5610
+ }
5611
+ function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
5612
+ const bytes = base64UrlDecode(blob);
5613
+ if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
5614
+ throw new Error("push envelope blob is too short");
5615
+ }
5616
+ const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
5617
+ const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
5618
+ const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
5619
+ const decoded = JSON.parse(new TextDecoder().decode(decrypted));
5620
+ const plaintext = parsePushEnvelopePlaintext(decoded);
5621
+ const now = Date.now();
5622
+ if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
5623
+ if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
5624
+ return plaintext;
5478
5625
  }
5479
5626
 
5480
5627
  // src/pairing/qr.ts
@@ -5650,7 +5797,17 @@ function parseActivityEventPayload(payload) {
5650
5797
  case "permission": {
5651
5798
  if (typeof payload.promptId !== "string") throw new Error('permission payload is missing "promptId"');
5652
5799
  if (typeof payload.pending !== "boolean") throw new Error('permission payload is missing "pending"');
5653
- return { type: "permission", promptId: payload.promptId, pending: payload.pending };
5800
+ if (payload.options === void 0) {
5801
+ return { type: "permission", promptId: payload.promptId, pending: payload.pending };
5802
+ }
5803
+ if (!Array.isArray(payload.options) || !payload.options.every((option) => typeof option === "string")) {
5804
+ throw new Error('permission payload has an invalid "options"');
5805
+ }
5806
+ return { type: "permission", promptId: payload.promptId, pending: payload.pending, options: payload.options };
5807
+ }
5808
+ case "session-ended": {
5809
+ if (typeof payload.intentional !== "boolean") throw new Error('session-ended payload is missing "intentional"');
5810
+ return { type: "session-ended", intentional: payload.intentional };
5654
5811
  }
5655
5812
  default:
5656
5813
  throw new Error('activity payload has an unknown "type"');
@@ -5681,6 +5838,15 @@ function isBridgeEvent(value) {
5681
5838
  if (typeof value.sessionId !== "string" || typeof value.taskId !== "string") return false;
5682
5839
  return isRecord(value.payload) && typeof value.payload.data === "string";
5683
5840
  }
5841
+ case "terminal-resize": {
5842
+ if (typeof value.sessionId !== "string" || typeof value.taskId !== "string") return false;
5843
+ try {
5844
+ parseTerminalDimensionsWire(value.payload);
5845
+ return true;
5846
+ } catch {
5847
+ return false;
5848
+ }
5849
+ }
5684
5850
  case "board": {
5685
5851
  if (typeof value.projectId !== "string") return false;
5686
5852
  if (value.taskId !== void 0 && typeof value.taskId !== "string") return false;
@@ -5710,6 +5876,9 @@ export {
5710
5876
  MAX_FRAME_LENGTH,
5711
5877
  PAIRING_URI_SCHEME,
5712
5878
  PROTOCOL_VERSION,
5879
+ PUSH_CATEGORIES,
5880
+ PUSH_ENVELOPE_MAX_AGE_MS,
5881
+ PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS,
5713
5882
  SecretstreamState,
5714
5883
  SessionFrameKind,
5715
5884
  X25519_KEY_LENGTH,
@@ -5740,8 +5909,10 @@ export {
5740
5909
  isBridgeEvent,
5741
5910
  isCapabilityVerb,
5742
5911
  isJsonValue,
5912
+ isPushCategory,
5743
5913
  isRecord,
5744
5914
  isRosterEntryExpired,
5915
+ openPushEnvelope,
5745
5916
  parseActivityEventPayload,
5746
5917
  parseBacklogItemWire,
5747
5918
  parseBoardColumnWire,
@@ -5749,16 +5920,20 @@ export {
5749
5920
  parseCapabilityRequestPayload,
5750
5921
  parseDiffFileContentWire,
5751
5922
  parseDiffFileListWire,
5923
+ parsePushEnvelopePlaintext,
5752
5924
  parseReadBoardResponsePayload,
5753
5925
  parseReadDiffResponsePayload,
5754
5926
  parseReadStreamResponsePayload,
5927
+ parseRegisterPushRequestPayload,
5755
5928
  parseSessionEventWire,
5756
5929
  parseSessionUsageWire,
5930
+ parseTerminalDimensionsWire,
5757
5931
  parseTranscriptEntriesWire,
5758
5932
  parseTranscriptEventPayload,
5759
5933
  parseTranscriptWindowResponsePayload,
5760
5934
  randomBytes4 as randomBytes,
5761
5935
  rosterDeviceCapabilitySet,
5936
+ sealPushEnvelope,
5762
5937
  signRosterEntry,
5763
5938
  unwrapSessionFrame,
5764
5939
  verifyRosterEntry,
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import type { CapabilityVerb } from '../capabilities/verbs';
17
17
  import type { JsonValue } from './messages';
18
- import { type ActivityReasonWire, type ActivityStateWire, type BacklogItemWire, type BoardColumnWire, type BoardTaskWire, type DiffFileContentWire, type DiffFileListWire, type SessionUsageWire, type TranscriptEntryWire } from '../events/payloads';
18
+ import { type ActivityReasonWire, type ActivityStateWire, type BacklogItemWire, type BoardColumnWire, type BoardTaskWire, type DiffFileContentWire, type DiffFileListWire, type SessionUsageWire, type TerminalDimensionsWire, type TranscriptEntryWire } from '../events/payloads';
19
19
  export interface ReadStreamRequestPayload {
20
20
  sessionId: string;
21
21
  /**
@@ -32,6 +32,12 @@ export interface ReadStreamRequestPayload {
32
32
  /** transcript-window only: maximum entries wanted (the desktop may cap this and may return fewer). */
33
33
  limit?: number;
34
34
  }
35
+ /**
36
+ * Mirrors the desktop's SessionStatus. 'suspended' is a registered-but-
37
+ * parked session (resumable placeholder); 'exited' can appear when a
38
+ * snapshot races the session's teardown.
39
+ */
40
+ export type ReadStreamSessionStatusWire = 'running' | 'queued' | 'suspended' | 'exited';
35
41
  /** Initial snapshot returned on subscribe; live updates arrive as TerminalEvent/ActivityEvent/TranscriptEvent. */
36
42
  export interface ReadStreamResponsePayload {
37
43
  scrollback: string;
@@ -42,6 +48,26 @@ export interface ReadStreamResponsePayload {
42
48
  usage: SessionUsageWire | null;
43
49
  /** The live outstanding permission-prompt id (see answer-permission-prompt), or null when none is pending. */
44
50
  awaitedPromptId: string | null;
51
+ /**
52
+ * The awaited prompt's numbered option labels, parsed by the desktop from
53
+ * the pending dialog's PTY frame, in keystroke order: options[0] is the
54
+ * row answered with "1\r", options[1] with "2\r", and so on. Absent from
55
+ * pre-0.6.0 desktops; absent or null means unknown (no numbered dialog
56
+ * could be parsed), and the phone falls back to its blind
57
+ * approve/deny keystrokes. Only meaningful while awaitedPromptId is set.
58
+ */
59
+ awaitedPromptOptions?: string[] | null;
60
+ /**
61
+ * The PTY grid the scrollback bytes are laid out for. Absent from
62
+ * pre-0.4.0 desktops; the phone then falls back to inferring a width
63
+ * from the scrollback content.
64
+ */
65
+ ptyDimensions?: TerminalDimensionsWire;
66
+ /**
67
+ * The session's lifecycle status at snapshot time. Absent from
68
+ * pre-0.5.0 desktops; the phone then assumes 'running'.
69
+ */
70
+ sessionStatus?: ReadStreamSessionStatusWire;
45
71
  }
46
72
  /** Phone-side narrowing of a read-stream subscribe response. Throws on a malformed required field. */
47
73
  export declare function parseReadStreamResponsePayload(payload: JsonValue): ReadStreamResponsePayload;
@@ -66,6 +92,13 @@ export interface ReadBoardRequestPayload {
66
92
  export interface ReadBoardProjectSummary {
67
93
  id: string;
68
94
  name: string;
95
+ /**
96
+ * Accent color for this project ("#rrggbb"). Today the desktop derives
97
+ * it deterministically from the project id; a user-set project color
98
+ * can later override it through this same field. Absent from pre-0.5.0
99
+ * desktops.
100
+ */
101
+ color?: string;
69
102
  }
70
103
  /** Returned when the request omits projectId - the phone's project-bootstrap listing. */
71
104
  export interface ReadBoardProjectListResponsePayload {
@@ -77,6 +110,14 @@ export interface ReadBoardSnapshotResponsePayload {
77
110
  columns: BoardColumnWire[];
78
111
  tasks: BoardTaskWire[];
79
112
  backlog: BacklogItemWire[];
113
+ /** The project's accent color ("#rrggbb"); same semantics as ReadBoardProjectSummary.color. Absent from pre-0.5.0 desktops. */
114
+ projectColor?: string;
115
+ /**
116
+ * The desktop's Layout "Ticket Numbers" setting: whether task cards
117
+ * display their #N number. Absent from pre-0.6.0 desktops (treat as
118
+ * true, the desktop default).
119
+ */
120
+ showTicketNumbers?: boolean;
80
121
  }
81
122
  export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload;
82
123
  /** Phone-side narrowing of a read-board response (project list or board snapshot). Throws on a malformed required field. */
@@ -120,13 +161,38 @@ export interface AnswerPermissionPromptRequestPayload {
120
161
  export interface AnswerPermissionPromptResponsePayload {
121
162
  answered: boolean;
122
163
  }
123
- export interface InteractiveTerminalRequestPayload {
164
+ /**
165
+ * Three actions ride the one interactive-terminal grant: a device trusted
166
+ * to type raw bytes into the PTY is equally trusted to resize it, so
167
+ * resize adds no new capability surface.
168
+ *
169
+ * - write (the default when `action` is omitted, which is what every
170
+ * pre-0.4.0 phone sends): raw bytes into the PTY.
171
+ * - resize: set the PTY grid so the TUI redraws phone-shaped. The desktop
172
+ * remembers its own last dimensions and restores them when the phone
173
+ * releases (explicitly, or implicitly on disconnect/revoke).
174
+ * - release-size: give the grid back to the desktop now.
175
+ */
176
+ export type InteractiveTerminalRequestPayload = {
124
177
  sessionId: string;
178
+ action?: 'write';
125
179
  data: string;
126
- }
127
- export interface InteractiveTerminalResponsePayload {
180
+ } | {
181
+ sessionId: string;
182
+ action: 'resize';
183
+ dimensions: TerminalDimensionsWire;
184
+ } | {
185
+ sessionId: string;
186
+ action: 'release-size';
187
+ };
188
+ export type InteractiveTerminalResponsePayload = {
128
189
  written: boolean;
129
- }
190
+ } | {
191
+ resized: boolean;
192
+ colsChanged: boolean;
193
+ } | {
194
+ released: boolean;
195
+ };
130
196
  export interface BoardToolRequestPayload {
131
197
  tool: string;
132
198
  params: JsonValue;
@@ -134,6 +200,24 @@ export interface BoardToolRequestPayload {
134
200
  export interface BoardToolResponsePayload {
135
201
  result: JsonValue;
136
202
  }
203
+ /**
204
+ * Registers (or unregisters) this device for E2E-encrypted push
205
+ * notifications. 'register' carries the device's Expo push token plus a
206
+ * device-generated 32-byte push key (base64url) the desktop seals every
207
+ * notification envelope with (see crypto/push-envelope.ts); 'unregister'
208
+ * carries neither. The desktop keys the registration by the requesting
209
+ * device's roster identity, never by anything in this payload.
210
+ */
211
+ export interface RegisterPushRequestPayload {
212
+ action: 'register' | 'unregister';
213
+ expoPushToken?: string;
214
+ pushKeyBase64?: string;
215
+ platform?: 'android' | 'ios';
216
+ }
217
+ export interface RegisterPushResponsePayload {
218
+ registered: boolean;
219
+ }
220
+ export declare function parseRegisterPushRequestPayload(payload: JsonValue): RegisterPushRequestPayload;
137
221
  export interface CapabilityRequestPayloadMap {
138
222
  'read-stream': ReadStreamRequestPayload;
139
223
  'read-board': ReadBoardRequestPayload;
@@ -144,6 +228,7 @@ export interface CapabilityRequestPayloadMap {
144
228
  'interactive-terminal': InteractiveTerminalRequestPayload;
145
229
  'board-tool-read': BoardToolRequestPayload;
146
230
  'board-tool-write': BoardToolRequestPayload;
231
+ 'register-push': RegisterPushRequestPayload;
147
232
  }
148
233
  export interface CapabilityResponsePayloadMap {
149
234
  'read-stream': ReadStreamResponsePayload | TranscriptWindowResponsePayload;
@@ -155,6 +240,7 @@ export interface CapabilityResponsePayloadMap {
155
240
  'interactive-terminal': InteractiveTerminalResponsePayload;
156
241
  'board-tool-read': BoardToolResponsePayload;
157
242
  'board-tool-write': BoardToolResponsePayload;
243
+ 'register-push': RegisterPushResponsePayload;
158
244
  }
159
245
  /**
160
246
  * Validates and narrows a decoded capability-request's generic JsonValue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangentic/protocol",
3
- "version": "0.3.0",
3
+ "version": "0.6.0",
4
4
  "description": "Kangentic mobile bridge wire protocol: framing, Noise-based pairing and session handshakes, signed device roster, and capability verbs shared between desktop and mobile.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",