@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.
- package/dist/capabilities/verbs.d.ts +1 -1
- package/dist/crypto/push-envelope.d.ts +38 -0
- package/dist/events/event.d.ts +34 -4
- package/dist/events/payloads.d.ts +17 -0
- package/dist/index.cjs +229 -45
- package/dist/index.d.ts +4 -3
- package/dist/index.mjs +220 -45
- package/dist/wire/payloads.d.ts +91 -5
- package/package.json +1 -1
|
@@ -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;
|
package/dist/events/event.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 {
|
|
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 (
|
|
5507
|
-
|
|
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/
|
|
5542
|
-
var
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
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
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 (
|
|
5405
|
-
|
|
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/
|
|
5440
|
-
var
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
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
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/wire/payloads.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
"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",
|