@kangentic/protocol 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crypto/slot.d.ts +16 -0
- package/dist/events/payloads.d.ts +72 -1
- package/dist/index.cjs +33 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +29 -2
- package/dist/pairing/confirm.d.ts +17 -2
- package/dist/pairing/relay-address.d.ts +16 -7
- package/dist/version.d.ts +10 -2
- package/package.json +1 -1
package/dist/crypto/slot.d.ts
CHANGED
|
@@ -1 +1,17 @@
|
|
|
1
1
|
export declare function deriveSessionSlotId(desktopStaticPublicKey: Uint8Array, phoneStaticPublicKey: Uint8Array): string;
|
|
2
|
+
/**
|
|
3
|
+
* Derives the relay rendezvous slot id for the PAIRING ceremony from the
|
|
4
|
+
* pairing token, for both peers to dial.
|
|
5
|
+
*
|
|
6
|
+
* The slot must be derived rather than being the token itself, because it
|
|
7
|
+
* travels in cleartext in the relay URL's query string while the same token is
|
|
8
|
+
* the Noise IKpsk0 pre-shared key. Dialing it verbatim published the PSK to
|
|
9
|
+
* every hop that can read a request URI - on a hosted relay that includes
|
|
10
|
+
* whatever terminates TLS - which reduced IKpsk0 to plain IK for such an
|
|
11
|
+
* observer. A one-way labeled hash keeps the routing label public and the PSK
|
|
12
|
+
* secret: the token itself now never leaves the QR code.
|
|
13
|
+
*
|
|
14
|
+
* The result is a routing label, NOT key material. Its 128 bits are sized to
|
|
15
|
+
* defeat blind enumeration, which is all a rendezvous label has to do.
|
|
16
|
+
*/
|
|
17
|
+
export declare function derivePairingSlotId(pairingToken: Uint8Array): string;
|
|
@@ -217,17 +217,80 @@ export interface SessionEventWire {
|
|
|
217
217
|
toolId?: string;
|
|
218
218
|
detail?: string;
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* The system column roles, mirroring the desktop's `SWIMLANE_ROLES` /
|
|
222
|
+
* `SwimlaneRole` (`src/shared/types.ts`). This package cannot import that
|
|
223
|
+
* file (see the module doc), so the two lists are a deliberate mirror, kept
|
|
224
|
+
* in sync by `tests/unit/protocol/board-column-role-parity.test.ts` on the
|
|
225
|
+
* desktop side rather than by a shared import.
|
|
226
|
+
*/
|
|
227
|
+
export declare const BOARD_COLUMN_ROLES: readonly ["todo", "done"];
|
|
228
|
+
export type BoardColumnRoleWire = (typeof BOARD_COLUMN_ROLES)[number];
|
|
229
|
+
/** True for a column whose role is the system 'todo' role. Prefer this at a call site over a literal `=== 'todo'`, which the `(string & {})` widening leaves unchecked. */
|
|
230
|
+
export declare function isTodoRole(role: string | null | undefined): boolean;
|
|
231
|
+
/** True for a column whose role is the system 'done' role. Prefer this at a call site over a literal `=== 'done'`, which the `(string & {})` widening leaves unchecked. */
|
|
232
|
+
export declare function isDoneRole(role: string | null | undefined): boolean;
|
|
220
233
|
/** Phone-needed subset of the desktop's Swimlane row (snake_case preserved so the desktop mapper is a mechanical pick). */
|
|
221
234
|
export interface BoardColumnWire {
|
|
222
235
|
id: string;
|
|
223
236
|
name: string;
|
|
224
237
|
description: string | null;
|
|
225
|
-
|
|
238
|
+
/**
|
|
239
|
+
* `'todo'` / `'done'` are the system roles; anything else (including a
|
|
240
|
+
* future role this package predates) is a custom or not-yet-known column,
|
|
241
|
+
* which is exactly what the `(string & {})` escape hatch preserves - the
|
|
242
|
+
* literal union alone would reject a value a newer desktop legitimately
|
|
243
|
+
* sends. That widening does NOT make a typo'd comparison a compile error
|
|
244
|
+
* (a string literal is assignable to `string & {}`), so call sites should
|
|
245
|
+
* use `isTodoRole` / `isDoneRole` rather than `=== 'todo'` / `=== 'done'`.
|
|
246
|
+
*/
|
|
247
|
+
role: BoardColumnRoleWire | (string & {}) | null;
|
|
226
248
|
position: number;
|
|
227
249
|
color: string;
|
|
228
250
|
icon: string | null;
|
|
229
251
|
is_archived: boolean;
|
|
230
252
|
is_ghost: boolean;
|
|
253
|
+
/**
|
|
254
|
+
* Whether moving a task into this column spawns a successor agent
|
|
255
|
+
* session, derived desktop-side from the column's `auto_spawn` flag and
|
|
256
|
+
* role. This is INTENT, not a guarantee:
|
|
257
|
+
*
|
|
258
|
+
* - `false` - this column does not intend to spawn. Reliable enough to act
|
|
259
|
+
* on ONLY when the column's own `role` is 'todo' or 'done': those two
|
|
260
|
+
* produce a full reset or an archive, and a Board Profile cannot change a
|
|
261
|
+
* column's role, so a client MAY skip a session-swap transition entirely
|
|
262
|
+
* for them. `role` rides this same object, so
|
|
263
|
+
* `isTodoRole(column.role) || isDoneRole(column.role)` is how a client
|
|
264
|
+
* recognizes that case. A `false` that came from `auto_spawn: false` on
|
|
265
|
+
* any other column is intent only, exactly like `true`: a Board Profile
|
|
266
|
+
* can set `autoSpawn: true` for one task on that column, and the move
|
|
267
|
+
* then does spawn.
|
|
268
|
+
* - `true` - this column intends to spawn, but a handful of desktop-side
|
|
269
|
+
* conditions can suspend the old session without a successor actually
|
|
270
|
+
* landing (a board profile disabling auto_spawn for this task, a failed
|
|
271
|
+
* worktree checkout that reverts the move, a shutdown or a newer move
|
|
272
|
+
* racing this one). A client MUST NOT treat `true` as a promise; keep
|
|
273
|
+
* whatever timeout already bounds a session-swap wait and let `true`
|
|
274
|
+
* only skip a redundant one.
|
|
275
|
+
* - `null` / absent - a desktop that predates this field. A client falls
|
|
276
|
+
* back to its pre-existing behavior, the same way an absent
|
|
277
|
+
* `pr_merge_readiness` reads as "never judged".
|
|
278
|
+
*
|
|
279
|
+
* Declared OPTIONAL (`?`) so a hand-built `BoardColumnWire` literal does
|
|
280
|
+
* not have to list it. `parseBoardColumnWire` populates the key from every
|
|
281
|
+
* real wire response regardless, so the optionality costs nothing at
|
|
282
|
+
* runtime. `docs/mobile-bridge.md` records why this differs from
|
|
283
|
+
* `pr_merge_readiness`, which shipped required.
|
|
284
|
+
*
|
|
285
|
+
* Ordering precondition this field depends on: the phone must learn a
|
|
286
|
+
* MOVE's destination column before the old session's `session-ended`
|
|
287
|
+
* arrives, or it cannot resolve `spawns_session` against the right column.
|
|
288
|
+
* That holds today because `handleTaskMove` emits its board-changed event
|
|
289
|
+
* at the commit point rather than at settle time. See the comment above
|
|
290
|
+
* `emitBoardChanged()` in `src/main/ipc/handlers/task-move.ts`, and
|
|
291
|
+
* `docs/mobile-bridge.md`.
|
|
292
|
+
*/
|
|
293
|
+
spawns_session?: boolean | null;
|
|
231
294
|
}
|
|
232
295
|
/** Phone-needed subset of the desktop's Task row. A non-null `session_id` is the live-session signal the phone's triage view keys on. */
|
|
233
296
|
export interface BoardTaskWire {
|
|
@@ -244,6 +307,14 @@ export interface BoardTaskWire {
|
|
|
244
307
|
pr_number: number | null;
|
|
245
308
|
pr_url: string | null;
|
|
246
309
|
pr_state: string | null;
|
|
310
|
+
/**
|
|
311
|
+
* Normalized merge readiness of the linked PR: `ready` / `blocked` /
|
|
312
|
+
* `conflicting` / `queued` / `running` / `unknown`, or null when never
|
|
313
|
+
* judged. `queued` and `running` mean a blocking check is still in flight.
|
|
314
|
+
* Absent from desktops that predate it, which the parser reads as null; a
|
|
315
|
+
* value a client does not know should render as plain open.
|
|
316
|
+
*/
|
|
317
|
+
pr_merge_readiness: string | null;
|
|
247
318
|
base_branch: string | null;
|
|
248
319
|
labels: string[];
|
|
249
320
|
priority: number;
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: tr
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
BOARD_COLUMN_ROLES: () => BOARD_COLUMN_ROLES,
|
|
23
24
|
BOARD_TOOL_READ_NAMES: () => BOARD_TOOL_READ_NAMES,
|
|
24
25
|
BOARD_TOOL_WRITE_NAMES: () => BOARD_TOOL_WRITE_NAMES,
|
|
25
26
|
CAPABILITY_VERBS: () => CAPABILITY_VERBS,
|
|
@@ -50,6 +51,7 @@ __export(index_exports, {
|
|
|
50
51
|
createPairingResponderHandshake: () => createPairingResponderHandshake,
|
|
51
52
|
decodeMessage: () => decodeMessage,
|
|
52
53
|
decodePairingQrPayload: () => decodePairingQrPayload,
|
|
54
|
+
derivePairingSlotId: () => derivePairingSlotId,
|
|
53
55
|
deriveSecretstreamPair: () => deriveSecretstreamPair,
|
|
54
56
|
deriveSessionSlotId: () => deriveSessionSlotId,
|
|
55
57
|
deriveShortAuthenticationString: () => deriveShortAuthenticationString,
|
|
@@ -68,11 +70,13 @@ __export(index_exports, {
|
|
|
68
70
|
isBoardToolWriteName: () => isBoardToolWriteName,
|
|
69
71
|
isBridgeEvent: () => isBridgeEvent,
|
|
70
72
|
isCapabilityVerb: () => isCapabilityVerb,
|
|
73
|
+
isDoneRole: () => isDoneRole,
|
|
71
74
|
isJsonValue: () => isJsonValue,
|
|
72
75
|
isPushCategory: () => isPushCategory,
|
|
73
76
|
isRecord: () => isRecord,
|
|
74
77
|
isRosterEntryExpired: () => isRosterEntryExpired,
|
|
75
78
|
isSecureRelayAddress: () => isSecureRelayAddress,
|
|
79
|
+
isTodoRole: () => isTodoRole,
|
|
76
80
|
openPairingConfirm: () => openPairingConfirm,
|
|
77
81
|
openPushEnvelope: () => openPushEnvelope,
|
|
78
82
|
parseActivityEventPayload: () => parseActivityEventPayload,
|
|
@@ -107,7 +111,7 @@ __export(index_exports, {
|
|
|
107
111
|
module.exports = __toCommonJS(index_exports);
|
|
108
112
|
|
|
109
113
|
// src/version.ts
|
|
110
|
-
var PROTOCOL_VERSION = "
|
|
114
|
+
var PROTOCOL_VERSION = "3";
|
|
111
115
|
function encodeProtocolVersion(version = PROTOCOL_VERSION) {
|
|
112
116
|
return new TextEncoder().encode(`kangentic-bridge-v${version}`);
|
|
113
117
|
}
|
|
@@ -4285,6 +4289,11 @@ function deriveSessionSlotId(desktopStaticPublicKey, phoneStaticPublicKey) {
|
|
|
4285
4289
|
const material = concatBytes4(desktopStaticPublicKey, phoneStaticPublicKey);
|
|
4286
4290
|
return bytesToHex(deriveLabeledKey(material, SESSION_SLOT_LABEL, SESSION_SLOT_LENGTH));
|
|
4287
4291
|
}
|
|
4292
|
+
var PAIRING_SLOT_LABEL = "kangentic-pairing-slot-v1";
|
|
4293
|
+
var PAIRING_SLOT_LENGTH = 16;
|
|
4294
|
+
function derivePairingSlotId(pairingToken) {
|
|
4295
|
+
return bytesToHex(deriveLabeledKey(pairingToken, PAIRING_SLOT_LABEL, PAIRING_SLOT_LENGTH));
|
|
4296
|
+
}
|
|
4288
4297
|
|
|
4289
4298
|
// ../../node_modules/fflate/esm/index.mjs
|
|
4290
4299
|
var import_module = require("module");
|
|
@@ -5243,6 +5252,15 @@ function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
|
|
|
5243
5252
|
}
|
|
5244
5253
|
|
|
5245
5254
|
// src/events/payloads.ts
|
|
5255
|
+
var BOARD_COLUMN_ROLES = ["todo", "done"];
|
|
5256
|
+
var TODO_ROLE = "todo";
|
|
5257
|
+
var DONE_ROLE = "done";
|
|
5258
|
+
function isTodoRole(role) {
|
|
5259
|
+
return role === TODO_ROLE;
|
|
5260
|
+
}
|
|
5261
|
+
function isDoneRole(role) {
|
|
5262
|
+
return role === DONE_ROLE;
|
|
5263
|
+
}
|
|
5246
5264
|
var DIFF_FILE_STATUSES = ["A", "M", "D", "R", "C", "U"];
|
|
5247
5265
|
var TRANSCRIPT_SYSTEM_SUBTYPES = ["compaction", "command", "command_output", "session_boundary"];
|
|
5248
5266
|
function requireString(record, field, context) {
|
|
@@ -5409,18 +5427,26 @@ function nullableNumber(record, field) {
|
|
|
5409
5427
|
const value = record[field];
|
|
5410
5428
|
return typeof value === "number" ? value : null;
|
|
5411
5429
|
}
|
|
5430
|
+
function nullableBoolean(record, field) {
|
|
5431
|
+
const value = record[field];
|
|
5432
|
+
return typeof value === "boolean" ? value : null;
|
|
5433
|
+
}
|
|
5412
5434
|
function parseBoardColumnWire(value) {
|
|
5413
5435
|
if (!isRecord(value)) throw new Error("board column must be an object");
|
|
5414
5436
|
return {
|
|
5415
5437
|
id: requireString(value, "id", "board column"),
|
|
5416
5438
|
name: requireString(value, "name", "board column"),
|
|
5417
5439
|
description: nullableString(value, "description"),
|
|
5440
|
+
// An unrecognized role passes through rather than throwing: it is a
|
|
5441
|
+
// custom column (or a future system role this package predates), not a
|
|
5442
|
+
// malformed payload. See the BoardColumnRoleWire doc comment.
|
|
5418
5443
|
role: nullableString(value, "role"),
|
|
5419
5444
|
position: requireNumber(value, "position", "board column"),
|
|
5420
5445
|
color: requireString(value, "color", "board column"),
|
|
5421
5446
|
icon: nullableString(value, "icon"),
|
|
5422
5447
|
is_archived: requireBoolean(value, "is_archived", "board column"),
|
|
5423
|
-
is_ghost: requireBoolean(value, "is_ghost", "board column")
|
|
5448
|
+
is_ghost: requireBoolean(value, "is_ghost", "board column"),
|
|
5449
|
+
spawns_session: nullableBoolean(value, "spawns_session")
|
|
5424
5450
|
};
|
|
5425
5451
|
}
|
|
5426
5452
|
function parseBoardTaskWire(value) {
|
|
@@ -5439,6 +5465,7 @@ function parseBoardTaskWire(value) {
|
|
|
5439
5465
|
pr_number: nullableNumber(value, "pr_number"),
|
|
5440
5466
|
pr_url: nullableString(value, "pr_url"),
|
|
5441
5467
|
pr_state: nullableString(value, "pr_state"),
|
|
5468
|
+
pr_merge_readiness: nullableString(value, "pr_merge_readiness"),
|
|
5442
5469
|
base_branch: nullableString(value, "base_branch"),
|
|
5443
5470
|
labels: Array.isArray(value.labels) ? parseStringArray(value.labels, "board task labels") : [],
|
|
5444
5471
|
priority: typeof value.priority === "number" ? value.priority : 0,
|
|
@@ -6140,6 +6167,7 @@ function isBridgeEvent(value) {
|
|
|
6140
6167
|
}
|
|
6141
6168
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6142
6169
|
0 && (module.exports = {
|
|
6170
|
+
BOARD_COLUMN_ROLES,
|
|
6143
6171
|
BOARD_TOOL_READ_NAMES,
|
|
6144
6172
|
BOARD_TOOL_WRITE_NAMES,
|
|
6145
6173
|
CAPABILITY_VERBS,
|
|
@@ -6170,6 +6198,7 @@ function isBridgeEvent(value) {
|
|
|
6170
6198
|
createPairingResponderHandshake,
|
|
6171
6199
|
decodeMessage,
|
|
6172
6200
|
decodePairingQrPayload,
|
|
6201
|
+
derivePairingSlotId,
|
|
6173
6202
|
deriveSecretstreamPair,
|
|
6174
6203
|
deriveSessionSlotId,
|
|
6175
6204
|
deriveShortAuthenticationString,
|
|
@@ -6188,11 +6217,13 @@ function isBridgeEvent(value) {
|
|
|
6188
6217
|
isBoardToolWriteName,
|
|
6189
6218
|
isBridgeEvent,
|
|
6190
6219
|
isCapabilityVerb,
|
|
6220
|
+
isDoneRole,
|
|
6191
6221
|
isJsonValue,
|
|
6192
6222
|
isPushCategory,
|
|
6193
6223
|
isRecord,
|
|
6194
6224
|
isRosterEntryExpired,
|
|
6195
6225
|
isSecureRelayAddress,
|
|
6226
|
+
isTodoRole,
|
|
6196
6227
|
openPairingConfirm,
|
|
6197
6228
|
openPushEnvelope,
|
|
6198
6229
|
parseActivityEventPayload,
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { createKKHandshake, type KKHandshakeOptions } from './crypto/noise/kk';
|
|
|
7
7
|
export { createPairingInitiatorHandshake, createPairingResponderHandshake, type PairingInitiatorOptions, type PairingResponderOptions, } from './crypto/pairing-handshake';
|
|
8
8
|
export { SecretstreamState, deriveSecretstreamPair, FrameTag, type SecretstreamDirectionPair, } from './crypto/secretstream';
|
|
9
9
|
export { deriveShortAuthenticationString, type ShortAuthenticationString } from './crypto/sas';
|
|
10
|
-
export { deriveSessionSlotId } from './crypto/slot';
|
|
10
|
+
export { deriveSessionSlotId, derivePairingSlotId } from './crypto/slot';
|
|
11
11
|
export type { JsonValue, BridgeMessage, HeartbeatMessage, CapabilityRequestMessage, CapabilityResponseMessage, EventMessage } from './wire/messages';
|
|
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';
|
|
@@ -22,5 +22,5 @@ export { formatKeyFingerprint } from './roster/fingerprint';
|
|
|
22
22
|
export { CAPABILITY_VERBS, isCapabilityVerb, capabilitySetFromArray, capabilitySetToArray, type CapabilityVerb, type CapabilitySet, } from './capabilities/verbs';
|
|
23
23
|
export { BOARD_TOOL_READ_NAMES, BOARD_TOOL_WRITE_NAMES, isBoardToolReadName, isBoardToolWriteName, type BoardToolReadName, type BoardToolWriteName, type BoardToolName, } from './capabilities/board-tools';
|
|
24
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';
|
|
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';
|
|
25
|
+
export { parseTranscriptEntriesWire, parseTranscriptEventPayload, type TranscriptEventPayload, type TranscriptUpsertWire, isActivityStateWire, isActivityReasonWire, parseSessionUsageWire, parseSessionSummaryWire, parseSessionEventWire, parseTerminalDimensionsWire, parseBoardColumnWire, parseBoardTaskWire, parseBacklogItemWire, parseDiffFileListWire, parseDiffFileContentWire, isTodoRole, isDoneRole, BOARD_COLUMN_ROLES, type TranscriptBlockWire, type TranscriptEntryWire, type TranscriptTurnUsageWire, type TranscriptSystemSubtypeWire, type ActivityStateWire, type ActivityReasonWire, type SessionUsageWire, type SessionSummaryWire, type SessionEventWire, type TerminalDimensionsWire, type BoardColumnWire, type BoardColumnRoleWire, type BoardTaskWire, type BacklogItemWire, type DiffFileStatusWire, type DiffFileWire, type DiffFileListWire, type DiffFileContentWire, } from './events/payloads';
|
|
26
26
|
export type { Transport, TransportState, Unsubscribe } from './transport/transport';
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var PROTOCOL_VERSION = "
|
|
2
|
+
var PROTOCOL_VERSION = "3";
|
|
3
3
|
function encodeProtocolVersion(version = PROTOCOL_VERSION) {
|
|
4
4
|
return new TextEncoder().encode(`kangentic-bridge-v${version}`);
|
|
5
5
|
}
|
|
@@ -4177,6 +4177,11 @@ function deriveSessionSlotId(desktopStaticPublicKey, phoneStaticPublicKey) {
|
|
|
4177
4177
|
const material = concatBytes4(desktopStaticPublicKey, phoneStaticPublicKey);
|
|
4178
4178
|
return bytesToHex(deriveLabeledKey(material, SESSION_SLOT_LABEL, SESSION_SLOT_LENGTH));
|
|
4179
4179
|
}
|
|
4180
|
+
var PAIRING_SLOT_LABEL = "kangentic-pairing-slot-v1";
|
|
4181
|
+
var PAIRING_SLOT_LENGTH = 16;
|
|
4182
|
+
function derivePairingSlotId(pairingToken) {
|
|
4183
|
+
return bytesToHex(deriveLabeledKey(pairingToken, PAIRING_SLOT_LABEL, PAIRING_SLOT_LENGTH));
|
|
4184
|
+
}
|
|
4180
4185
|
|
|
4181
4186
|
// ../../node_modules/fflate/esm/browser.js
|
|
4182
4187
|
var u8 = Uint8Array;
|
|
@@ -5126,6 +5131,15 @@ function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
|
|
|
5126
5131
|
}
|
|
5127
5132
|
|
|
5128
5133
|
// src/events/payloads.ts
|
|
5134
|
+
var BOARD_COLUMN_ROLES = ["todo", "done"];
|
|
5135
|
+
var TODO_ROLE = "todo";
|
|
5136
|
+
var DONE_ROLE = "done";
|
|
5137
|
+
function isTodoRole(role) {
|
|
5138
|
+
return role === TODO_ROLE;
|
|
5139
|
+
}
|
|
5140
|
+
function isDoneRole(role) {
|
|
5141
|
+
return role === DONE_ROLE;
|
|
5142
|
+
}
|
|
5129
5143
|
var DIFF_FILE_STATUSES = ["A", "M", "D", "R", "C", "U"];
|
|
5130
5144
|
var TRANSCRIPT_SYSTEM_SUBTYPES = ["compaction", "command", "command_output", "session_boundary"];
|
|
5131
5145
|
function requireString(record, field, context) {
|
|
@@ -5292,18 +5306,26 @@ function nullableNumber(record, field) {
|
|
|
5292
5306
|
const value = record[field];
|
|
5293
5307
|
return typeof value === "number" ? value : null;
|
|
5294
5308
|
}
|
|
5309
|
+
function nullableBoolean(record, field) {
|
|
5310
|
+
const value = record[field];
|
|
5311
|
+
return typeof value === "boolean" ? value : null;
|
|
5312
|
+
}
|
|
5295
5313
|
function parseBoardColumnWire(value) {
|
|
5296
5314
|
if (!isRecord(value)) throw new Error("board column must be an object");
|
|
5297
5315
|
return {
|
|
5298
5316
|
id: requireString(value, "id", "board column"),
|
|
5299
5317
|
name: requireString(value, "name", "board column"),
|
|
5300
5318
|
description: nullableString(value, "description"),
|
|
5319
|
+
// An unrecognized role passes through rather than throwing: it is a
|
|
5320
|
+
// custom column (or a future system role this package predates), not a
|
|
5321
|
+
// malformed payload. See the BoardColumnRoleWire doc comment.
|
|
5301
5322
|
role: nullableString(value, "role"),
|
|
5302
5323
|
position: requireNumber(value, "position", "board column"),
|
|
5303
5324
|
color: requireString(value, "color", "board column"),
|
|
5304
5325
|
icon: nullableString(value, "icon"),
|
|
5305
5326
|
is_archived: requireBoolean(value, "is_archived", "board column"),
|
|
5306
|
-
is_ghost: requireBoolean(value, "is_ghost", "board column")
|
|
5327
|
+
is_ghost: requireBoolean(value, "is_ghost", "board column"),
|
|
5328
|
+
spawns_session: nullableBoolean(value, "spawns_session")
|
|
5307
5329
|
};
|
|
5308
5330
|
}
|
|
5309
5331
|
function parseBoardTaskWire(value) {
|
|
@@ -5322,6 +5344,7 @@ function parseBoardTaskWire(value) {
|
|
|
5322
5344
|
pr_number: nullableNumber(value, "pr_number"),
|
|
5323
5345
|
pr_url: nullableString(value, "pr_url"),
|
|
5324
5346
|
pr_state: nullableString(value, "pr_state"),
|
|
5347
|
+
pr_merge_readiness: nullableString(value, "pr_merge_readiness"),
|
|
5325
5348
|
base_branch: nullableString(value, "base_branch"),
|
|
5326
5349
|
labels: Array.isArray(value.labels) ? parseStringArray(value.labels, "board task labels") : [],
|
|
5327
5350
|
priority: typeof value.priority === "number" ? value.priority : 0,
|
|
@@ -6022,6 +6045,7 @@ function isBridgeEvent(value) {
|
|
|
6022
6045
|
}
|
|
6023
6046
|
}
|
|
6024
6047
|
export {
|
|
6048
|
+
BOARD_COLUMN_ROLES,
|
|
6025
6049
|
BOARD_TOOL_READ_NAMES,
|
|
6026
6050
|
BOARD_TOOL_WRITE_NAMES,
|
|
6027
6051
|
CAPABILITY_VERBS,
|
|
@@ -6052,6 +6076,7 @@ export {
|
|
|
6052
6076
|
createPairingResponderHandshake,
|
|
6053
6077
|
decodeMessage,
|
|
6054
6078
|
decodePairingQrPayload,
|
|
6079
|
+
derivePairingSlotId,
|
|
6055
6080
|
deriveSecretstreamPair,
|
|
6056
6081
|
deriveSessionSlotId,
|
|
6057
6082
|
deriveShortAuthenticationString,
|
|
@@ -6070,11 +6095,13 @@ export {
|
|
|
6070
6095
|
isBoardToolWriteName,
|
|
6071
6096
|
isBridgeEvent,
|
|
6072
6097
|
isCapabilityVerb,
|
|
6098
|
+
isDoneRole,
|
|
6073
6099
|
isJsonValue,
|
|
6074
6100
|
isPushCategory,
|
|
6075
6101
|
isRecord,
|
|
6076
6102
|
isRosterEntryExpired,
|
|
6077
6103
|
isSecureRelayAddress,
|
|
6104
|
+
isTodoRole,
|
|
6078
6105
|
openPairingConfirm,
|
|
6079
6106
|
openPushEnvelope,
|
|
6080
6107
|
parseActivityEventPayload,
|
|
@@ -14,8 +14,14 @@
|
|
|
14
14
|
* desktop's key, the two transcripts necessarily agree - which means the
|
|
15
15
|
* SAS the human compared necessarily agreed too. The AEAD open IS the
|
|
16
16
|
* desktop's SAS verification; that is why the frame carries no digits of
|
|
17
|
-
* its own
|
|
18
|
-
*
|
|
17
|
+
* its own.
|
|
18
|
+
*
|
|
19
|
+
* A failed open is IGNORED rather than treated as a ceremony-ending failure.
|
|
20
|
+
* The frame is unauthenticated and the relay slot is reachable by anyone who
|
|
21
|
+
* can read the request URI, so treating one bad frame as a mismatch let an
|
|
22
|
+
* observer end a pairing at will. The caller stays in its sas-pending phase
|
|
23
|
+
* and lets its own timeout decide - see pairing-service.ts's
|
|
24
|
+
* handleConfirmFrame().
|
|
19
25
|
*
|
|
20
26
|
* There is no reject frame. Backing out of the ceremony on the phone closes
|
|
21
27
|
* the transport without sending this frame; close-without-confirm is the
|
|
@@ -35,5 +41,14 @@ export declare function sealPairingConfirm(initiatorToResponder: CipherState): U
|
|
|
35
41
|
* tampered/wrong-key ciphertext or an unexpected plaintext, so the caller
|
|
36
42
|
* can treat "could not verify" uniformly whether the failure was
|
|
37
43
|
* cryptographic or structural.
|
|
44
|
+
*
|
|
45
|
+
* A false return from a FAILED DECRYPT leaves `initiatorToResponder` untouched:
|
|
46
|
+
* CipherState advances its nonce only on a successful decrypt, so a caller may
|
|
47
|
+
* keep waiting for the real frame after rejecting any number of bad ones. Scoped
|
|
48
|
+
* to that path deliberately - the structural checks below return false only
|
|
49
|
+
* after a decrypt already succeeded and already advanced the nonce. They are
|
|
50
|
+
* unreachable in practice (an authenticating tag means the plaintext is the one
|
|
51
|
+
* sealPairingConfirm wrote) and kept as belt-and-braces, but a caller that ever
|
|
52
|
+
* made them reachable would not inherit the keep-waiting guarantee.
|
|
38
53
|
*/
|
|
39
54
|
export declare function openPairingConfirm(initiatorToResponder: CipherState, frame: Uint8Array): boolean;
|
|
@@ -10,15 +10,24 @@
|
|
|
10
10
|
export declare const MAX_RELAY_ADDRESS_LENGTH = 512;
|
|
11
11
|
/**
|
|
12
12
|
* True if the phone's pairing QR scanner will accept this relay address.
|
|
13
|
-
* wss:// is always secure; ws:// is accepted only for loopback
|
|
14
|
-
*
|
|
13
|
+
* wss:// is always secure; ws:// is accepted only for loopback.
|
|
14
|
+
*
|
|
15
|
+
* The relay address arrives inside a scanned QR payload, so it is attacker
|
|
16
|
+
* controllable: a crafted QR naming a plaintext host would route the whole
|
|
17
|
+
* ceremony through that host, and the pairing then persists it to the trust
|
|
18
|
+
* anchor for every later session. That is what the restriction prevents, and
|
|
19
|
+
* it holds regardless of how strong the frames themselves are.
|
|
20
|
+
*
|
|
21
|
+
* Note this rule predates derivePairingSlotId() and used to be justified by
|
|
22
|
+
* the pairing token being dialed verbatim as `?slot=`, which put the Noise PSK
|
|
23
|
+
* in cleartext on the wire. The slot is a derived routing label now, so that
|
|
24
|
+
* particular exposure is gone - the address-pinning reason above is not, which
|
|
25
|
+
* is why the restriction stays.
|
|
15
26
|
*
|
|
16
27
|
* Parses the AUTHORITY rather than prefix-matching. Prefix matching accepted
|
|
17
28
|
* `ws://127.0.0.1:8080@evil.test`: everything before an '@' is userinfo, so
|
|
18
|
-
* that address dials evil.test while looking like loopback
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* parsing also subsumes the old boundary check, since `localhost.evil.com`
|
|
22
|
-
* is simply a different host.
|
|
29
|
+
* that address dials evil.test while looking like loopback. Authority parsing
|
|
30
|
+
* also subsumes the old boundary check, since `localhost.evil.com` is simply a
|
|
31
|
+
* different host.
|
|
23
32
|
*/
|
|
24
33
|
export declare function isSecureRelayAddress(relayAddress: string): boolean;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The protocol version string. Bumped on any wire-incompatible change to
|
|
3
|
-
* framing, message shapes,
|
|
3
|
+
* framing, message shapes, the handshake patterns, or the relay slot
|
|
4
|
+
* derivations in this package.
|
|
5
|
+
*
|
|
6
|
+
* A slot derivation counts because the slot is a zero-negotiation rendezvous
|
|
7
|
+
* value: peers computing it differently never meet, and would otherwise just
|
|
8
|
+
* hang until the relay's park timeout. Binding the version here instead turns
|
|
9
|
+
* that into an explicit "update the app" at QR-scan time, before anything
|
|
10
|
+
* dials. v3 derives the pairing slot from the token rather than using the
|
|
11
|
+
* token verbatim (see crypto/slot.ts's derivePairingSlotId).
|
|
4
12
|
*
|
|
5
13
|
* Bound into every Noise handshake's prologue (see crypto/noise/kk.ts and
|
|
6
14
|
* crypto/pairing-handshake.ts), so two peers running different protocol
|
|
@@ -8,5 +16,5 @@
|
|
|
8
16
|
* differing prologue produces a differing transcript hash `h` from the
|
|
9
17
|
* first MixHash call, which cascades into every derived key.
|
|
10
18
|
*/
|
|
11
|
-
export declare const PROTOCOL_VERSION = "
|
|
19
|
+
export declare const PROTOCOL_VERSION = "3";
|
|
12
20
|
export declare function encodeProtocolVersion(version?: string): Uint8Array;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kangentic/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|