@kangentic/protocol 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/index.cjs +58 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.mjs +55 -4
- package/dist/pairing/confirm.d.ts +39 -0
- package/dist/roster/fingerprint.d.ts +10 -0
- package/dist/wire/payloads.d.ts +46 -1
- package/package.json +1 -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
|
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,
|
|
@@ -93,6 +95,7 @@ __export(index_exports, {
|
|
|
93
95
|
parseTranscriptWindowResponsePayload: () => parseTranscriptWindowResponsePayload,
|
|
94
96
|
randomBytes: () => randomBytes4,
|
|
95
97
|
rosterDeviceCapabilitySet: () => rosterDeviceCapabilitySet,
|
|
98
|
+
sealPairingConfirm: () => sealPairingConfirm,
|
|
96
99
|
sealPushEnvelope: () => sealPushEnvelope,
|
|
97
100
|
signRosterEntry: () => signRosterEntry,
|
|
98
101
|
unwrapSessionFrame: () => unwrapSessionFrame,
|
|
@@ -5579,19 +5582,34 @@ function parseReadBoardResponsePayload(payload) {
|
|
|
5579
5582
|
if (typeof payload.projectId !== "string") throw new Error('read-board response is missing "projectId"');
|
|
5580
5583
|
if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
|
|
5581
5584
|
if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
|
|
5582
|
-
if (!Array.isArray(payload.backlog))
|
|
5585
|
+
if (payload.backlog !== void 0 && !Array.isArray(payload.backlog)) {
|
|
5586
|
+
throw new Error('read-board response has a non-array "backlog"');
|
|
5587
|
+
}
|
|
5583
5588
|
if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
|
|
5584
5589
|
throw new Error('read-board response has a non-boolean "showTicketNumbers"');
|
|
5585
5590
|
}
|
|
5591
|
+
if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
|
|
5592
|
+
throw new Error('read-board response has an invalid "view"');
|
|
5593
|
+
}
|
|
5586
5594
|
return {
|
|
5587
5595
|
projectId: payload.projectId,
|
|
5588
5596
|
columns: payload.columns.map((column) => parseBoardColumnWire(column)),
|
|
5589
5597
|
tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
|
|
5590
|
-
backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
|
|
5598
|
+
...payload.backlog !== void 0 ? { backlog: payload.backlog.map((item) => parseBacklogItemWire(item)) } : {},
|
|
5591
5599
|
...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
|
|
5592
|
-
...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
|
|
5600
|
+
...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {},
|
|
5601
|
+
...payload.view !== void 0 ? { view: payload.view } : {},
|
|
5602
|
+
...payload.taskCountsByColumnId !== void 0 ? { taskCountsByColumnId: parseTaskCountsByColumnId(payload.taskCountsByColumnId) } : {}
|
|
5593
5603
|
};
|
|
5594
5604
|
}
|
|
5605
|
+
function parseTaskCountsByColumnId(value) {
|
|
5606
|
+
if (!isRecord(value)) throw new Error('read-board response has a non-object "taskCountsByColumnId"');
|
|
5607
|
+
const counts = {};
|
|
5608
|
+
for (const [columnId, count] of Object.entries(value)) {
|
|
5609
|
+
if (typeof count === "number" && Number.isInteger(count) && count >= 0) counts[columnId] = count;
|
|
5610
|
+
}
|
|
5611
|
+
return counts;
|
|
5612
|
+
}
|
|
5595
5613
|
function parseReadBoardRequestPayload(payload) {
|
|
5596
5614
|
if (!isRecord(payload)) throw new Error("read-board payload must be an object");
|
|
5597
5615
|
if (payload.projectId !== void 0 && typeof payload.projectId !== "string") {
|
|
@@ -5600,7 +5618,10 @@ function parseReadBoardRequestPayload(payload) {
|
|
|
5600
5618
|
if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe") {
|
|
5601
5619
|
throw new Error('read-board payload has an invalid "action"');
|
|
5602
5620
|
}
|
|
5603
|
-
|
|
5621
|
+
if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
|
|
5622
|
+
throw new Error('read-board payload has an invalid "view"');
|
|
5623
|
+
}
|
|
5624
|
+
return { projectId: payload.projectId, action: payload.action, view: payload.view };
|
|
5604
5625
|
}
|
|
5605
5626
|
function parseReadDiffResponsePayload(payload) {
|
|
5606
5627
|
if (!isRecord(payload)) throw new Error("read-diff response must be an object");
|
|
@@ -5841,6 +5862,27 @@ function decodePairingQrPayload(uri) {
|
|
|
5841
5862
|
return { desktopStaticPublicKey, pairingToken, relayAddress, expiresAt, protocolVersion };
|
|
5842
5863
|
}
|
|
5843
5864
|
|
|
5865
|
+
// src/pairing/confirm.ts
|
|
5866
|
+
var PAIRING_CONFIRM_PLAINTEXT = new TextEncoder().encode("kangentic-pairing-confirm-v1");
|
|
5867
|
+
var EMPTY_ASSOCIATED_DATA = new Uint8Array(0);
|
|
5868
|
+
function sealPairingConfirm(initiatorToResponder) {
|
|
5869
|
+
return initiatorToResponder.encryptWithAd(EMPTY_ASSOCIATED_DATA, PAIRING_CONFIRM_PLAINTEXT);
|
|
5870
|
+
}
|
|
5871
|
+
function openPairingConfirm(initiatorToResponder, frame) {
|
|
5872
|
+
let plaintext;
|
|
5873
|
+
try {
|
|
5874
|
+
plaintext = initiatorToResponder.decryptWithAd(EMPTY_ASSOCIATED_DATA, frame);
|
|
5875
|
+
} catch {
|
|
5876
|
+
return false;
|
|
5877
|
+
}
|
|
5878
|
+
if (plaintext.length !== PAIRING_CONFIRM_PLAINTEXT.length) return false;
|
|
5879
|
+
let mismatch = 0;
|
|
5880
|
+
for (let index = 0; index < plaintext.length; index += 1) {
|
|
5881
|
+
mismatch |= plaintext[index] ^ PAIRING_CONFIRM_PLAINTEXT[index];
|
|
5882
|
+
}
|
|
5883
|
+
return mismatch === 0;
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5844
5886
|
// src/roster/roster.ts
|
|
5845
5887
|
function uint32LengthPrefixed(bytes) {
|
|
5846
5888
|
const prefix = new Uint8Array(4);
|
|
@@ -5880,6 +5922,15 @@ function capabilitySetToRosterCapabilities(capabilities) {
|
|
|
5880
5922
|
return capabilitySetToArray(capabilities);
|
|
5881
5923
|
}
|
|
5882
5924
|
|
|
5925
|
+
// src/roster/fingerprint.ts
|
|
5926
|
+
function formatKeyFingerprint(publicKeyHex, groups = 4) {
|
|
5927
|
+
const clusters = [];
|
|
5928
|
+
for (let index = 0; index < groups; index += 1) {
|
|
5929
|
+
clusters.push(publicKeyHex.slice(index * 4, index * 4 + 4));
|
|
5930
|
+
}
|
|
5931
|
+
return clusters.join(" ");
|
|
5932
|
+
}
|
|
5933
|
+
|
|
5883
5934
|
// src/capabilities/board-tools.ts
|
|
5884
5935
|
var BOARD_TOOL_READ_NAMES = [
|
|
5885
5936
|
"search_tasks",
|
|
@@ -6041,6 +6092,7 @@ function isBridgeEvent(value) {
|
|
|
6041
6092
|
encodeProtocolVersion,
|
|
6042
6093
|
encodeRosterEntryForSigning,
|
|
6043
6094
|
findRosterDevice,
|
|
6095
|
+
formatKeyFingerprint,
|
|
6044
6096
|
generateEd25519KeyPair,
|
|
6045
6097
|
generateX25519KeyPair,
|
|
6046
6098
|
hexToBytes,
|
|
@@ -6055,6 +6107,7 @@ function isBridgeEvent(value) {
|
|
|
6055
6107
|
isRecord,
|
|
6056
6108
|
isRosterEntryExpired,
|
|
6057
6109
|
isSecureRelayAddress,
|
|
6110
|
+
openPairingConfirm,
|
|
6058
6111
|
openPushEnvelope,
|
|
6059
6112
|
parseActivityEventPayload,
|
|
6060
6113
|
parseBacklogItemWire,
|
|
@@ -6076,6 +6129,7 @@ function isBridgeEvent(value) {
|
|
|
6076
6129
|
parseTranscriptWindowResponsePayload,
|
|
6077
6130
|
randomBytes,
|
|
6078
6131
|
rosterDeviceCapabilitySet,
|
|
6132
|
+
sealPairingConfirm,
|
|
6079
6133
|
sealPushEnvelope,
|
|
6080
6134
|
signRosterEntry,
|
|
6081
6135
|
unwrapSessionFrame,
|
package/dist/index.d.ts
CHANGED
|
@@ -12,11 +12,13 @@ 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 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';
|
package/dist/index.mjs
CHANGED
|
@@ -5466,19 +5466,34 @@ function parseReadBoardResponsePayload(payload) {
|
|
|
5466
5466
|
if (typeof payload.projectId !== "string") throw new Error('read-board response is missing "projectId"');
|
|
5467
5467
|
if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
|
|
5468
5468
|
if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
|
|
5469
|
-
if (!Array.isArray(payload.backlog))
|
|
5469
|
+
if (payload.backlog !== void 0 && !Array.isArray(payload.backlog)) {
|
|
5470
|
+
throw new Error('read-board response has a non-array "backlog"');
|
|
5471
|
+
}
|
|
5470
5472
|
if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
|
|
5471
5473
|
throw new Error('read-board response has a non-boolean "showTicketNumbers"');
|
|
5472
5474
|
}
|
|
5475
|
+
if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
|
|
5476
|
+
throw new Error('read-board response has an invalid "view"');
|
|
5477
|
+
}
|
|
5473
5478
|
return {
|
|
5474
5479
|
projectId: payload.projectId,
|
|
5475
5480
|
columns: payload.columns.map((column) => parseBoardColumnWire(column)),
|
|
5476
5481
|
tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
|
|
5477
|
-
backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
|
|
5482
|
+
...payload.backlog !== void 0 ? { backlog: payload.backlog.map((item) => parseBacklogItemWire(item)) } : {},
|
|
5478
5483
|
...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
|
|
5479
|
-
...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
|
|
5484
|
+
...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {},
|
|
5485
|
+
...payload.view !== void 0 ? { view: payload.view } : {},
|
|
5486
|
+
...payload.taskCountsByColumnId !== void 0 ? { taskCountsByColumnId: parseTaskCountsByColumnId(payload.taskCountsByColumnId) } : {}
|
|
5480
5487
|
};
|
|
5481
5488
|
}
|
|
5489
|
+
function parseTaskCountsByColumnId(value) {
|
|
5490
|
+
if (!isRecord(value)) throw new Error('read-board response has a non-object "taskCountsByColumnId"');
|
|
5491
|
+
const counts = {};
|
|
5492
|
+
for (const [columnId, count] of Object.entries(value)) {
|
|
5493
|
+
if (typeof count === "number" && Number.isInteger(count) && count >= 0) counts[columnId] = count;
|
|
5494
|
+
}
|
|
5495
|
+
return counts;
|
|
5496
|
+
}
|
|
5482
5497
|
function parseReadBoardRequestPayload(payload) {
|
|
5483
5498
|
if (!isRecord(payload)) throw new Error("read-board payload must be an object");
|
|
5484
5499
|
if (payload.projectId !== void 0 && typeof payload.projectId !== "string") {
|
|
@@ -5487,7 +5502,10 @@ function parseReadBoardRequestPayload(payload) {
|
|
|
5487
5502
|
if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe") {
|
|
5488
5503
|
throw new Error('read-board payload has an invalid "action"');
|
|
5489
5504
|
}
|
|
5490
|
-
|
|
5505
|
+
if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
|
|
5506
|
+
throw new Error('read-board payload has an invalid "view"');
|
|
5507
|
+
}
|
|
5508
|
+
return { projectId: payload.projectId, action: payload.action, view: payload.view };
|
|
5491
5509
|
}
|
|
5492
5510
|
function parseReadDiffResponsePayload(payload) {
|
|
5493
5511
|
if (!isRecord(payload)) throw new Error("read-diff response must be an object");
|
|
@@ -5728,6 +5746,27 @@ function decodePairingQrPayload(uri) {
|
|
|
5728
5746
|
return { desktopStaticPublicKey, pairingToken, relayAddress, expiresAt, protocolVersion };
|
|
5729
5747
|
}
|
|
5730
5748
|
|
|
5749
|
+
// src/pairing/confirm.ts
|
|
5750
|
+
var PAIRING_CONFIRM_PLAINTEXT = new TextEncoder().encode("kangentic-pairing-confirm-v1");
|
|
5751
|
+
var EMPTY_ASSOCIATED_DATA = new Uint8Array(0);
|
|
5752
|
+
function sealPairingConfirm(initiatorToResponder) {
|
|
5753
|
+
return initiatorToResponder.encryptWithAd(EMPTY_ASSOCIATED_DATA, PAIRING_CONFIRM_PLAINTEXT);
|
|
5754
|
+
}
|
|
5755
|
+
function openPairingConfirm(initiatorToResponder, frame) {
|
|
5756
|
+
let plaintext;
|
|
5757
|
+
try {
|
|
5758
|
+
plaintext = initiatorToResponder.decryptWithAd(EMPTY_ASSOCIATED_DATA, frame);
|
|
5759
|
+
} catch {
|
|
5760
|
+
return false;
|
|
5761
|
+
}
|
|
5762
|
+
if (plaintext.length !== PAIRING_CONFIRM_PLAINTEXT.length) return false;
|
|
5763
|
+
let mismatch = 0;
|
|
5764
|
+
for (let index = 0; index < plaintext.length; index += 1) {
|
|
5765
|
+
mismatch |= plaintext[index] ^ PAIRING_CONFIRM_PLAINTEXT[index];
|
|
5766
|
+
}
|
|
5767
|
+
return mismatch === 0;
|
|
5768
|
+
}
|
|
5769
|
+
|
|
5731
5770
|
// src/roster/roster.ts
|
|
5732
5771
|
function uint32LengthPrefixed(bytes) {
|
|
5733
5772
|
const prefix = new Uint8Array(4);
|
|
@@ -5767,6 +5806,15 @@ function capabilitySetToRosterCapabilities(capabilities) {
|
|
|
5767
5806
|
return capabilitySetToArray(capabilities);
|
|
5768
5807
|
}
|
|
5769
5808
|
|
|
5809
|
+
// src/roster/fingerprint.ts
|
|
5810
|
+
function formatKeyFingerprint(publicKeyHex, groups = 4) {
|
|
5811
|
+
const clusters = [];
|
|
5812
|
+
for (let index = 0; index < groups; index += 1) {
|
|
5813
|
+
clusters.push(publicKeyHex.slice(index * 4, index * 4 + 4));
|
|
5814
|
+
}
|
|
5815
|
+
return clusters.join(" ");
|
|
5816
|
+
}
|
|
5817
|
+
|
|
5770
5818
|
// src/capabilities/board-tools.ts
|
|
5771
5819
|
var BOARD_TOOL_READ_NAMES = [
|
|
5772
5820
|
"search_tasks",
|
|
@@ -5927,6 +5975,7 @@ export {
|
|
|
5927
5975
|
encodeProtocolVersion,
|
|
5928
5976
|
encodeRosterEntryForSigning,
|
|
5929
5977
|
findRosterDevice,
|
|
5978
|
+
formatKeyFingerprint,
|
|
5930
5979
|
generateEd25519KeyPair,
|
|
5931
5980
|
generateX25519KeyPair,
|
|
5932
5981
|
hexToBytes,
|
|
@@ -5941,6 +5990,7 @@ export {
|
|
|
5941
5990
|
isRecord,
|
|
5942
5991
|
isRosterEntryExpired,
|
|
5943
5992
|
isSecureRelayAddress,
|
|
5993
|
+
openPairingConfirm,
|
|
5944
5994
|
openPushEnvelope,
|
|
5945
5995
|
parseActivityEventPayload,
|
|
5946
5996
|
parseBacklogItemWire,
|
|
@@ -5962,6 +6012,7 @@ export {
|
|
|
5962
6012
|
parseTranscriptWindowResponsePayload,
|
|
5963
6013
|
randomBytes4 as randomBytes,
|
|
5964
6014
|
rosterDeviceCapabilitySet,
|
|
6015
|
+
sealPairingConfirm,
|
|
5965
6016
|
sealPushEnvelope,
|
|
5966
6017
|
signRosterEntry,
|
|
5967
6018
|
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
|
@@ -99,10 +99,41 @@ 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
119
|
/** Defaults to 'subscribe' when omitted. 'unsubscribe' only has an effect when projectId is set - the no-projectId project list is a one-shot read with no live feed to tear down. */
|
|
105
120
|
action?: 'subscribe' | 'unsubscribe';
|
|
121
|
+
/**
|
|
122
|
+
* subscribe only: the projection wanted (protocol 0.9.0). Omitted means the
|
|
123
|
+
* legacy full board WITH the backlog.
|
|
124
|
+
*
|
|
125
|
+
* Measured on a 15-project desktop, the full boards were 63 kB compressed
|
|
126
|
+
* of a ~96 kB cold start, of which 23 kB was a backlog no phone has ever
|
|
127
|
+
* rendered and another 30 kB was tasks with no session on them. Worse, a
|
|
128
|
+
* board subscription re-snapshots on every board change, so that payload
|
|
129
|
+
* repeats for as long as the phone is connected. The 'sessions' projection
|
|
130
|
+
* is 12 kB for the same 15 projects.
|
|
131
|
+
*
|
|
132
|
+
* A pre-0.9.0 desktop ignores the field and returns a full board with no
|
|
133
|
+
* `view` echo, which is exactly how a 0.9.0 phone detects that it must not
|
|
134
|
+
* treat the snapshot as filtered.
|
|
135
|
+
*/
|
|
136
|
+
view?: ReadBoardView;
|
|
106
137
|
}
|
|
107
138
|
export interface ReadBoardProjectSummary {
|
|
108
139
|
id: string;
|
|
@@ -123,8 +154,10 @@ export interface ReadBoardProjectListResponsePayload {
|
|
|
123
154
|
export interface ReadBoardSnapshotResponsePayload {
|
|
124
155
|
projectId: string;
|
|
125
156
|
columns: BoardColumnWire[];
|
|
157
|
+
/** Non-archived tasks; filtered to the ones carrying a `session_id` when `view` is 'sessions'. */
|
|
126
158
|
tasks: BoardTaskWire[];
|
|
127
|
-
|
|
159
|
+
/** Absent whenever the request carried a `view` (protocol 0.9.0) - no phone has ever rendered it. */
|
|
160
|
+
backlog?: BacklogItemWire[];
|
|
128
161
|
/** The project's accent color ("#rrggbb"); same semantics as ReadBoardProjectSummary.color. Absent from pre-0.5.0 desktops. */
|
|
129
162
|
projectColor?: string;
|
|
130
163
|
/**
|
|
@@ -133,6 +166,18 @@ export interface ReadBoardSnapshotResponsePayload {
|
|
|
133
166
|
* true, the desktop default).
|
|
134
167
|
*/
|
|
135
168
|
showTicketNumbers?: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* The projection actually applied, echoed back. Absent from a pre-0.9.0
|
|
171
|
+
* desktop, which always sends a full board - so treat an absent `view` as
|
|
172
|
+
* 'full', never as "filtered, unknown how".
|
|
173
|
+
*/
|
|
174
|
+
view?: ReadBoardView;
|
|
175
|
+
/**
|
|
176
|
+
* Non-archived task count per column id, sent with a 'sessions' view because
|
|
177
|
+
* `tasks` is filtered and a count taken from it would be wrong. Appending a
|
|
178
|
+
* card to a column needs the real length.
|
|
179
|
+
*/
|
|
180
|
+
taskCountsByColumnId?: Record<string, number>;
|
|
136
181
|
}
|
|
137
182
|
export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload;
|
|
138
183
|
/** Phone-side narrowing of a read-board response (project list or board snapshot). Throws on a malformed required field. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kangentic/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|