@kangentic/protocol 0.1.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.
@@ -0,0 +1,19 @@
1
+ export declare const PAIRING_URI_SCHEME = "kangentic-pair";
2
+ export interface PairingQrPayload {
3
+ desktopStaticPublicKey: Uint8Array;
4
+ pairingToken: Uint8Array;
5
+ relayAddress: string;
6
+ /** ISO 8601. The pairing service is the actual enforcement point; this is carried for the phone's own UX (e.g. "this code expired"). */
7
+ expiresAt: string;
8
+ /**
9
+ * The bridge protocol version (see version.ts's PROTOCOL_VERSION), so the
10
+ * phone can surface "this desktop is running an incompatible version,
11
+ * please update" BEFORE attempting a handshake doomed to fail, instead
12
+ * of only an opaque authentication error. The actual downgrade
13
+ * PROTECTION is the Noise prologue binding (crypto/pairing-handshake.ts),
14
+ * which is enforced independent of this field; this is diagnostic only.
15
+ */
16
+ protocolVersion: string;
17
+ }
18
+ export declare function encodePairingQrPayload(payload: PairingQrPayload): string;
19
+ export declare function decodePairingQrPayload(uri: string): PairingQrPayload;
@@ -0,0 +1,33 @@
1
+ import { type CapabilitySet, type CapabilityVerb } from '../capabilities/verbs';
2
+ export interface RosterDeviceEntry {
3
+ /** Stable identifier for this device, independent of display name - derived from its static public key by the caller. */
4
+ deviceId: string;
5
+ /** The device's X25519 static identity public key (32 bytes). */
6
+ staticPublicKey: Uint8Array;
7
+ displayName: string;
8
+ capabilities: CapabilityVerb[];
9
+ /** ISO 8601. */
10
+ pairedAt: string;
11
+ /** ISO 8601, or null for no expiry (Tailscale-style per-device key expiry is opt-in). */
12
+ expiresAt: string | null;
13
+ /** Ed25519 signature over encodeForSigning(this entry minus signature), by the roster's master signing key. */
14
+ signature: Uint8Array;
15
+ }
16
+ export interface DeviceRoster {
17
+ /** The desktop's Ed25519 master signing public key - the roster's root of trust. */
18
+ masterSigningPublicKey: Uint8Array;
19
+ devices: RosterDeviceEntry[];
20
+ }
21
+ /**
22
+ * A deterministic, unambiguous byte encoding of a roster entry (minus its
23
+ * signature) for signing/verification. Every field is length-prefixed so
24
+ * concatenation cannot be reinterpreted across a field boundary (e.g.
25
+ * deviceId="ab"+displayName="c" cannot collide with deviceId="a"+displayName="bc").
26
+ */
27
+ export declare function encodeRosterEntryForSigning(entry: Omit<RosterDeviceEntry, 'signature'>): Uint8Array;
28
+ export declare function signRosterEntry(masterSigningSecretKey: Uint8Array, entry: Omit<RosterDeviceEntry, 'signature'>): RosterDeviceEntry;
29
+ export declare function verifyRosterEntry(masterSigningPublicKey: Uint8Array, entry: RosterDeviceEntry): boolean;
30
+ export declare function isRosterEntryExpired(entry: RosterDeviceEntry, now: Date): boolean;
31
+ export declare function findRosterDevice(roster: DeviceRoster, deviceId: string): RosterDeviceEntry | undefined;
32
+ export declare function rosterDeviceCapabilitySet(entry: RosterDeviceEntry): CapabilitySet;
33
+ export declare function capabilitySetToRosterCapabilities(capabilities: CapabilitySet): CapabilityVerb[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The swappable transport boundary. A Transport moves opaque frames
3
+ * (already-encrypted secretstream frames, or raw Noise handshake bytes
4
+ * during pairing/session setup) between two peers; it knows nothing about
5
+ * Noise, capability verbs, or the message schema. The relay client
6
+ * (src/main/mobile-bridge/transport/relay-client.ts, desktop-side) is the
7
+ * v1 implementation; a WebRTC data channel implementation (Phase 4) slots
8
+ * in behind this exact interface, so nothing above the transport layer
9
+ * changes when direct P2P becomes available.
10
+ */
11
+ export type TransportState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'closed';
12
+ export type Unsubscribe = () => void;
13
+ export interface Transport {
14
+ readonly state: TransportState;
15
+ connect(): Promise<void>;
16
+ /** Sends one opaque frame. Throws if the transport is not connected. */
17
+ send(frame: Uint8Array): void;
18
+ /** Synchronous, idempotent teardown. */
19
+ close(): void;
20
+ onFrame(listener: (frame: Uint8Array) => void): Unsubscribe;
21
+ onStateChange(listener: (state: TransportState) => void): Unsubscribe;
22
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The protocol version string. Bumped on any wire-incompatible change to
3
+ * framing, message shapes, or the handshake patterns in this package.
4
+ *
5
+ * Bound into every Noise handshake's prologue (see crypto/noise/kk.ts and
6
+ * crypto/pairing-handshake.ts), so two peers running different protocol
7
+ * versions fail the handshake outright instead of silently downgrading: a
8
+ * differing prologue produces a differing transcript hash `h` from the
9
+ * first MixHash call, which cascades into every derived key.
10
+ */
11
+ export declare const PROTOCOL_VERSION = "1";
12
+ export declare function encodeProtocolVersion(version?: string): Uint8Array;
@@ -0,0 +1,2 @@
1
+ export declare function base64UrlEncode(bytes: Uint8Array): string;
2
+ export declare function base64UrlDecode(encoded: string): Uint8Array;
@@ -0,0 +1,4 @@
1
+ import type { BridgeMessage } from './messages';
2
+ export declare const MAX_FRAME_LENGTH: number;
3
+ export declare function encodeMessage(message: BridgeMessage): Uint8Array;
4
+ export declare function decodeMessage(bytes: Uint8Array): BridgeMessage;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The application-level message shapes carried inside secretstream frames
3
+ * once a bridge session is established (crypto/secretstream.ts handles
4
+ * the encrypted framing; this is what gets encoded/decoded as the
5
+ * plaintext payload - see framing.ts).
6
+ *
7
+ * Phase 1 wires the transport and the message ENVELOPE, not real
8
+ * capability-verb handlers or event feeds - those are Phase 2 (data
9
+ * feeds, interactive control) and Phase 3 (notifications). `payload` on
10
+ * the request/response/event variants is intentionally a generic JSON
11
+ * value here rather than a fully-typed union per verb/event, since the
12
+ * real shapes depend on desktop internals (SessionManager, repositories,
13
+ * DiffService) that Phase 2 integrates with; over-specifying them now
14
+ * would just be guessing. The capability verb ENUM itself
15
+ * (capabilities/verbs.ts) and the event type skeleton (events/) are
16
+ * final for Phase 1; only their payload contents are deferred.
17
+ */
18
+ import type { CapabilityVerb } from '../capabilities/verbs';
19
+ import type { BridgeEvent } from '../events/event';
20
+ /** JSON-serializable value - deliberately not `any`; every message payload must round-trip through JSON.stringify/parse. */
21
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
22
+ [key: string]: JsonValue;
23
+ };
24
+ export interface HeartbeatMessage {
25
+ type: 'heartbeat';
26
+ }
27
+ export interface CapabilityRequestMessage {
28
+ type: 'capability-request';
29
+ requestId: string;
30
+ verb: CapabilityVerb;
31
+ payload: JsonValue;
32
+ }
33
+ export interface CapabilityResponseMessage {
34
+ type: 'capability-response';
35
+ requestId: string;
36
+ ok: boolean;
37
+ payload?: JsonValue;
38
+ /** Present only when ok is false. */
39
+ error?: string;
40
+ }
41
+ export interface EventMessage {
42
+ type: 'event';
43
+ event: BridgeEvent;
44
+ }
45
+ export type BridgeMessage = HeartbeatMessage | CapabilityRequestMessage | CapabilityResponseMessage | EventMessage;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A one-byte frame-kind prefix for the ONGOING SESSION connection only
3
+ * (not pairing, which never mixes frame kinds on its dedicated
4
+ * connection). A bridge session periodically re-handshakes on the same
5
+ * transport connection it also carries application traffic on (Noise KK
6
+ * every ~2 minutes, per the research doc's bounded post-compromise
7
+ * security design), so an incoming frame is otherwise ambiguous: "the
8
+ * next message of an in-progress Noise handshake" and "a secretstream-sealed
9
+ * application frame" are both just opaque bytes at the transport level.
10
+ * This tiny, unauthenticated-but-harmless prefix removes that ambiguity
11
+ * for BOTH peers (desktop and mobile), so it lives here rather than as a
12
+ * desktop-only convention.
13
+ */
14
+ export declare const SessionFrameKind: {
15
+ readonly Handshake: 0;
16
+ readonly Application: 1;
17
+ };
18
+ export type SessionFrameKind = (typeof SessionFrameKind)[keyof typeof SessionFrameKind];
19
+ export declare function wrapSessionFrame(kind: SessionFrameKind, payload: Uint8Array): Uint8Array;
20
+ export declare function unwrapSessionFrame(frame: Uint8Array): {
21
+ kind: SessionFrameKind;
22
+ payload: Uint8Array;
23
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@kangentic/protocol",
3
+ "version": "0.1.0",
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
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Kangentic/kangentic.git"
18
+ },
19
+ "keywords": [
20
+ "kangentic",
21
+ "protocol",
22
+ "noise-protocol",
23
+ "e2ee",
24
+ "mobile"
25
+ ],
26
+ "author": {
27
+ "name": "Kangentic",
28
+ "url": "https://github.com/Kangentic"
29
+ },
30
+ "license": "AGPL-3.0-only",
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "files": [
35
+ "dist/",
36
+ "LICENSE",
37
+ "README.md"
38
+ ],
39
+ "scripts": {
40
+ "build": "node build.mjs && tsc -p tsconfig.build.json"
41
+ },
42
+ "dependencies": {
43
+ "@noble/ciphers": "^2.2.0",
44
+ "@noble/curves": "^2.2.0",
45
+ "@noble/hashes": "^2.2.0"
46
+ },
47
+ "devDependencies": {
48
+ "esbuild": "^0.28.0",
49
+ "typescript": "^6.0.3"
50
+ }
51
+ }