@kangentic/protocol 0.10.0 → 0.11.1
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/index.cjs +33 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +33 -8
- package/dist/pairing/relay-address.d.ts +12 -6
- package/dist/wire/payloads.d.ts +25 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -5607,10 +5607,15 @@ function parseReadBoardResponsePayload(payload) {
|
|
|
5607
5607
|
return {
|
|
5608
5608
|
id: project.id,
|
|
5609
5609
|
name: project.name,
|
|
5610
|
-
...project.color !== void 0 ? { color: parseAccentColor(project.color, `read-board project ${index}`) } : {}
|
|
5610
|
+
...project.color !== void 0 ? { color: parseAccentColor(project.color, `read-board project ${index}`) } : {},
|
|
5611
|
+
...typeof project.groupId === "string" ? { groupId: project.groupId } : {},
|
|
5612
|
+
...typeof project.position === "number" ? { position: project.position } : {}
|
|
5611
5613
|
};
|
|
5612
5614
|
});
|
|
5613
|
-
|
|
5615
|
+
const groups = Array.isArray(payload.groups) ? payload.groups.flatMap(
|
|
5616
|
+
(group) => isRecord(group) && typeof group.id === "string" && typeof group.name === "string" && typeof group.position === "number" ? [{ id: group.id, name: group.name, position: group.position }] : []
|
|
5617
|
+
) : void 0;
|
|
5618
|
+
return { projects, ...groups !== void 0 ? { groups } : {} };
|
|
5614
5619
|
}
|
|
5615
5620
|
if (typeof payload.projectId !== "string") throw new Error('read-board response is missing "projectId"');
|
|
5616
5621
|
if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
|
|
@@ -5828,14 +5833,34 @@ function parseCapabilityRequestPayload(verb, payload) {
|
|
|
5828
5833
|
|
|
5829
5834
|
// src/pairing/relay-address.ts
|
|
5830
5835
|
var MAX_RELAY_ADDRESS_LENGTH = 512;
|
|
5831
|
-
var
|
|
5836
|
+
var LOOPBACK_WS_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
|
|
5837
|
+
var PLAINTEXT_SCHEME = "ws://";
|
|
5838
|
+
function hostWithoutPort(authority) {
|
|
5839
|
+
if (authority.startsWith("[")) {
|
|
5840
|
+
const closingBracket = authority.indexOf("]");
|
|
5841
|
+
if (closingBracket === -1) return null;
|
|
5842
|
+
const afterLiteral = authority.slice(closingBracket + 1);
|
|
5843
|
+
if (afterLiteral !== "" && !/^:[0-9]+$/.test(afterLiteral)) return null;
|
|
5844
|
+
return authority.slice(0, closingBracket + 1);
|
|
5845
|
+
}
|
|
5846
|
+
const portSeparator = authority.lastIndexOf(":");
|
|
5847
|
+
if (portSeparator === -1) return authority;
|
|
5848
|
+
const port = authority.slice(portSeparator + 1);
|
|
5849
|
+
return /^[0-9]+$/.test(port) ? authority.slice(0, portSeparator) : authority;
|
|
5850
|
+
}
|
|
5851
|
+
function plaintextRelayHost(relayAddress) {
|
|
5852
|
+
if (!relayAddress.startsWith(PLAINTEXT_SCHEME)) return null;
|
|
5853
|
+
const afterScheme = relayAddress.slice(PLAINTEXT_SCHEME.length);
|
|
5854
|
+
const authorityEnd = afterScheme.search(/[/?#]/);
|
|
5855
|
+
const authority = authorityEnd === -1 ? afterScheme : afterScheme.slice(0, authorityEnd);
|
|
5856
|
+
if (authority.includes("@")) return null;
|
|
5857
|
+
const host = hostWithoutPort(authority);
|
|
5858
|
+
return host === null ? null : host.toLowerCase();
|
|
5859
|
+
}
|
|
5832
5860
|
function isSecureRelayAddress(relayAddress) {
|
|
5833
5861
|
if (relayAddress.startsWith("wss://")) return true;
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
const boundaryChar = relayAddress.charAt(prefix.length);
|
|
5837
|
-
return boundaryChar === "" || boundaryChar === ":" || boundaryChar === "/" || boundaryChar === "?" || boundaryChar === "#";
|
|
5838
|
-
});
|
|
5862
|
+
const host = plaintextRelayHost(relayAddress);
|
|
5863
|
+
return host !== null && LOOPBACK_WS_HOSTS.includes(host);
|
|
5839
5864
|
}
|
|
5840
5865
|
|
|
5841
5866
|
// src/pairing/qr.ts
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ 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 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';
|
|
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 ReadBoardProjectGroup, 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
18
|
export { sealPairingConfirm, openPairingConfirm } from './pairing/confirm';
|
package/dist/index.mjs
CHANGED
|
@@ -5490,10 +5490,15 @@ function parseReadBoardResponsePayload(payload) {
|
|
|
5490
5490
|
return {
|
|
5491
5491
|
id: project.id,
|
|
5492
5492
|
name: project.name,
|
|
5493
|
-
...project.color !== void 0 ? { color: parseAccentColor(project.color, `read-board project ${index}`) } : {}
|
|
5493
|
+
...project.color !== void 0 ? { color: parseAccentColor(project.color, `read-board project ${index}`) } : {},
|
|
5494
|
+
...typeof project.groupId === "string" ? { groupId: project.groupId } : {},
|
|
5495
|
+
...typeof project.position === "number" ? { position: project.position } : {}
|
|
5494
5496
|
};
|
|
5495
5497
|
});
|
|
5496
|
-
|
|
5498
|
+
const groups = Array.isArray(payload.groups) ? payload.groups.flatMap(
|
|
5499
|
+
(group) => isRecord(group) && typeof group.id === "string" && typeof group.name === "string" && typeof group.position === "number" ? [{ id: group.id, name: group.name, position: group.position }] : []
|
|
5500
|
+
) : void 0;
|
|
5501
|
+
return { projects, ...groups !== void 0 ? { groups } : {} };
|
|
5497
5502
|
}
|
|
5498
5503
|
if (typeof payload.projectId !== "string") throw new Error('read-board response is missing "projectId"');
|
|
5499
5504
|
if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
|
|
@@ -5711,14 +5716,34 @@ function parseCapabilityRequestPayload(verb, payload) {
|
|
|
5711
5716
|
|
|
5712
5717
|
// src/pairing/relay-address.ts
|
|
5713
5718
|
var MAX_RELAY_ADDRESS_LENGTH = 512;
|
|
5714
|
-
var
|
|
5719
|
+
var LOOPBACK_WS_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
|
|
5720
|
+
var PLAINTEXT_SCHEME = "ws://";
|
|
5721
|
+
function hostWithoutPort(authority) {
|
|
5722
|
+
if (authority.startsWith("[")) {
|
|
5723
|
+
const closingBracket = authority.indexOf("]");
|
|
5724
|
+
if (closingBracket === -1) return null;
|
|
5725
|
+
const afterLiteral = authority.slice(closingBracket + 1);
|
|
5726
|
+
if (afterLiteral !== "" && !/^:[0-9]+$/.test(afterLiteral)) return null;
|
|
5727
|
+
return authority.slice(0, closingBracket + 1);
|
|
5728
|
+
}
|
|
5729
|
+
const portSeparator = authority.lastIndexOf(":");
|
|
5730
|
+
if (portSeparator === -1) return authority;
|
|
5731
|
+
const port = authority.slice(portSeparator + 1);
|
|
5732
|
+
return /^[0-9]+$/.test(port) ? authority.slice(0, portSeparator) : authority;
|
|
5733
|
+
}
|
|
5734
|
+
function plaintextRelayHost(relayAddress) {
|
|
5735
|
+
if (!relayAddress.startsWith(PLAINTEXT_SCHEME)) return null;
|
|
5736
|
+
const afterScheme = relayAddress.slice(PLAINTEXT_SCHEME.length);
|
|
5737
|
+
const authorityEnd = afterScheme.search(/[/?#]/);
|
|
5738
|
+
const authority = authorityEnd === -1 ? afterScheme : afterScheme.slice(0, authorityEnd);
|
|
5739
|
+
if (authority.includes("@")) return null;
|
|
5740
|
+
const host = hostWithoutPort(authority);
|
|
5741
|
+
return host === null ? null : host.toLowerCase();
|
|
5742
|
+
}
|
|
5715
5743
|
function isSecureRelayAddress(relayAddress) {
|
|
5716
5744
|
if (relayAddress.startsWith("wss://")) return true;
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
const boundaryChar = relayAddress.charAt(prefix.length);
|
|
5720
|
-
return boundaryChar === "" || boundaryChar === ":" || boundaryChar === "/" || boundaryChar === "?" || boundaryChar === "#";
|
|
5721
|
-
});
|
|
5745
|
+
const host = plaintextRelayHost(relayAddress);
|
|
5746
|
+
return host !== null && LOOPBACK_WS_HOSTS.includes(host);
|
|
5722
5747
|
}
|
|
5723
5748
|
|
|
5724
5749
|
// src/pairing/qr.ts
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relay address rules
|
|
3
|
-
*
|
|
2
|
+
* Relay address rules, mirrored by the mobile app's src/pairing/qr.ts (which
|
|
3
|
+
* hardened these rules first; this module now matches it). This module
|
|
4
|
+
* imports nothing on purpose: it is
|
|
4
5
|
* deep-imported by the desktop's src/shared/relay.ts, which the RENDERER
|
|
5
6
|
* bundles, and pulling in the rest of this package (crypto/primitives ->
|
|
6
7
|
* @noble/*) would drag Noise crypto into that bundle for no reason. Keep it
|
|
@@ -10,9 +11,14 @@ export declare const MAX_RELAY_ADDRESS_LENGTH = 512;
|
|
|
10
11
|
/**
|
|
11
12
|
* True if the phone's pairing QR scanner will accept this relay address.
|
|
12
13
|
* wss:// is always secure; ws:// is accepted only for loopback, since the
|
|
13
|
-
* pairing token doubles as the Noise PSK and is dialed verbatim as
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* pairing token doubles as the Noise PSK and is dialed verbatim as `?slot=`.
|
|
15
|
+
*
|
|
16
|
+
* Parses the AUTHORITY rather than prefix-matching. Prefix matching accepted
|
|
17
|
+
* `ws://127.0.0.1:8080@evil.test`: everything before an '@' is userinfo, so
|
|
18
|
+
* that address dials evil.test while looking like loopback, putting the PSK
|
|
19
|
+
* on the wire in cleartext to an attacker-chosen host - and the pairing then
|
|
20
|
+
* persists that host to the trust anchor for every later session. Authority
|
|
21
|
+
* parsing also subsumes the old boundary check, since `localhost.evil.com`
|
|
22
|
+
* is simply a different host.
|
|
17
23
|
*/
|
|
18
24
|
export declare function isSecureRelayAddress(relayAddress: string): boolean;
|
package/dist/wire/payloads.d.ts
CHANGED
|
@@ -151,9 +151,28 @@ export interface ReadBoardRequestPayload {
|
|
|
151
151
|
*/
|
|
152
152
|
view?: ReadBoardView;
|
|
153
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* A desktop project group ("KANGENTIC", "TROY WEB"), for a phone rendering
|
|
156
|
+
* its project list with the same structure the desktop sidebar shows
|
|
157
|
+
* (protocol 0.11.0).
|
|
158
|
+
*/
|
|
159
|
+
export interface ReadBoardProjectGroup {
|
|
160
|
+
id: string;
|
|
161
|
+
name: string;
|
|
162
|
+
/** Display order among groups. */
|
|
163
|
+
position: number;
|
|
164
|
+
}
|
|
154
165
|
export interface ReadBoardProjectSummary {
|
|
155
166
|
id: string;
|
|
156
167
|
name: string;
|
|
168
|
+
/**
|
|
169
|
+
* The group this project belongs to, or null/absent for an ungrouped one
|
|
170
|
+
* (protocol 0.11.0). Absent from an older desktop, which is indistinguishable
|
|
171
|
+
* from ungrouped and renders the same flat list as before.
|
|
172
|
+
*/
|
|
173
|
+
groupId?: string | null;
|
|
174
|
+
/** Display order within the group, mirroring the desktop's own ordering (protocol 0.11.0). */
|
|
175
|
+
position?: number;
|
|
157
176
|
/**
|
|
158
177
|
* Accent color for this project ("#rrggbb"). Today the desktop derives
|
|
159
178
|
* it deterministically from the project id; a user-set project color
|
|
@@ -165,6 +184,12 @@ export interface ReadBoardProjectSummary {
|
|
|
165
184
|
/** Returned when the request omits projectId - the phone's project-bootstrap listing. */
|
|
166
185
|
export interface ReadBoardProjectListResponsePayload {
|
|
167
186
|
projects: ReadBoardProjectSummary[];
|
|
187
|
+
/**
|
|
188
|
+
* The desktop's project groups, in display order (protocol 0.11.0). Absent
|
|
189
|
+
* from an older desktop; a phone that gets none renders one flat list, which
|
|
190
|
+
* is exactly the pre-0.11.0 behaviour.
|
|
191
|
+
*/
|
|
192
|
+
groups?: ReadBoardProjectGroup[];
|
|
168
193
|
}
|
|
169
194
|
/** Returned when the request carries a projectId - a snapshot of that project's board. */
|
|
170
195
|
export interface ReadBoardSnapshotResponsePayload {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kangentic/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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",
|