@kangentic/protocol 0.1.1 → 0.3.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/version.d.ts CHANGED
@@ -8,5 +8,5 @@
8
8
  * differing prologue produces a differing transcript hash `h` from the
9
9
  * first MixHash call, which cascades into every derived key.
10
10
  */
11
- export declare const PROTOCOL_VERSION = "1";
11
+ export declare const PROTOCOL_VERSION = "2";
12
12
  export declare function encodeProtocolVersion(version?: string): Uint8Array;
@@ -1,4 +1,8 @@
1
1
  import type { BridgeMessage } from './messages';
2
2
  export declare const MAX_FRAME_LENGTH: number;
3
+ /** Upper bound on the JSON a compressed frame may declare/inflate to. */
4
+ export declare const MAX_DECODED_LENGTH: number;
5
+ /** JSON below this size ships raw - deflate overhead is not worth it. */
6
+ export declare const COMPRESSION_THRESHOLD: number;
3
7
  export declare function encodeMessage(message: BridgeMessage): Uint8Array;
4
8
  export declare function decodeMessage(bytes: Uint8Array): BridgeMessage;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The single `isJsonValue` runtime check, shared by framing.ts (validating a
3
+ * whole decoded BridgeMessage) and payloads.ts (validating one verb's
4
+ * request payload before a handler trusts it). Split out so neither module
5
+ * has to re-derive the other's copy.
6
+ */
7
+ import type { JsonValue } from './messages';
8
+ export declare function isJsonValue(value: unknown): value is JsonValue;
9
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Concrete per-verb request/response payload shapes. `CapabilityRequestMessage.payload`
3
+ * and `CapabilityResponseMessage.payload` stay `JsonValue` on the envelope
4
+ * (messages.ts) - these interfaces narrow that generic value at the handler
5
+ * boundary, and the `parse*RequestPayload` guards below are the runtime
6
+ * checks a handler runs against an untrusted phone-originated request before
7
+ * trusting any field.
8
+ *
9
+ * Fields that mirror an app-internal shape the protocol package cannot
10
+ * import (git diff results, board rows, an MCP tool's result) stay
11
+ * `JsonValue`, same rationale as messages.ts/events/event.ts: this package
12
+ * is a dependency-light leaf shared by desktop and phone, so it does not
13
+ * know the desktop app's internal types. Only the fixed envelope fields
14
+ * (ids, actions, booleans) are concretely typed and runtime-validated here.
15
+ */
16
+ import type { CapabilityVerb } from '../capabilities/verbs';
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';
19
+ export interface ReadStreamRequestPayload {
20
+ sessionId: string;
21
+ /**
22
+ * 'transcript-window' is a one-shot windowed-history read: the newest
23
+ * `limit` transcript entries strictly before `beforeIndex` (or the tail
24
+ * when `beforeIndex` is omitted). The desktop may return fewer entries
25
+ * than `limit` to keep the response frame small - page again from the
26
+ * returned `startIndex`. Live updates after 'subscribe' arrive as
27
+ * incremental TranscriptEvent deltas, never full transcripts.
28
+ */
29
+ action: 'subscribe' | 'unsubscribe' | 'transcript-window';
30
+ /** transcript-window only: fetch entries strictly before this absolute index. Omit for the newest window. */
31
+ beforeIndex?: number;
32
+ /** transcript-window only: maximum entries wanted (the desktop may cap this and may return fewer). */
33
+ limit?: number;
34
+ }
35
+ /** Initial snapshot returned on subscribe; live updates arrive as TerminalEvent/ActivityEvent/TranscriptEvent. */
36
+ export interface ReadStreamResponsePayload {
37
+ scrollback: string;
38
+ activity: {
39
+ state: ActivityStateWire | null;
40
+ reason: ActivityReasonWire | null;
41
+ };
42
+ usage: SessionUsageWire | null;
43
+ /** The live outstanding permission-prompt id (see answer-permission-prompt), or null when none is pending. */
44
+ awaitedPromptId: string | null;
45
+ }
46
+ /** Phone-side narrowing of a read-stream subscribe response. Throws on a malformed required field. */
47
+ export declare function parseReadStreamResponsePayload(payload: JsonValue): ReadStreamResponsePayload;
48
+ /**
49
+ * A contiguous slice of the transcript, returned by the read-stream
50
+ * 'transcript-window' action. `startIndex` is the absolute index of
51
+ * `entries[0]`; `startIndex > 0` means more history exists above.
52
+ */
53
+ export interface TranscriptWindowResponsePayload {
54
+ revision: number;
55
+ totalEntries: number;
56
+ startIndex: number;
57
+ entries: TranscriptEntryWire[];
58
+ }
59
+ /** Phone-side narrowing of a transcript-window response. Throws on a malformed required field. */
60
+ export declare function parseTranscriptWindowResponsePayload(payload: JsonValue): TranscriptWindowResponsePayload;
61
+ export interface ReadBoardRequestPayload {
62
+ projectId?: string;
63
+ /** Defaults to 'subscribe' when omitted. 'unsubscribe' only has an effect when projectId is set - the no-projectId project list is a one-shot read with no live feed to tear down. */
64
+ action?: 'subscribe' | 'unsubscribe';
65
+ }
66
+ export interface ReadBoardProjectSummary {
67
+ id: string;
68
+ name: string;
69
+ }
70
+ /** Returned when the request omits projectId - the phone's project-bootstrap listing. */
71
+ export interface ReadBoardProjectListResponsePayload {
72
+ projects: ReadBoardProjectSummary[];
73
+ }
74
+ /** Returned when the request carries a projectId - a snapshot of that project's board. */
75
+ export interface ReadBoardSnapshotResponsePayload {
76
+ projectId: string;
77
+ columns: BoardColumnWire[];
78
+ tasks: BoardTaskWire[];
79
+ backlog: BacklogItemWire[];
80
+ }
81
+ export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload;
82
+ /** Phone-side narrowing of a read-board response (project list or board snapshot). Throws on a malformed required field. */
83
+ export declare function parseReadBoardResponsePayload(payload: JsonValue): ReadBoardResponsePayload;
84
+ /** Mirrors DiffService's GitDiffScope in the desktop app; kept as a literal union here rather than an import, since the protocol package does not depend on the app. */
85
+ export type ReadDiffScope = 'working' | 'staged' | 'branch';
86
+ export interface ReadDiffRequestPayload {
87
+ taskId: string;
88
+ projectId: string;
89
+ filePath?: string;
90
+ scope?: ReadDiffScope;
91
+ /** Defaults to 'subscribe' when omitted. Only the file-list watch (no filePath) has a live feed to tear down; a single-file content fetch is always one-shot. */
92
+ action?: 'subscribe' | 'unsubscribe';
93
+ }
94
+ /** The desktop's GitDiffFilesResult mirror (no filePath) or GitFileContentResult mirror (filePath set). */
95
+ export type ReadDiffResponsePayload = DiffFileListWire | DiffFileContentWire;
96
+ /** Phone-side narrowing of a read-diff response, discriminated by the presence of "files". Throws on a malformed required field. */
97
+ export declare function parseReadDiffResponsePayload(payload: JsonValue): ReadDiffResponsePayload;
98
+ export interface SendUserMessageRequestPayload {
99
+ sessionId: string;
100
+ text: string;
101
+ }
102
+ export interface SendUserMessageResponsePayload {
103
+ delivered: boolean;
104
+ }
105
+ export interface MoveTaskRequestPayload {
106
+ taskId: string;
107
+ targetSwimlaneId: string;
108
+ targetPosition: number;
109
+ projectId: string;
110
+ }
111
+ export interface MoveTaskResponsePayload {
112
+ ok: boolean;
113
+ }
114
+ export interface AnswerPermissionPromptRequestPayload {
115
+ sessionId: string;
116
+ /** The prompt id the phone believes is outstanding - the handler rejects a stale/replayed answer whose id no longer matches the live awaited prompt. */
117
+ promptId: string;
118
+ keystrokes: string;
119
+ }
120
+ export interface AnswerPermissionPromptResponsePayload {
121
+ answered: boolean;
122
+ }
123
+ export interface InteractiveTerminalRequestPayload {
124
+ sessionId: string;
125
+ data: string;
126
+ }
127
+ export interface InteractiveTerminalResponsePayload {
128
+ written: boolean;
129
+ }
130
+ export interface BoardToolRequestPayload {
131
+ tool: string;
132
+ params: JsonValue;
133
+ }
134
+ export interface BoardToolResponsePayload {
135
+ result: JsonValue;
136
+ }
137
+ export interface CapabilityRequestPayloadMap {
138
+ 'read-stream': ReadStreamRequestPayload;
139
+ 'read-board': ReadBoardRequestPayload;
140
+ 'read-diff': ReadDiffRequestPayload;
141
+ 'send-user-message': SendUserMessageRequestPayload;
142
+ 'move-task': MoveTaskRequestPayload;
143
+ 'answer-permission-prompt': AnswerPermissionPromptRequestPayload;
144
+ 'interactive-terminal': InteractiveTerminalRequestPayload;
145
+ 'board-tool-read': BoardToolRequestPayload;
146
+ 'board-tool-write': BoardToolRequestPayload;
147
+ }
148
+ export interface CapabilityResponsePayloadMap {
149
+ 'read-stream': ReadStreamResponsePayload | TranscriptWindowResponsePayload;
150
+ 'read-board': ReadBoardResponsePayload;
151
+ 'read-diff': ReadDiffResponsePayload;
152
+ 'send-user-message': SendUserMessageResponsePayload;
153
+ 'move-task': MoveTaskResponsePayload;
154
+ 'answer-permission-prompt': AnswerPermissionPromptResponsePayload;
155
+ 'interactive-terminal': InteractiveTerminalResponsePayload;
156
+ 'board-tool-read': BoardToolResponsePayload;
157
+ 'board-tool-write': BoardToolResponsePayload;
158
+ }
159
+ /**
160
+ * Validates and narrows a decoded capability-request's generic JsonValue
161
+ * payload into its verb-specific shape. This is the runtime trust boundary:
162
+ * every field the phone supplies is checked before a handler reads it.
163
+ */
164
+ export declare function parseCapabilityRequestPayload<Verb extends CapabilityVerb>(verb: Verb, payload: JsonValue): CapabilityRequestPayloadMap[Verb];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangentic/protocol",
3
- "version": "0.1.1",
3
+ "version": "0.3.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",
@@ -42,7 +42,8 @@
42
42
  "dependencies": {
43
43
  "@noble/ciphers": "^2.2.0",
44
44
  "@noble/curves": "^2.2.0",
45
- "@noble/hashes": "^2.2.0"
45
+ "@noble/hashes": "^2.2.0",
46
+ "fflate": "^0.8.3"
46
47
  },
47
48
  "devDependencies": {
48
49
  "esbuild": "^0.28.0",