@kangentic/protocol 0.8.0 → 0.10.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/README.md +5 -1
- package/dist/capabilities/board-tools.d.ts +2 -2
- package/dist/events/payloads.d.ts +48 -0
- package/dist/index.cjs +121 -5
- package/dist/index.d.ts +4 -2
- package/dist/index.mjs +117 -5
- package/dist/pairing/confirm.d.ts +39 -0
- package/dist/roster/fingerprint.d.ts +10 -0
- package/dist/wire/payloads.d.ts +92 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -20,7 +20,11 @@ model, and the honest relay-metadata statement.
|
|
|
20
20
|
- `Noise_KK_25519_ChaChaPoly_BLAKE2s` for ongoing sessions (mutual authentication by construction,
|
|
21
21
|
no trust-on-first-use).
|
|
22
22
|
- A token-bound Noise PSK handshake for the one-time pairing ceremony, confirmed by a Matrix-style
|
|
23
|
-
short authentication string (SAS) on both screens.
|
|
23
|
+
short authentication string (SAS) compared on both screens - digits only, no emoji.
|
|
24
|
+
- A sealed pairing-confirm frame (`pairing/confirm.ts`), sent once the human taps Confirm on the
|
|
25
|
+
phone: it is a liveness/intent signal, not the security boundary. It opens only if both peers
|
|
26
|
+
completed the same handshake transcript, which is exactly the property the SAS comparison
|
|
27
|
+
already vouches for - the human's comparison remains the actual defense.
|
|
24
28
|
- No shell, file, or arbitrary-command verb exists in the protocol.
|
|
25
29
|
|
|
26
30
|
This package has not undergone a third-party security audit. Report suspected vulnerabilities
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* this protocol surface honest: a tool renamed or removed desktop-side
|
|
14
14
|
* fails that test instead of silently breaking phones.
|
|
15
15
|
*/
|
|
16
|
-
export declare const BOARD_TOOL_READ_NAMES: readonly ["search_tasks", "find_task", "get_current_task", "get_task_stats", "get_usage_stats", "board_summary", "list_sessions", "get_session_history", "get_column_detail", "get_handoff_context", "get_transcript", "get_session_files", "get_session_events", "get_activity_intervals"];
|
|
17
|
-
export declare const BOARD_TOOL_WRITE_NAMES: readonly ["create_task", "update_task", "delete_task", "link_pr", "remove_attachment", "update_column", "create_backlog_task", "promote_backlog", "update_backlog_item", "delete_backlog_item"];
|
|
16
|
+
export declare const BOARD_TOOL_READ_NAMES: readonly ["search_tasks", "find_task", "get_current_task", "get_task_stats", "get_usage_stats", "board_summary", "list_sessions", "get_session_history", "get_column_detail", "list_board_profiles", "get_handoff_context", "get_transcript", "get_session_files", "get_session_events", "get_activity_intervals"];
|
|
17
|
+
export declare const BOARD_TOOL_WRITE_NAMES: readonly ["create_task", "update_task", "delete_task", "link_pr", "remove_attachment", "update_column", "create_board_profile", "update_board_profile", "delete_board_profile", "create_backlog_task", "promote_backlog", "update_backlog_item", "delete_backlog_item"];
|
|
18
18
|
export type BoardToolReadName = (typeof BOARD_TOOL_READ_NAMES)[number];
|
|
19
19
|
export type BoardToolWriteName = (typeof BOARD_TOOL_WRITE_NAMES)[number];
|
|
20
20
|
export type BoardToolName = BoardToolReadName | BoardToolWriteName;
|
|
@@ -167,6 +167,44 @@ export interface SessionUsageWire {
|
|
|
167
167
|
effort?: string;
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Phone-needed subset of the desktop's SessionSummary: what a task COST over
|
|
172
|
+
* its whole life, as opposed to SessionUsageWire's live snapshot of one
|
|
173
|
+
* running agent.
|
|
174
|
+
*
|
|
175
|
+
* Every figure is a lifetime aggregate across every session record of the
|
|
176
|
+
* task, which is what makes it meaningful for a completed one: a task worked
|
|
177
|
+
* across five `--resume` legs has five session rows, and reporting only the
|
|
178
|
+
* final leg would under-report everything. The desktop's getSummaryForTask
|
|
179
|
+
* owns those aggregation rules (SUM cost/duration/tool calls/lines, MAX files
|
|
180
|
+
* changed, and tokens taken as the latest row per session lineage so a
|
|
181
|
+
* resumed session is not double-counted) - they are not re-derived here.
|
|
182
|
+
*
|
|
183
|
+
* `sessionId` is the transcript ANCHOR, not a live session: it resolves
|
|
184
|
+
* through the desktop's session records, which outlive the agent, so it reads
|
|
185
|
+
* a finished task's conversation with nothing running. An archived task's
|
|
186
|
+
* `session_id` on the board is null by then, so this is the only handle to it.
|
|
187
|
+
*/
|
|
188
|
+
export interface SessionSummaryWire {
|
|
189
|
+
sessionId: string;
|
|
190
|
+
totalCostUsd: number;
|
|
191
|
+
totalInputTokens: number;
|
|
192
|
+
totalOutputTokens: number;
|
|
193
|
+
modelDisplayName: string;
|
|
194
|
+
durationMs: number;
|
|
195
|
+
toolCallCount: number;
|
|
196
|
+
compactionCount: number;
|
|
197
|
+
linesAdded: number;
|
|
198
|
+
linesRemoved: number;
|
|
199
|
+
filesChanged: number;
|
|
200
|
+
/** ISO 8601. The task's creation, which is where its timeline starts. */
|
|
201
|
+
taskCreatedAt: string;
|
|
202
|
+
/** ISO 8601. Earliest session start across every leg. */
|
|
203
|
+
startedAt: string;
|
|
204
|
+
/** ISO 8601, or null while a leg is still open. Latest exit or suspend. */
|
|
205
|
+
exitedAt: string | null;
|
|
206
|
+
exitCode: number | null;
|
|
207
|
+
}
|
|
170
208
|
/**
|
|
171
209
|
* Loose mirror of the desktop's SessionEvent telemetry entry. `type` is
|
|
172
210
|
* deliberately a plain string, not the desktop's EventType enum, so a new
|
|
@@ -267,6 +305,16 @@ export declare function isActivityStateWire(value: unknown): value is ActivitySt
|
|
|
267
305
|
export declare function isActivityReasonWire(value: unknown): value is ActivityReasonWire;
|
|
268
306
|
/** Narrows a usage payload to the SessionUsageWire subset. Throws on a malformed required field. */
|
|
269
307
|
export declare function parseSessionUsageWire(payload: JsonValue): SessionUsageWire;
|
|
308
|
+
/**
|
|
309
|
+
* Narrows a session-summary payload to SessionSummaryWire. Throws on a
|
|
310
|
+
* malformed required field.
|
|
311
|
+
*
|
|
312
|
+
* The numeric metrics are required rather than optional because the desktop
|
|
313
|
+
* COALESCEs each to 0 before it sends them, so "no data" arrives as a real
|
|
314
|
+
* zero. Making them optional here would invite a phone to render an em-dash
|
|
315
|
+
* where the honest answer is 0.
|
|
316
|
+
*/
|
|
317
|
+
export declare function parseSessionSummaryWire(payload: JsonValue): SessionSummaryWire;
|
|
270
318
|
/** Narrows a session-event payload to SessionEventWire. Throws on a malformed required field. */
|
|
271
319
|
export declare function parseSessionEventWire(payload: JsonValue): SessionEventWire;
|
|
272
320
|
/** Narrows one board-column row. Throws on a malformed required field. */
|
package/dist/index.cjs
CHANGED
|
@@ -58,6 +58,7 @@ __export(index_exports, {
|
|
|
58
58
|
encodeProtocolVersion: () => encodeProtocolVersion,
|
|
59
59
|
encodeRosterEntryForSigning: () => encodeRosterEntryForSigning,
|
|
60
60
|
findRosterDevice: () => findRosterDevice,
|
|
61
|
+
formatKeyFingerprint: () => formatKeyFingerprint,
|
|
61
62
|
generateEd25519KeyPair: () => generateEd25519KeyPair,
|
|
62
63
|
generateX25519KeyPair: () => generateX25519KeyPair,
|
|
63
64
|
hexToBytes: () => hexToBytes,
|
|
@@ -72,6 +73,7 @@ __export(index_exports, {
|
|
|
72
73
|
isRecord: () => isRecord,
|
|
73
74
|
isRosterEntryExpired: () => isRosterEntryExpired,
|
|
74
75
|
isSecureRelayAddress: () => isSecureRelayAddress,
|
|
76
|
+
openPairingConfirm: () => openPairingConfirm,
|
|
75
77
|
openPushEnvelope: () => openPushEnvelope,
|
|
76
78
|
parseActivityEventPayload: () => parseActivityEventPayload,
|
|
77
79
|
parseBacklogItemWire: () => parseBacklogItemWire,
|
|
@@ -86,6 +88,7 @@ __export(index_exports, {
|
|
|
86
88
|
parseReadStreamResponsePayload: () => parseReadStreamResponsePayload,
|
|
87
89
|
parseRegisterPushRequestPayload: () => parseRegisterPushRequestPayload,
|
|
88
90
|
parseSessionEventWire: () => parseSessionEventWire,
|
|
91
|
+
parseSessionSummaryWire: () => parseSessionSummaryWire,
|
|
89
92
|
parseSessionUsageWire: () => parseSessionUsageWire,
|
|
90
93
|
parseTerminalDimensionsWire: () => parseTerminalDimensionsWire,
|
|
91
94
|
parseTranscriptEntriesWire: () => parseTranscriptEntriesWire,
|
|
@@ -93,6 +96,7 @@ __export(index_exports, {
|
|
|
93
96
|
parseTranscriptWindowResponsePayload: () => parseTranscriptWindowResponsePayload,
|
|
94
97
|
randomBytes: () => randomBytes4,
|
|
95
98
|
rosterDeviceCapabilitySet: () => rosterDeviceCapabilitySet,
|
|
99
|
+
sealPairingConfirm: () => sealPairingConfirm,
|
|
96
100
|
sealPushEnvelope: () => sealPushEnvelope,
|
|
97
101
|
signRosterEntry: () => signRosterEntry,
|
|
98
102
|
unwrapSessionFrame: () => unwrapSessionFrame,
|
|
@@ -5361,6 +5365,26 @@ function parseSessionUsageWire(payload) {
|
|
|
5361
5365
|
}
|
|
5362
5366
|
};
|
|
5363
5367
|
}
|
|
5368
|
+
function parseSessionSummaryWire(payload) {
|
|
5369
|
+
if (!isRecord(payload)) throw new Error("session summary payload must be an object");
|
|
5370
|
+
return {
|
|
5371
|
+
sessionId: requireString(payload, "sessionId", "session summary"),
|
|
5372
|
+
totalCostUsd: requireNumber(payload, "totalCostUsd", "session summary"),
|
|
5373
|
+
totalInputTokens: requireNumber(payload, "totalInputTokens", "session summary"),
|
|
5374
|
+
totalOutputTokens: requireNumber(payload, "totalOutputTokens", "session summary"),
|
|
5375
|
+
modelDisplayName: requireString(payload, "modelDisplayName", "session summary"),
|
|
5376
|
+
durationMs: requireNumber(payload, "durationMs", "session summary"),
|
|
5377
|
+
toolCallCount: requireNumber(payload, "toolCallCount", "session summary"),
|
|
5378
|
+
compactionCount: requireNumber(payload, "compactionCount", "session summary"),
|
|
5379
|
+
linesAdded: requireNumber(payload, "linesAdded", "session summary"),
|
|
5380
|
+
linesRemoved: requireNumber(payload, "linesRemoved", "session summary"),
|
|
5381
|
+
filesChanged: requireNumber(payload, "filesChanged", "session summary"),
|
|
5382
|
+
taskCreatedAt: requireString(payload, "taskCreatedAt", "session summary"),
|
|
5383
|
+
startedAt: requireString(payload, "startedAt", "session summary"),
|
|
5384
|
+
exitedAt: typeof payload.exitedAt === "string" ? payload.exitedAt : null,
|
|
5385
|
+
exitCode: typeof payload.exitCode === "number" ? payload.exitCode : null
|
|
5386
|
+
};
|
|
5387
|
+
}
|
|
5364
5388
|
function parseSessionEventWire(payload) {
|
|
5365
5389
|
if (!isRecord(payload)) throw new Error("session event payload must be an object");
|
|
5366
5390
|
return {
|
|
@@ -5563,6 +5587,18 @@ function parseAccentColor(value, context) {
|
|
|
5563
5587
|
}
|
|
5564
5588
|
function parseReadBoardResponsePayload(payload) {
|
|
5565
5589
|
if (!isRecord(payload)) throw new Error("read-board response must be an object");
|
|
5590
|
+
if (Array.isArray(payload.archivedTasks)) {
|
|
5591
|
+
if (typeof payload.projectId !== "string") throw new Error('read-board archived response is missing "projectId"');
|
|
5592
|
+
if (typeof payload.archivedTotalCount !== "number") {
|
|
5593
|
+
throw new Error('read-board archived response is missing "archivedTotalCount"');
|
|
5594
|
+
}
|
|
5595
|
+
return {
|
|
5596
|
+
projectId: payload.projectId,
|
|
5597
|
+
archivedTasks: payload.archivedTasks.map((task) => parseBoardTaskWire(task)),
|
|
5598
|
+
archivedTotalCount: payload.archivedTotalCount,
|
|
5599
|
+
summariesByTaskId: parseSummariesByTaskId(payload.summariesByTaskId)
|
|
5600
|
+
};
|
|
5601
|
+
}
|
|
5566
5602
|
if (Array.isArray(payload.projects)) {
|
|
5567
5603
|
const projects = payload.projects.map((project, index) => {
|
|
5568
5604
|
if (!isRecord(project) || typeof project.id !== "string" || typeof project.name !== "string") {
|
|
@@ -5579,28 +5615,70 @@ function parseReadBoardResponsePayload(payload) {
|
|
|
5579
5615
|
if (typeof payload.projectId !== "string") throw new Error('read-board response is missing "projectId"');
|
|
5580
5616
|
if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
|
|
5581
5617
|
if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
|
|
5582
|
-
if (!Array.isArray(payload.backlog))
|
|
5618
|
+
if (payload.backlog !== void 0 && !Array.isArray(payload.backlog)) {
|
|
5619
|
+
throw new Error('read-board response has a non-array "backlog"');
|
|
5620
|
+
}
|
|
5583
5621
|
if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
|
|
5584
5622
|
throw new Error('read-board response has a non-boolean "showTicketNumbers"');
|
|
5585
5623
|
}
|
|
5624
|
+
if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
|
|
5625
|
+
throw new Error('read-board response has an invalid "view"');
|
|
5626
|
+
}
|
|
5586
5627
|
return {
|
|
5587
5628
|
projectId: payload.projectId,
|
|
5588
5629
|
columns: payload.columns.map((column) => parseBoardColumnWire(column)),
|
|
5589
5630
|
tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
|
|
5590
|
-
backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
|
|
5631
|
+
...payload.backlog !== void 0 ? { backlog: payload.backlog.map((item) => parseBacklogItemWire(item)) } : {},
|
|
5591
5632
|
...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
|
|
5592
|
-
...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
|
|
5633
|
+
...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {},
|
|
5634
|
+
...payload.view !== void 0 ? { view: payload.view } : {},
|
|
5635
|
+
...payload.taskCountsByColumnId !== void 0 ? { taskCountsByColumnId: parseTaskCountsByColumnId(payload.taskCountsByColumnId) } : {}
|
|
5593
5636
|
};
|
|
5594
5637
|
}
|
|
5638
|
+
function parseTaskCountsByColumnId(value) {
|
|
5639
|
+
if (!isRecord(value)) throw new Error('read-board response has a non-object "taskCountsByColumnId"');
|
|
5640
|
+
const counts = {};
|
|
5641
|
+
for (const [columnId, count] of Object.entries(value)) {
|
|
5642
|
+
if (typeof count === "number" && Number.isInteger(count) && count >= 0) counts[columnId] = count;
|
|
5643
|
+
}
|
|
5644
|
+
return counts;
|
|
5645
|
+
}
|
|
5646
|
+
function parseSummariesByTaskId(value) {
|
|
5647
|
+
if (value === void 0) return {};
|
|
5648
|
+
if (!isRecord(value)) throw new Error('read-board archived response has a non-object "summariesByTaskId"');
|
|
5649
|
+
const summaries = {};
|
|
5650
|
+
for (const [taskId, summary] of Object.entries(value)) {
|
|
5651
|
+
try {
|
|
5652
|
+
summaries[taskId] = parseSessionSummaryWire(summary);
|
|
5653
|
+
} catch {
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
return summaries;
|
|
5657
|
+
}
|
|
5595
5658
|
function parseReadBoardRequestPayload(payload) {
|
|
5596
5659
|
if (!isRecord(payload)) throw new Error("read-board payload must be an object");
|
|
5597
5660
|
if (payload.projectId !== void 0 && typeof payload.projectId !== "string") {
|
|
5598
5661
|
throw new Error('read-board payload has a non-string "projectId"');
|
|
5599
5662
|
}
|
|
5600
|
-
if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe") {
|
|
5663
|
+
if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe" && payload.action !== "archived") {
|
|
5601
5664
|
throw new Error('read-board payload has an invalid "action"');
|
|
5602
5665
|
}
|
|
5603
|
-
|
|
5666
|
+
if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
|
|
5667
|
+
throw new Error('read-board payload has an invalid "view"');
|
|
5668
|
+
}
|
|
5669
|
+
if (payload.limit !== void 0 && (typeof payload.limit !== "number" || !Number.isInteger(payload.limit) || payload.limit < 1)) {
|
|
5670
|
+
throw new Error('read-board payload has an invalid "limit"');
|
|
5671
|
+
}
|
|
5672
|
+
if (payload.offset !== void 0 && (typeof payload.offset !== "number" || !Number.isInteger(payload.offset) || payload.offset < 0)) {
|
|
5673
|
+
throw new Error('read-board payload has an invalid "offset"');
|
|
5674
|
+
}
|
|
5675
|
+
return {
|
|
5676
|
+
projectId: payload.projectId,
|
|
5677
|
+
action: payload.action,
|
|
5678
|
+
view: payload.view,
|
|
5679
|
+
limit: payload.limit,
|
|
5680
|
+
offset: payload.offset
|
|
5681
|
+
};
|
|
5604
5682
|
}
|
|
5605
5683
|
function parseReadDiffResponsePayload(payload) {
|
|
5606
5684
|
if (!isRecord(payload)) throw new Error("read-diff response must be an object");
|
|
@@ -5841,6 +5919,27 @@ function decodePairingQrPayload(uri) {
|
|
|
5841
5919
|
return { desktopStaticPublicKey, pairingToken, relayAddress, expiresAt, protocolVersion };
|
|
5842
5920
|
}
|
|
5843
5921
|
|
|
5922
|
+
// src/pairing/confirm.ts
|
|
5923
|
+
var PAIRING_CONFIRM_PLAINTEXT = new TextEncoder().encode("kangentic-pairing-confirm-v1");
|
|
5924
|
+
var EMPTY_ASSOCIATED_DATA = new Uint8Array(0);
|
|
5925
|
+
function sealPairingConfirm(initiatorToResponder) {
|
|
5926
|
+
return initiatorToResponder.encryptWithAd(EMPTY_ASSOCIATED_DATA, PAIRING_CONFIRM_PLAINTEXT);
|
|
5927
|
+
}
|
|
5928
|
+
function openPairingConfirm(initiatorToResponder, frame) {
|
|
5929
|
+
let plaintext;
|
|
5930
|
+
try {
|
|
5931
|
+
plaintext = initiatorToResponder.decryptWithAd(EMPTY_ASSOCIATED_DATA, frame);
|
|
5932
|
+
} catch {
|
|
5933
|
+
return false;
|
|
5934
|
+
}
|
|
5935
|
+
if (plaintext.length !== PAIRING_CONFIRM_PLAINTEXT.length) return false;
|
|
5936
|
+
let mismatch = 0;
|
|
5937
|
+
for (let index = 0; index < plaintext.length; index += 1) {
|
|
5938
|
+
mismatch |= plaintext[index] ^ PAIRING_CONFIRM_PLAINTEXT[index];
|
|
5939
|
+
}
|
|
5940
|
+
return mismatch === 0;
|
|
5941
|
+
}
|
|
5942
|
+
|
|
5844
5943
|
// src/roster/roster.ts
|
|
5845
5944
|
function uint32LengthPrefixed(bytes) {
|
|
5846
5945
|
const prefix = new Uint8Array(4);
|
|
@@ -5880,6 +5979,15 @@ function capabilitySetToRosterCapabilities(capabilities) {
|
|
|
5880
5979
|
return capabilitySetToArray(capabilities);
|
|
5881
5980
|
}
|
|
5882
5981
|
|
|
5982
|
+
// src/roster/fingerprint.ts
|
|
5983
|
+
function formatKeyFingerprint(publicKeyHex, groups = 4) {
|
|
5984
|
+
const clusters = [];
|
|
5985
|
+
for (let index = 0; index < groups; index += 1) {
|
|
5986
|
+
clusters.push(publicKeyHex.slice(index * 4, index * 4 + 4));
|
|
5987
|
+
}
|
|
5988
|
+
return clusters.join(" ");
|
|
5989
|
+
}
|
|
5990
|
+
|
|
5883
5991
|
// src/capabilities/board-tools.ts
|
|
5884
5992
|
var BOARD_TOOL_READ_NAMES = [
|
|
5885
5993
|
"search_tasks",
|
|
@@ -5891,6 +5999,7 @@ var BOARD_TOOL_READ_NAMES = [
|
|
|
5891
5999
|
"list_sessions",
|
|
5892
6000
|
"get_session_history",
|
|
5893
6001
|
"get_column_detail",
|
|
6002
|
+
"list_board_profiles",
|
|
5894
6003
|
"get_handoff_context",
|
|
5895
6004
|
"get_transcript",
|
|
5896
6005
|
"get_session_files",
|
|
@@ -5904,6 +6013,9 @@ var BOARD_TOOL_WRITE_NAMES = [
|
|
|
5904
6013
|
"link_pr",
|
|
5905
6014
|
"remove_attachment",
|
|
5906
6015
|
"update_column",
|
|
6016
|
+
"create_board_profile",
|
|
6017
|
+
"update_board_profile",
|
|
6018
|
+
"delete_board_profile",
|
|
5907
6019
|
"create_backlog_task",
|
|
5908
6020
|
"promote_backlog",
|
|
5909
6021
|
"update_backlog_item",
|
|
@@ -6041,6 +6153,7 @@ function isBridgeEvent(value) {
|
|
|
6041
6153
|
encodeProtocolVersion,
|
|
6042
6154
|
encodeRosterEntryForSigning,
|
|
6043
6155
|
findRosterDevice,
|
|
6156
|
+
formatKeyFingerprint,
|
|
6044
6157
|
generateEd25519KeyPair,
|
|
6045
6158
|
generateX25519KeyPair,
|
|
6046
6159
|
hexToBytes,
|
|
@@ -6055,6 +6168,7 @@ function isBridgeEvent(value) {
|
|
|
6055
6168
|
isRecord,
|
|
6056
6169
|
isRosterEntryExpired,
|
|
6057
6170
|
isSecureRelayAddress,
|
|
6171
|
+
openPairingConfirm,
|
|
6058
6172
|
openPushEnvelope,
|
|
6059
6173
|
parseActivityEventPayload,
|
|
6060
6174
|
parseBacklogItemWire,
|
|
@@ -6069,6 +6183,7 @@ function isBridgeEvent(value) {
|
|
|
6069
6183
|
parseReadStreamResponsePayload,
|
|
6070
6184
|
parseRegisterPushRequestPayload,
|
|
6071
6185
|
parseSessionEventWire,
|
|
6186
|
+
parseSessionSummaryWire,
|
|
6072
6187
|
parseSessionUsageWire,
|
|
6073
6188
|
parseTerminalDimensionsWire,
|
|
6074
6189
|
parseTranscriptEntriesWire,
|
|
@@ -6076,6 +6191,7 @@ function isBridgeEvent(value) {
|
|
|
6076
6191
|
parseTranscriptWindowResponsePayload,
|
|
6077
6192
|
randomBytes,
|
|
6078
6193
|
rosterDeviceCapabilitySet,
|
|
6194
|
+
sealPairingConfirm,
|
|
6079
6195
|
sealPushEnvelope,
|
|
6080
6196
|
signRosterEntry,
|
|
6081
6197
|
unwrapSessionFrame,
|
package/dist/index.d.ts
CHANGED
|
@@ -12,13 +12,15 @@ 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, 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';
|
|
15
|
+
export { parseCapabilityRequestPayload, parseReadStreamResponsePayload, parseReadBoardResponsePayload, parseReadDiffResponsePayload, parseTranscriptWindowResponsePayload, parseRegisterPushRequestPayload, type CapabilityRequestPayloadMap, type CapabilityResponsePayloadMap, type ReadStreamRequestPayload, type ReadStreamResponsePayload, type ReadStreamSessionStatusWire, type TranscriptWindowResponsePayload, type ReadBoardRequestPayload, type ReadBoardView, type ReadBoardProjectSummary, type ReadBoardProjectListResponsePayload, type ReadBoardSnapshotResponsePayload, type ReadBoardArchivedResponsePayload, 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
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';
|
|
17
17
|
export { PAIRING_URI_SCHEME, encodePairingQrPayload, decodePairingQrPayload, type PairingQrPayload, } from './pairing/qr';
|
|
18
|
+
export { sealPairingConfirm, openPairingConfirm } from './pairing/confirm';
|
|
18
19
|
export { MAX_RELAY_ADDRESS_LENGTH, isSecureRelayAddress } from './pairing/relay-address';
|
|
19
20
|
export { signRosterEntry, verifyRosterEntry, encodeRosterEntryForSigning, isRosterEntryExpired, findRosterDevice, rosterDeviceCapabilitySet, capabilitySetToRosterCapabilities, type RosterDeviceEntry, type DeviceRoster, } from './roster/roster';
|
|
21
|
+
export { formatKeyFingerprint } from './roster/fingerprint';
|
|
20
22
|
export { CAPABILITY_VERBS, isCapabilityVerb, capabilitySetFromArray, capabilitySetToArray, type CapabilityVerb, type CapabilitySet, } from './capabilities/verbs';
|
|
21
23
|
export { BOARD_TOOL_READ_NAMES, BOARD_TOOL_WRITE_NAMES, isBoardToolReadName, isBoardToolWriteName, type BoardToolReadName, type BoardToolWriteName, type BoardToolName, } from './capabilities/board-tools';
|
|
22
24
|
export { isBridgeEvent, parseActivityEventPayload, type BridgeEvent, type TranscriptEvent, type ActivityEvent, type ActivityEventPayload, type TerminalEvent, type TerminalResizeEvent, type BoardEvent, type BoardEventPayload, type DiffEvent, } from './events/event';
|
|
23
|
-
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';
|
|
25
|
+
export { parseTranscriptEntriesWire, parseTranscriptEventPayload, type TranscriptEventPayload, type TranscriptUpsertWire, isActivityStateWire, isActivityReasonWire, parseSessionUsageWire, parseSessionSummaryWire, parseSessionEventWire, parseTerminalDimensionsWire, parseBoardColumnWire, parseBoardTaskWire, parseBacklogItemWire, parseDiffFileListWire, parseDiffFileContentWire, type TranscriptBlockWire, type TranscriptEntryWire, type TranscriptTurnUsageWire, type TranscriptSystemSubtypeWire, type ActivityStateWire, type ActivityReasonWire, type SessionUsageWire, type SessionSummaryWire, type SessionEventWire, type TerminalDimensionsWire, type BoardColumnWire, type BoardTaskWire, type BacklogItemWire, type DiffFileStatusWire, type DiffFileWire, type DiffFileListWire, type DiffFileContentWire, } from './events/payloads';
|
|
24
26
|
export type { Transport, TransportState, Unsubscribe } from './transport/transport';
|
package/dist/index.mjs
CHANGED
|
@@ -5248,6 +5248,26 @@ function parseSessionUsageWire(payload) {
|
|
|
5248
5248
|
}
|
|
5249
5249
|
};
|
|
5250
5250
|
}
|
|
5251
|
+
function parseSessionSummaryWire(payload) {
|
|
5252
|
+
if (!isRecord(payload)) throw new Error("session summary payload must be an object");
|
|
5253
|
+
return {
|
|
5254
|
+
sessionId: requireString(payload, "sessionId", "session summary"),
|
|
5255
|
+
totalCostUsd: requireNumber(payload, "totalCostUsd", "session summary"),
|
|
5256
|
+
totalInputTokens: requireNumber(payload, "totalInputTokens", "session summary"),
|
|
5257
|
+
totalOutputTokens: requireNumber(payload, "totalOutputTokens", "session summary"),
|
|
5258
|
+
modelDisplayName: requireString(payload, "modelDisplayName", "session summary"),
|
|
5259
|
+
durationMs: requireNumber(payload, "durationMs", "session summary"),
|
|
5260
|
+
toolCallCount: requireNumber(payload, "toolCallCount", "session summary"),
|
|
5261
|
+
compactionCount: requireNumber(payload, "compactionCount", "session summary"),
|
|
5262
|
+
linesAdded: requireNumber(payload, "linesAdded", "session summary"),
|
|
5263
|
+
linesRemoved: requireNumber(payload, "linesRemoved", "session summary"),
|
|
5264
|
+
filesChanged: requireNumber(payload, "filesChanged", "session summary"),
|
|
5265
|
+
taskCreatedAt: requireString(payload, "taskCreatedAt", "session summary"),
|
|
5266
|
+
startedAt: requireString(payload, "startedAt", "session summary"),
|
|
5267
|
+
exitedAt: typeof payload.exitedAt === "string" ? payload.exitedAt : null,
|
|
5268
|
+
exitCode: typeof payload.exitCode === "number" ? payload.exitCode : null
|
|
5269
|
+
};
|
|
5270
|
+
}
|
|
5251
5271
|
function parseSessionEventWire(payload) {
|
|
5252
5272
|
if (!isRecord(payload)) throw new Error("session event payload must be an object");
|
|
5253
5273
|
return {
|
|
@@ -5450,6 +5470,18 @@ function parseAccentColor(value, context) {
|
|
|
5450
5470
|
}
|
|
5451
5471
|
function parseReadBoardResponsePayload(payload) {
|
|
5452
5472
|
if (!isRecord(payload)) throw new Error("read-board response must be an object");
|
|
5473
|
+
if (Array.isArray(payload.archivedTasks)) {
|
|
5474
|
+
if (typeof payload.projectId !== "string") throw new Error('read-board archived response is missing "projectId"');
|
|
5475
|
+
if (typeof payload.archivedTotalCount !== "number") {
|
|
5476
|
+
throw new Error('read-board archived response is missing "archivedTotalCount"');
|
|
5477
|
+
}
|
|
5478
|
+
return {
|
|
5479
|
+
projectId: payload.projectId,
|
|
5480
|
+
archivedTasks: payload.archivedTasks.map((task) => parseBoardTaskWire(task)),
|
|
5481
|
+
archivedTotalCount: payload.archivedTotalCount,
|
|
5482
|
+
summariesByTaskId: parseSummariesByTaskId(payload.summariesByTaskId)
|
|
5483
|
+
};
|
|
5484
|
+
}
|
|
5453
5485
|
if (Array.isArray(payload.projects)) {
|
|
5454
5486
|
const projects = payload.projects.map((project, index) => {
|
|
5455
5487
|
if (!isRecord(project) || typeof project.id !== "string" || typeof project.name !== "string") {
|
|
@@ -5466,28 +5498,70 @@ function parseReadBoardResponsePayload(payload) {
|
|
|
5466
5498
|
if (typeof payload.projectId !== "string") throw new Error('read-board response is missing "projectId"');
|
|
5467
5499
|
if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
|
|
5468
5500
|
if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
|
|
5469
|
-
if (!Array.isArray(payload.backlog))
|
|
5501
|
+
if (payload.backlog !== void 0 && !Array.isArray(payload.backlog)) {
|
|
5502
|
+
throw new Error('read-board response has a non-array "backlog"');
|
|
5503
|
+
}
|
|
5470
5504
|
if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
|
|
5471
5505
|
throw new Error('read-board response has a non-boolean "showTicketNumbers"');
|
|
5472
5506
|
}
|
|
5507
|
+
if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
|
|
5508
|
+
throw new Error('read-board response has an invalid "view"');
|
|
5509
|
+
}
|
|
5473
5510
|
return {
|
|
5474
5511
|
projectId: payload.projectId,
|
|
5475
5512
|
columns: payload.columns.map((column) => parseBoardColumnWire(column)),
|
|
5476
5513
|
tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
|
|
5477
|
-
backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
|
|
5514
|
+
...payload.backlog !== void 0 ? { backlog: payload.backlog.map((item) => parseBacklogItemWire(item)) } : {},
|
|
5478
5515
|
...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
|
|
5479
|
-
...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
|
|
5516
|
+
...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {},
|
|
5517
|
+
...payload.view !== void 0 ? { view: payload.view } : {},
|
|
5518
|
+
...payload.taskCountsByColumnId !== void 0 ? { taskCountsByColumnId: parseTaskCountsByColumnId(payload.taskCountsByColumnId) } : {}
|
|
5480
5519
|
};
|
|
5481
5520
|
}
|
|
5521
|
+
function parseTaskCountsByColumnId(value) {
|
|
5522
|
+
if (!isRecord(value)) throw new Error('read-board response has a non-object "taskCountsByColumnId"');
|
|
5523
|
+
const counts = {};
|
|
5524
|
+
for (const [columnId, count] of Object.entries(value)) {
|
|
5525
|
+
if (typeof count === "number" && Number.isInteger(count) && count >= 0) counts[columnId] = count;
|
|
5526
|
+
}
|
|
5527
|
+
return counts;
|
|
5528
|
+
}
|
|
5529
|
+
function parseSummariesByTaskId(value) {
|
|
5530
|
+
if (value === void 0) return {};
|
|
5531
|
+
if (!isRecord(value)) throw new Error('read-board archived response has a non-object "summariesByTaskId"');
|
|
5532
|
+
const summaries = {};
|
|
5533
|
+
for (const [taskId, summary] of Object.entries(value)) {
|
|
5534
|
+
try {
|
|
5535
|
+
summaries[taskId] = parseSessionSummaryWire(summary);
|
|
5536
|
+
} catch {
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
return summaries;
|
|
5540
|
+
}
|
|
5482
5541
|
function parseReadBoardRequestPayload(payload) {
|
|
5483
5542
|
if (!isRecord(payload)) throw new Error("read-board payload must be an object");
|
|
5484
5543
|
if (payload.projectId !== void 0 && typeof payload.projectId !== "string") {
|
|
5485
5544
|
throw new Error('read-board payload has a non-string "projectId"');
|
|
5486
5545
|
}
|
|
5487
|
-
if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe") {
|
|
5546
|
+
if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe" && payload.action !== "archived") {
|
|
5488
5547
|
throw new Error('read-board payload has an invalid "action"');
|
|
5489
5548
|
}
|
|
5490
|
-
|
|
5549
|
+
if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
|
|
5550
|
+
throw new Error('read-board payload has an invalid "view"');
|
|
5551
|
+
}
|
|
5552
|
+
if (payload.limit !== void 0 && (typeof payload.limit !== "number" || !Number.isInteger(payload.limit) || payload.limit < 1)) {
|
|
5553
|
+
throw new Error('read-board payload has an invalid "limit"');
|
|
5554
|
+
}
|
|
5555
|
+
if (payload.offset !== void 0 && (typeof payload.offset !== "number" || !Number.isInteger(payload.offset) || payload.offset < 0)) {
|
|
5556
|
+
throw new Error('read-board payload has an invalid "offset"');
|
|
5557
|
+
}
|
|
5558
|
+
return {
|
|
5559
|
+
projectId: payload.projectId,
|
|
5560
|
+
action: payload.action,
|
|
5561
|
+
view: payload.view,
|
|
5562
|
+
limit: payload.limit,
|
|
5563
|
+
offset: payload.offset
|
|
5564
|
+
};
|
|
5491
5565
|
}
|
|
5492
5566
|
function parseReadDiffResponsePayload(payload) {
|
|
5493
5567
|
if (!isRecord(payload)) throw new Error("read-diff response must be an object");
|
|
@@ -5728,6 +5802,27 @@ function decodePairingQrPayload(uri) {
|
|
|
5728
5802
|
return { desktopStaticPublicKey, pairingToken, relayAddress, expiresAt, protocolVersion };
|
|
5729
5803
|
}
|
|
5730
5804
|
|
|
5805
|
+
// src/pairing/confirm.ts
|
|
5806
|
+
var PAIRING_CONFIRM_PLAINTEXT = new TextEncoder().encode("kangentic-pairing-confirm-v1");
|
|
5807
|
+
var EMPTY_ASSOCIATED_DATA = new Uint8Array(0);
|
|
5808
|
+
function sealPairingConfirm(initiatorToResponder) {
|
|
5809
|
+
return initiatorToResponder.encryptWithAd(EMPTY_ASSOCIATED_DATA, PAIRING_CONFIRM_PLAINTEXT);
|
|
5810
|
+
}
|
|
5811
|
+
function openPairingConfirm(initiatorToResponder, frame) {
|
|
5812
|
+
let plaintext;
|
|
5813
|
+
try {
|
|
5814
|
+
plaintext = initiatorToResponder.decryptWithAd(EMPTY_ASSOCIATED_DATA, frame);
|
|
5815
|
+
} catch {
|
|
5816
|
+
return false;
|
|
5817
|
+
}
|
|
5818
|
+
if (plaintext.length !== PAIRING_CONFIRM_PLAINTEXT.length) return false;
|
|
5819
|
+
let mismatch = 0;
|
|
5820
|
+
for (let index = 0; index < plaintext.length; index += 1) {
|
|
5821
|
+
mismatch |= plaintext[index] ^ PAIRING_CONFIRM_PLAINTEXT[index];
|
|
5822
|
+
}
|
|
5823
|
+
return mismatch === 0;
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5731
5826
|
// src/roster/roster.ts
|
|
5732
5827
|
function uint32LengthPrefixed(bytes) {
|
|
5733
5828
|
const prefix = new Uint8Array(4);
|
|
@@ -5767,6 +5862,15 @@ function capabilitySetToRosterCapabilities(capabilities) {
|
|
|
5767
5862
|
return capabilitySetToArray(capabilities);
|
|
5768
5863
|
}
|
|
5769
5864
|
|
|
5865
|
+
// src/roster/fingerprint.ts
|
|
5866
|
+
function formatKeyFingerprint(publicKeyHex, groups = 4) {
|
|
5867
|
+
const clusters = [];
|
|
5868
|
+
for (let index = 0; index < groups; index += 1) {
|
|
5869
|
+
clusters.push(publicKeyHex.slice(index * 4, index * 4 + 4));
|
|
5870
|
+
}
|
|
5871
|
+
return clusters.join(" ");
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5770
5874
|
// src/capabilities/board-tools.ts
|
|
5771
5875
|
var BOARD_TOOL_READ_NAMES = [
|
|
5772
5876
|
"search_tasks",
|
|
@@ -5778,6 +5882,7 @@ var BOARD_TOOL_READ_NAMES = [
|
|
|
5778
5882
|
"list_sessions",
|
|
5779
5883
|
"get_session_history",
|
|
5780
5884
|
"get_column_detail",
|
|
5885
|
+
"list_board_profiles",
|
|
5781
5886
|
"get_handoff_context",
|
|
5782
5887
|
"get_transcript",
|
|
5783
5888
|
"get_session_files",
|
|
@@ -5791,6 +5896,9 @@ var BOARD_TOOL_WRITE_NAMES = [
|
|
|
5791
5896
|
"link_pr",
|
|
5792
5897
|
"remove_attachment",
|
|
5793
5898
|
"update_column",
|
|
5899
|
+
"create_board_profile",
|
|
5900
|
+
"update_board_profile",
|
|
5901
|
+
"delete_board_profile",
|
|
5794
5902
|
"create_backlog_task",
|
|
5795
5903
|
"promote_backlog",
|
|
5796
5904
|
"update_backlog_item",
|
|
@@ -5927,6 +6035,7 @@ export {
|
|
|
5927
6035
|
encodeProtocolVersion,
|
|
5928
6036
|
encodeRosterEntryForSigning,
|
|
5929
6037
|
findRosterDevice,
|
|
6038
|
+
formatKeyFingerprint,
|
|
5930
6039
|
generateEd25519KeyPair,
|
|
5931
6040
|
generateX25519KeyPair,
|
|
5932
6041
|
hexToBytes,
|
|
@@ -5941,6 +6050,7 @@ export {
|
|
|
5941
6050
|
isRecord,
|
|
5942
6051
|
isRosterEntryExpired,
|
|
5943
6052
|
isSecureRelayAddress,
|
|
6053
|
+
openPairingConfirm,
|
|
5944
6054
|
openPushEnvelope,
|
|
5945
6055
|
parseActivityEventPayload,
|
|
5946
6056
|
parseBacklogItemWire,
|
|
@@ -5955,6 +6065,7 @@ export {
|
|
|
5955
6065
|
parseReadStreamResponsePayload,
|
|
5956
6066
|
parseRegisterPushRequestPayload,
|
|
5957
6067
|
parseSessionEventWire,
|
|
6068
|
+
parseSessionSummaryWire,
|
|
5958
6069
|
parseSessionUsageWire,
|
|
5959
6070
|
parseTerminalDimensionsWire,
|
|
5960
6071
|
parseTranscriptEntriesWire,
|
|
@@ -5962,6 +6073,7 @@ export {
|
|
|
5962
6073
|
parseTranscriptWindowResponsePayload,
|
|
5963
6074
|
randomBytes4 as randomBytes,
|
|
5964
6075
|
rosterDeviceCapabilitySet,
|
|
6076
|
+
sealPairingConfirm,
|
|
5965
6077
|
sealPushEnvelope,
|
|
5966
6078
|
signRosterEntry,
|
|
5967
6079
|
unwrapSessionFrame,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pairing-confirm frame: one AEAD-sealed message the phone sends after
|
|
3
|
+
* the human taps Confirm, over the transport key the completed IKpsk0
|
|
4
|
+
* handshake just produced (HandshakeState.writeMessage's `split` result -
|
|
5
|
+
* see pairing-handshake.ts). This is a LIVENESS AND INTENT signal, not the
|
|
6
|
+
* pairing's security boundary: a relay-in-the-middle already holds a valid
|
|
7
|
+
* session with the desktop and could forge any frame it wants. The actual
|
|
8
|
+
* defense is unchanged - the human compares the desktop's SAS digits
|
|
9
|
+
* against the phone's, and only taps Confirm on a match.
|
|
10
|
+
*
|
|
11
|
+
* What this frame buys instead: both peers derive their transport keys
|
|
12
|
+
* from the SAME completed handshake transcript (getHandshakeHash/split are
|
|
13
|
+
* both functions of the transcript), so if the sealed frame opens under the
|
|
14
|
+
* desktop's key, the two transcripts necessarily agree - which means the
|
|
15
|
+
* SAS the human compared necessarily agreed too. The AEAD open IS the
|
|
16
|
+
* desktop's SAS verification; that is why the frame carries no digits of
|
|
17
|
+
* its own, and why a failed open is treated as a mismatch/failure, not a
|
|
18
|
+
* silent no-op.
|
|
19
|
+
*
|
|
20
|
+
* There is no reject frame. Backing out of the ceremony on the phone closes
|
|
21
|
+
* the transport without sending this frame; close-without-confirm is the
|
|
22
|
+
* rejection, observed by the desktop as its pairing timeout (see
|
|
23
|
+
* pairing-service.ts's phase timers).
|
|
24
|
+
*/
|
|
25
|
+
import { CipherState } from '../crypto/noise/cipher-state';
|
|
26
|
+
/**
|
|
27
|
+
* Seals the confirm frame under the initiator-to-responder cipher state
|
|
28
|
+
* (index 0 of HandshakeState's `split` pair, by Noise Protocol Framework
|
|
29
|
+
* convention). Called once per ceremony, so nonce reuse under this key is
|
|
30
|
+
* not a concern - the key itself is fresh per handshake transcript.
|
|
31
|
+
*/
|
|
32
|
+
export declare function sealPairingConfirm(initiatorToResponder: CipherState): Uint8Array;
|
|
33
|
+
/**
|
|
34
|
+
* Opens a received confirm frame. Returns false (never throws) on a
|
|
35
|
+
* tampered/wrong-key ciphertext or an unexpected plaintext, so the caller
|
|
36
|
+
* can treat "could not verify" uniformly whether the failure was
|
|
37
|
+
* cryptographic or structural.
|
|
38
|
+
*/
|
|
39
|
+
export declare function openPairingConfirm(initiatorToResponder: CipherState, frame: Uint8Array): boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a device's static public key as a short, human-comparable
|
|
3
|
+
* fingerprint - the identity the pairing ceremony actually bound and the
|
|
4
|
+
* SAS actually confirmed, unlike a name (not unique) or a hardware id
|
|
5
|
+
* (unavailable on modern Android, permanent PII). Matches
|
|
6
|
+
* kangentic-mobile's src/screens/usePairedDesktopInfo.ts formatKeyFingerprint
|
|
7
|
+
* exactly, so a user can hold the phone next to the desktop and compare
|
|
8
|
+
* directly: front-only slice, no ellipsis, no case change.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatKeyFingerprint(publicKeyHex: string, groups?: number): string;
|
package/dist/wire/payloads.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import type { CapabilityVerb } from '../capabilities/verbs';
|
|
17
17
|
import type { JsonValue } from './messages';
|
|
18
18
|
import { type PushCategory } from '../crypto/push-envelope';
|
|
19
|
-
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
|
+
import { type ActivityReasonWire, type ActivityStateWire, type BacklogItemWire, type BoardColumnWire, type BoardTaskWire, type DiffFileContentWire, type DiffFileListWire, type SessionSummaryWire, type SessionUsageWire, type TerminalDimensionsWire, type TranscriptEntryWire } from '../events/payloads';
|
|
20
20
|
export interface ReadStreamRequestPayload {
|
|
21
21
|
sessionId: string;
|
|
22
22
|
/**
|
|
@@ -99,10 +99,57 @@ export interface TranscriptWindowResponsePayload {
|
|
|
99
99
|
}
|
|
100
100
|
/** Phone-side narrowing of a transcript-window response. Throws on a malformed required field. */
|
|
101
101
|
export declare function parseTranscriptWindowResponsePayload(payload: JsonValue): TranscriptWindowResponsePayload;
|
|
102
|
+
/**
|
|
103
|
+
* Which projection of a board the phone wants.
|
|
104
|
+
*
|
|
105
|
+
* - 'sessions': columns, per-column task counts, and only the non-archived
|
|
106
|
+
* tasks that carry a `session_id`. This is what an agent feed renders: it
|
|
107
|
+
* watches every project at once but only ever draws the handful of tasks
|
|
108
|
+
* with a live agent on them.
|
|
109
|
+
* - 'full': every non-archived task, for the one project whose board the user
|
|
110
|
+
* actually has open.
|
|
111
|
+
*
|
|
112
|
+
* Neither carries the backlog. Sending the field at all is the signal that
|
|
113
|
+
* this phone tolerates a response without one; a phone that omits it gets the
|
|
114
|
+
* pre-0.9.0 payload unchanged.
|
|
115
|
+
*/
|
|
116
|
+
export type ReadBoardView = 'full' | 'sessions';
|
|
102
117
|
export interface ReadBoardRequestPayload {
|
|
103
118
|
projectId?: string;
|
|
104
|
-
/**
|
|
105
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Defaults to 'subscribe' when omitted. 'unsubscribe' only has an effect
|
|
121
|
+
* when projectId is set - the no-projectId project list is a one-shot read
|
|
122
|
+
* with no live feed to tear down.
|
|
123
|
+
*
|
|
124
|
+
* 'archived' (protocol 0.10.0) is a one-shot page of the project's COMPLETED
|
|
125
|
+
* tasks, which no other projection carries: both 'full' and 'sessions' are
|
|
126
|
+
* built from the desktop's non-archived task list. It is deliberately not a
|
|
127
|
+
* field on the snapshot, because a board subscription re-snapshots on every
|
|
128
|
+
* board change - archived tasks would then repeat down the wire forever,
|
|
129
|
+
* which is the exact bloat the 0.9.0 projections removed. Completed work
|
|
130
|
+
* changes rarely and is read on demand.
|
|
131
|
+
*/
|
|
132
|
+
action?: 'subscribe' | 'unsubscribe' | 'archived';
|
|
133
|
+
/** 'archived' only: page size, newest-archived first. The desktop clamps it. */
|
|
134
|
+
limit?: number;
|
|
135
|
+
/** 'archived' only: how many newest to skip, for paging. Defaults to 0. */
|
|
136
|
+
offset?: number;
|
|
137
|
+
/**
|
|
138
|
+
* subscribe only: the projection wanted (protocol 0.9.0). Omitted means the
|
|
139
|
+
* legacy full board WITH the backlog.
|
|
140
|
+
*
|
|
141
|
+
* Measured on a 15-project desktop, the full boards were 63 kB compressed
|
|
142
|
+
* of a ~96 kB cold start, of which 23 kB was a backlog no phone has ever
|
|
143
|
+
* rendered and another 30 kB was tasks with no session on them. Worse, a
|
|
144
|
+
* board subscription re-snapshots on every board change, so that payload
|
|
145
|
+
* repeats for as long as the phone is connected. The 'sessions' projection
|
|
146
|
+
* is 12 kB for the same 15 projects.
|
|
147
|
+
*
|
|
148
|
+
* A pre-0.9.0 desktop ignores the field and returns a full board with no
|
|
149
|
+
* `view` echo, which is exactly how a 0.9.0 phone detects that it must not
|
|
150
|
+
* treat the snapshot as filtered.
|
|
151
|
+
*/
|
|
152
|
+
view?: ReadBoardView;
|
|
106
153
|
}
|
|
107
154
|
export interface ReadBoardProjectSummary {
|
|
108
155
|
id: string;
|
|
@@ -123,8 +170,10 @@ export interface ReadBoardProjectListResponsePayload {
|
|
|
123
170
|
export interface ReadBoardSnapshotResponsePayload {
|
|
124
171
|
projectId: string;
|
|
125
172
|
columns: BoardColumnWire[];
|
|
173
|
+
/** Non-archived tasks; filtered to the ones carrying a `session_id` when `view` is 'sessions'. */
|
|
126
174
|
tasks: BoardTaskWire[];
|
|
127
|
-
|
|
175
|
+
/** Absent whenever the request carried a `view` (protocol 0.9.0) - no phone has ever rendered it. */
|
|
176
|
+
backlog?: BacklogItemWire[];
|
|
128
177
|
/** The project's accent color ("#rrggbb"); same semantics as ReadBoardProjectSummary.color. Absent from pre-0.5.0 desktops. */
|
|
129
178
|
projectColor?: string;
|
|
130
179
|
/**
|
|
@@ -133,9 +182,46 @@ export interface ReadBoardSnapshotResponsePayload {
|
|
|
133
182
|
* true, the desktop default).
|
|
134
183
|
*/
|
|
135
184
|
showTicketNumbers?: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* The projection actually applied, echoed back. Absent from a pre-0.9.0
|
|
187
|
+
* desktop, which always sends a full board - so treat an absent `view` as
|
|
188
|
+
* 'full', never as "filtered, unknown how".
|
|
189
|
+
*/
|
|
190
|
+
view?: ReadBoardView;
|
|
191
|
+
/**
|
|
192
|
+
* Non-archived task count per column id, sent with a 'sessions' view because
|
|
193
|
+
* `tasks` is filtered and a count taken from it would be wrong. Appending a
|
|
194
|
+
* card to a column needs the real length.
|
|
195
|
+
*/
|
|
196
|
+
taskCountsByColumnId?: Record<string, number>;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Returned for `action: 'archived'` - one page of a project's completed tasks
|
|
200
|
+
* (protocol 0.10.0).
|
|
201
|
+
*
|
|
202
|
+
* These never appear in a board snapshot: the desktop builds those from its
|
|
203
|
+
* non-archived task list, so a phone that only ever subscribed has no way to
|
|
204
|
+
* know a completed task exists at all.
|
|
205
|
+
*/
|
|
206
|
+
export interface ReadBoardArchivedResponsePayload {
|
|
207
|
+
projectId: string;
|
|
208
|
+
/** Newest-archived first. At most the requested `limit`. */
|
|
209
|
+
archivedTasks: BoardTaskWire[];
|
|
210
|
+
/**
|
|
211
|
+
* Total archived tasks in the project, NOT the length of the page above.
|
|
212
|
+
* Drives the column count and tells the phone whether another page exists.
|
|
213
|
+
*/
|
|
214
|
+
archivedTotalCount: number;
|
|
215
|
+
/**
|
|
216
|
+
* Lifetime cost/duration/churn per task id, for the tasks on THIS page that
|
|
217
|
+
* have recorded metrics. Sparse on purpose: a task archived without ever
|
|
218
|
+
* running an agent has nothing to summarize and is simply absent, which is
|
|
219
|
+
* not an error.
|
|
220
|
+
*/
|
|
221
|
+
summariesByTaskId: Record<string, SessionSummaryWire>;
|
|
136
222
|
}
|
|
137
|
-
export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload;
|
|
138
|
-
/** Phone-side narrowing of a read-board response (project list or
|
|
223
|
+
export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload | ReadBoardArchivedResponsePayload;
|
|
224
|
+
/** Phone-side narrowing of a read-board response (project list, board snapshot, or archived page). Throws on a malformed required field. */
|
|
139
225
|
export declare function parseReadBoardResponsePayload(payload: JsonValue): ReadBoardResponsePayload;
|
|
140
226
|
/** 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. */
|
|
141
227
|
export type ReadDiffScope = 'working' | 'staged' | 'branch';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kangentic/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"author": {
|
|
27
27
|
"name": "Kangentic",
|
|
28
|
+
"email": "hello@kangentic.com",
|
|
28
29
|
"url": "https://github.com/Kangentic"
|
|
29
30
|
},
|
|
30
31
|
"license": "AGPL-3.0-only",
|