@uxnan/shared 0.0.1-alpha.20260627

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.
Files changed (68) hide show
  1. package/README.md +96 -0
  2. package/dist/src/agents/agent-adapter.d.ts +78 -0
  3. package/dist/src/agents/agent-adapter.js +2 -0
  4. package/dist/src/agents/agent-adapter.js.map +1 -0
  5. package/dist/src/agents/agent-capabilities.d.ts +106 -0
  6. package/dist/src/agents/agent-capabilities.js +7 -0
  7. package/dist/src/agents/agent-capabilities.js.map +1 -0
  8. package/dist/src/agents/agent-config.d.ts +17 -0
  9. package/dist/src/agents/agent-config.js +2 -0
  10. package/dist/src/agents/agent-config.js.map +1 -0
  11. package/dist/src/constants.d.ts +30 -0
  12. package/dist/src/constants.js +31 -0
  13. package/dist/src/constants.js.map +1 -0
  14. package/dist/src/e2ee/envelope.d.ts +17 -0
  15. package/dist/src/e2ee/envelope.js +7 -0
  16. package/dist/src/e2ee/envelope.js.map +1 -0
  17. package/dist/src/e2ee/handshake.d.ts +82 -0
  18. package/dist/src/e2ee/handshake.js +14 -0
  19. package/dist/src/e2ee/handshake.js.map +1 -0
  20. package/dist/src/e2ee/pairing-payload.d.ts +44 -0
  21. package/dist/src/e2ee/pairing-payload.js +82 -0
  22. package/dist/src/e2ee/pairing-payload.js.map +1 -0
  23. package/dist/src/index.d.ts +25 -0
  24. package/dist/src/index.js +32 -0
  25. package/dist/src/index.js.map +1 -0
  26. package/dist/src/jsonrpc/envelope.d.ts +39 -0
  27. package/dist/src/jsonrpc/envelope.js +36 -0
  28. package/dist/src/jsonrpc/envelope.js.map +1 -0
  29. package/dist/src/jsonrpc/errors.d.ts +43 -0
  30. package/dist/src/jsonrpc/errors.js +73 -0
  31. package/dist/src/jsonrpc/errors.js.map +1 -0
  32. package/dist/src/jsonrpc/method-registry.d.ts +7 -0
  33. package/dist/src/jsonrpc/method-registry.js +79 -0
  34. package/dist/src/jsonrpc/method-registry.js.map +1 -0
  35. package/dist/src/jsonrpc/methods.d.ts +526 -0
  36. package/dist/src/jsonrpc/methods.js +2 -0
  37. package/dist/src/jsonrpc/methods.js.map +1 -0
  38. package/dist/src/jsonrpc/notifications.d.ts +85 -0
  39. package/dist/src/jsonrpc/notifications.js +19 -0
  40. package/dist/src/jsonrpc/notifications.js.map +1 -0
  41. package/dist/src/models/approval.d.ts +37 -0
  42. package/dist/src/models/approval.js +12 -0
  43. package/dist/src/models/approval.js.map +1 -0
  44. package/dist/src/models/git.d.ts +191 -0
  45. package/dist/src/models/git.js +8 -0
  46. package/dist/src/models/git.js.map +1 -0
  47. package/dist/src/models/project.d.ts +22 -0
  48. package/dist/src/models/project.js +5 -0
  49. package/dist/src/models/project.js.map +1 -0
  50. package/dist/src/models/session.d.ts +28 -0
  51. package/dist/src/models/session.js +7 -0
  52. package/dist/src/models/session.js.map +1 -0
  53. package/dist/src/models/thread.d.ts +104 -0
  54. package/dist/src/models/thread.js +8 -0
  55. package/dist/src/models/thread.js.map +1 -0
  56. package/dist/src/models/workspace.d.ts +133 -0
  57. package/dist/src/models/workspace.js +7 -0
  58. package/dist/src/models/workspace.js.map +1 -0
  59. package/dist/src/notifications/push-payload.d.ts +23 -0
  60. package/dist/src/notifications/push-payload.js +7 -0
  61. package/dist/src/notifications/push-payload.js.map +1 -0
  62. package/dist/src/validators/json-schema/schemas.d.ts +13 -0
  63. package/dist/src/validators/json-schema/schemas.js +82 -0
  64. package/dist/src/validators/json-schema/schemas.js.map +1 -0
  65. package/dist/src/validators/validate.d.ts +20 -0
  66. package/dist/src/validators/validate.js +46 -0
  67. package/dist/src/validators/validate.js.map +1 -0
  68. package/package.json +36 -0
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @uxnan/shared
2
+
3
+ ![TypeScript](https://img.shields.io/badge/TypeScript-ESM-3178C6?style=for-the-badge&logo=typescript&logoColor=white)
4
+ ![Node.js](https://img.shields.io/badge/Node.js-%E2%89%A518-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)
5
+ ![JSON Schema](https://img.shields.io/badge/validation-Ajv-000000?style=for-the-badge&logo=json&logoColor=white)
6
+ ![Contracts](https://img.shields.io/badge/60_methods_%7C_8_notifications-blue?style=for-the-badge)
7
+
8
+ Shared JSON-RPC and E2EE contracts for the [Uxnan](../README.md) ecosystem — the
9
+ single source of truth every component agrees on. Consumed as a local workspace
10
+ dependency by the **[bridge](../bridge/README.md)** and the
11
+ **[relay](../relay/README.md)**; the mobile app keeps manually-synced Dart
12
+ equivalents (see
13
+ [`architecture/02b-contracts-and-requirements.md`](../architecture/02b-contracts-and-requirements.md)
14
+ §1 for the canonical contract list).
15
+
16
+ > **Status:** implemented and stable — **60 JSON-RPC methods** + **8 streaming
17
+ > notifications**, kept lock-step at build time with the `METHOD_NAMES` array and
18
+ > the `StreamNotification` enum (a compile-time assertion in
19
+ > `src/jsonrpc/method-registry.ts` fails the build on any drift). Changes are
20
+ > recorded in [`CHANGELOG.md`](CHANGELOG.md).
21
+
22
+ ## Why it exists
23
+
24
+ Three independent codebases — a Node.js bridge, a Node.js relay, and a Flutter app
25
+ — have to agree on exactly the same messages on the wire. Rather than letting each
26
+ one drift its own way, every shape lives here once: the request and response
27
+ envelopes, the streaming notifications, the E2EE handshake, the pairing payload,
28
+ and the domain and agent models. The bridge and relay import this package
29
+ directly; the mobile app mirrors it in Dart. When a contract changes, it changes
30
+ here first, and the build refuses to pass if the registry and the spec disagree.
31
+
32
+ <details>
33
+ <summary><b>Diagram — one contract, three consumers</b></summary>
34
+
35
+ ```mermaid
36
+ flowchart TB
37
+ shared["@uxnan/shared<br/>JSON-RPC + E2EE contracts"]
38
+ bridge["uxnan-bridge<br/>(imports directly)"]
39
+ relay["uxnan-relay<br/>(imports directly)"]
40
+ mobile["uxnanmobile<br/>(hand-synced Dart mirror)"]
41
+ shared --> bridge
42
+ shared --> relay
43
+ shared -. mirrored .-> mobile
44
+ ```
45
+
46
+ </details>
47
+
48
+ ## What's inside
49
+
50
+ | Area | Exports |
51
+ |---|---|
52
+ | JSON-RPC | envelope types + constructors (`makeRequest`, `makeNotification`, `makeResponse`, `makeErrorResponse`), error codes (`JsonRpcErrorCode` + Uxnan-specific `-32000..-32008`), `RpcError`, typed method registry (`JsonRpcMethodRegistry` + `METHOD_NAMES`), `isKnownMethod` |
53
+ | Streaming | `StreamNotification` enum + param types (`TurnStartedParams`, `MessageDeltaParams`, `ThinkingDeltaParams`, `ContentBlockParams`, `TurnCompletedParams`, `TurnUsage`, `TurnErrorParams`, `TurnAbortedParams`, `ModelResolvedParams`) |
54
+ | E2EE | handshake messages (`clientHello` / `serverHello` / `clientAuth` / `ready`), `buildHandshakeTranscript`, `SecureEnvelope`, `PairingPayload` v2 (`relay` optional + `hosts: string[]`) with `Base64(utf8(JSON))` QR encoding |
55
+ | Models | thread / turn / message (with `MessageContent` polymorphic blocks), git, workspace (incl. `browseDirs` + `exists`), project, auth, session/trust, approval |
56
+ | Agents | `IAgentAdapter` (with `respondApproval`, `listModels`, `nativeSessionId`, `SendTurnOptions { threadId, turnId, text, service?, effort?, options?, attachments?, cwd?, accessMode? }`), `AgentModel` (incl. `version?`, `isDefault?`, `options?`, `contextWindow?`), `AgentCapabilities` (incl. `images`, `approvals`, `reportsContextUsage`), `AgentConfig` (cwd, agentId, model, plus optional `binaryPath`/`extraArgs`) |
57
+ | Validation | Ajv validators for requests, responses, envelopes, pairing payload, push payloads |
58
+
59
+ ## Usage
60
+
61
+ ```ts
62
+ import {
63
+ makeRequest,
64
+ isKnownMethod,
65
+ validateJsonRpcRequest,
66
+ METHOD_NAMES,
67
+ type AgentModel,
68
+ type PairingPayload,
69
+ } from '@uxnan/shared';
70
+ ```
71
+
72
+ ## Develop
73
+
74
+ ```bash
75
+ npm run build # tsc → dist/
76
+ npm test # tsc + node --test dist/test
77
+ npm run typecheck # tsc --noEmit
78
+ ```
79
+
80
+ Requires Node ≥18. The package is ESM-only.
81
+
82
+ ## Source of truth
83
+
84
+ The canonical contract list lives in this package — see
85
+ [`src/jsonrpc/method-registry.ts`](src/jsonrpc/method-registry.ts) (`METHOD_NAMES`)
86
+ and [`src/jsonrpc/notifications.ts`](src/jsonrpc/notifications.ts)
87
+ (`StreamNotification`). The spec mirrors it in
88
+ [`architecture/02b-contracts-and-requirements.md`](../architecture/02b-contracts-and-requirements.md)
89
+ §1.2 / §1.4. Per `AGENTS.md` → *Spec drift control*, any change here MUST be
90
+ reflected in the spec in the same change set.
91
+
92
+ ## Publish (planned)
93
+
94
+ `@uxnan/shared` is published to npm **first**; `uxnan-bridge` and `uxnan-relay`
95
+ then pin `"@uxnan/shared": "^0.x"` instead of the `"*"` workspace spec they
96
+ use today. See `bridge/FOR-DEV.md` → *Packaging*.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Contract every agent CLI adapter must implement.
3
+ *
4
+ * Source: architecture/02a-system-architecture.md §5.8.2 (adapters/base-adapter).
5
+ */
6
+ import type { AgentCapabilities, AgentId, AgentModel } from './agent-capabilities.js';
7
+ import type { AgentConfig } from './agent-config.js';
8
+ import type { TurnAttachment } from '../models/workspace.js';
9
+ import type { ApprovalDecision } from '../models/approval.js';
10
+ import type { AccessMode } from '../models/thread.js';
11
+ /** A single streamed event produced by a running agent turn. */
12
+ export interface AgentStreamEvent {
13
+ type: 'delta'
14
+ /** A chunk of the agent's reasoning / "thinking" (`data.text`). */
15
+ | 'thinking'
16
+ /** A structured content block — command/diff/tool (`data.content`). */
17
+ | 'block' | 'turn_started' | 'turn_completed' | 'turn_error' | 'turn_aborted'
18
+ /** The agent reported the concrete model it resolved an alias to (`data.text`). */
19
+ | 'model_resolved';
20
+ threadId: string;
21
+ turnId: string;
22
+ /** Free-form payload depending on `type`. */
23
+ data?: unknown;
24
+ }
25
+ export interface SendTurnOptions {
26
+ threadId: string;
27
+ turnId: string;
28
+ text: string;
29
+ /** Model/service identifier (e.g. `provider/model`) for adapters that accept one. */
30
+ service?: string;
31
+ /** Legacy flat reasoning effort / variant (e.g. `high`, `max`, `minimal`). */
32
+ effort?: string;
33
+ /**
34
+ * Chosen per-model run-option values keyed by `AgentModelOption.key` (e.g.
35
+ * `{ reasoning: 'high' }`). Adapters translate these into CLI flags; the
36
+ * legacy `effort` is used as a fallback for the `reasoning` knob.
37
+ */
38
+ options?: Record<string, string | boolean>;
39
+ /**
40
+ * Inline image attachments for this turn. The {@link AgentManager}
41
+ * materializes these to temp files and appends a reference to {@link text}
42
+ * before the adapter runs, so adapters need no per-CLI image handling.
43
+ */
44
+ attachments?: TurnAttachment[];
45
+ /** Working directory the agent should run in for this turn. */
46
+ cwd?: string;
47
+ /**
48
+ * The thread's persisted access (approval) mode (see {@link AccessMode}).
49
+ * Adapters that gate tool execution map it to their per-turn permission flag
50
+ * — `requestApproval` keeps interactive approvals in play, `approveForMe`
51
+ * auto-approves, `fullAccess` bypasses gating. Absent → the adapter's
52
+ * configured default posture (no behaviour change).
53
+ */
54
+ accessMode?: AccessMode;
55
+ }
56
+ export interface IAgentAdapter {
57
+ readonly agentId: AgentId;
58
+ readonly capabilities: AgentCapabilities;
59
+ /** Start (or attach to) the agent runtime for the given config. */
60
+ start(config: AgentConfig): Promise<void>;
61
+ /** Stop the agent runtime and release resources. */
62
+ stop(): Promise<void>;
63
+ /** Send a user turn; streamed events are delivered via {@link onEvent}. */
64
+ sendTurn(options: SendTurnOptions): Promise<void>;
65
+ /** Cancel an in-flight turn. */
66
+ cancelTurn(threadId: string, turnId: string): Promise<void>;
67
+ /**
68
+ * Reply to a pending approval the agent emitted (as an `approval` content
69
+ * block) for {@link threadId}. Optional: adapters that never request approval
70
+ * (or that run non-interactively) don't implement it. Implementations should
71
+ * be a no-op when there is no pending approval for `approvalId`.
72
+ */
73
+ respondApproval?(threadId: string, approvalId: string, decision: ApprovalDecision): Promise<void>;
74
+ /** Subscribe to streaming events. Returns an unsubscribe function. */
75
+ onEvent(listener: (event: AgentStreamEvent) => void): () => void;
76
+ /** List the models this agent's CLI reports as available (optional). */
77
+ listModels?(): Promise<AgentModel[]>;
78
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=agent-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-adapter.js","sourceRoot":"","sources":["../../../src/agents/agent-adapter.ts"],"names":[],"mappings":""}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Declarative description of what an agent CLI adapter supports.
3
+ *
4
+ * Source: architecture/02a-system-architecture.md §5.8.2 (adapters).
5
+ */
6
+ export type AgentId = 'codex' | 'opencode' | 'claude-code' | 'gemini-cli' | 'pi-agent' | 'aider'
7
+ /** Built-in reference/dev agent that echoes the prompt (no external CLI). */
8
+ | 'echo';
9
+ export interface AgentCapabilities {
10
+ /** Agent supports interactive plan mode. */
11
+ planMode: boolean;
12
+ /** Agent emits streaming token deltas. */
13
+ streaming: boolean;
14
+ /** Agent supports approval requests (tool gating). */
15
+ approvals: boolean;
16
+ /** Agent supports forking / resuming threads. */
17
+ forking: boolean;
18
+ /** Agent supports image inputs. */
19
+ images: boolean;
20
+ /**
21
+ * Agent reports per-turn token/context usage (`usage` on `turn/completed`),
22
+ * so the phone can show a context meter (at 0 until the first turn). Optional
23
+ * for back-compat; absent/false means the agent reports no usage (e.g.
24
+ * OpenCode) and the meter stays hidden.
25
+ */
26
+ reportsContextUsage?: boolean;
27
+ }
28
+ /**
29
+ * A registered agent the phone can pick for a thread, returned by `agent/list`.
30
+ */
31
+ export interface AgentDescriptor {
32
+ agentId: AgentId;
33
+ /** Human-facing label (e.g. "OpenCode"). */
34
+ displayName: string;
35
+ /** Whether the agent's CLI/runtime is resolvable on this PC right now. */
36
+ available: boolean;
37
+ capabilities: AgentCapabilities;
38
+ /** Default model the bridge will use when the phone does not pick one. */
39
+ defaultModel?: string;
40
+ }
41
+ /** One selectable value of an {@link AgentModelOption} of kind `enum`. */
42
+ export interface AgentModelOptionValue {
43
+ /** Value sent back in `turn/send` `options` when chosen. */
44
+ value: string;
45
+ /** Human-facing label for this value. */
46
+ label: string;
47
+ }
48
+ /**
49
+ * A per-model run-option "knob" the phone should let the user set for a turn
50
+ * (e.g. reasoning effort). Declared per {@link AgentModel} because the same
51
+ * agent's models can differ. The phone is a generic renderer: it shows only the
52
+ * knobs the bridge advertises and sends the chosen values back on `turn/send`
53
+ * (keyed by {@link AgentModelOption.key}); the bridge translates them into each
54
+ * CLI's real flag. Consumers MUST ignore an unknown `kind` so adding a knob
55
+ * never breaks an older app.
56
+ */
57
+ export interface AgentModelOption {
58
+ /** Stable key echoed back in `turn/send` `options` (e.g. `reasoning`). */
59
+ key: string;
60
+ /** Control kind. Unknown kinds are ignored by the phone (forward-compatible). */
61
+ kind: 'enum' | 'toggle';
62
+ /** Human-facing label for the control. */
63
+ label: string;
64
+ /** For `enum`: the selectable values (omit for `toggle`). */
65
+ values?: AgentModelOptionValue[];
66
+ /** Default value when the agent has one (string for `enum`, boolean for `toggle`). */
67
+ default?: string | boolean;
68
+ }
69
+ /**
70
+ * A selectable model an agent reports, returned by `agent/models`.
71
+ *
72
+ * `id` is the wire value passed back to the agent for routing — a stable alias
73
+ * for Claude Code (`opus`/`sonnet`/`haiku`), a `provider/model` id for OpenCode,
74
+ * or a concrete model id for Codex. `displayName`, `description`, `version` and
75
+ * `isDefault` are presentation hints; consumers must tolerate any of them being
76
+ * absent (older bridges report bare id strings).
77
+ */
78
+ export interface AgentModel {
79
+ /** Value sent back to the agent to select this model (the routing key). */
80
+ id: string;
81
+ /** Human-facing label. Falls back to `id` when the CLI offers nothing better. */
82
+ displayName: string;
83
+ /** One-line description, when the CLI provides one (e.g. Codex). */
84
+ description?: string;
85
+ /**
86
+ * Concrete underlying version when `id` is an alias that resolves to a
87
+ * moving target — e.g. Claude Code's `opus` → `claude-opus-4-8`. Surfaced so
88
+ * the user can see which exact model an alias currently maps to.
89
+ */
90
+ version?: string;
91
+ /** Whether this is the agent's current default model. */
92
+ isDefault?: boolean;
93
+ /**
94
+ * Per-model run-option knobs (reasoning effort, etc.) the phone may let the
95
+ * user set. Absent/empty when the model has none. The phone renders these
96
+ * generically and sends chosen values on `turn/send` via `options`.
97
+ */
98
+ options?: AgentModelOption[];
99
+ /**
100
+ * Context-window size in tokens, when the CLI reports it (e.g. pi's
101
+ * `--list-models` `context` column). Lets the adapter emit `usage.contextWindow`
102
+ * per turn so the phone can show context usage as a percentage. Absent when the
103
+ * CLI does not expose it.
104
+ */
105
+ contextWindow?: number;
106
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Declarative description of what an agent CLI adapter supports.
3
+ *
4
+ * Source: architecture/02a-system-architecture.md §5.8.2 (adapters).
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=agent-capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-capabilities.js","sourceRoot":"","sources":["../../../src/agents/agent-capabilities.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Per-project agent configuration.
3
+ *
4
+ * Source: architecture/02a-system-architecture.md §5.8.2.
5
+ */
6
+ import type { AgentId } from './agent-capabilities.js';
7
+ export interface AgentConfig {
8
+ agentId: AgentId;
9
+ /** Absolute path to the agent CLI binary, or null to resolve from PATH. */
10
+ binaryPath?: string;
11
+ /** Extra CLI arguments passed on every invocation. */
12
+ extraArgs?: string[];
13
+ /** Working directory override; defaults to the project cwd. */
14
+ cwd?: string;
15
+ /** Default model for this project/agent (e.g. `provider/model`), when pinned. */
16
+ model?: string;
17
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=agent-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-config.js","sourceRoot":"","sources":["../../../src/agents/agent-config.ts"],"names":[],"mappings":""}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Protocol-wide constants shared by the bridge, relay and mobile app.
3
+ *
4
+ * These mirror the mobile side's `lib/core/constants/protocol_constants.dart`
5
+ * and MUST stay byte-for-byte compatible with it.
6
+ *
7
+ * Source: architecture/02a-system-architecture.md §5.9.1.
8
+ */
9
+ /** Secure transport protocol version negotiated in the handshake. */
10
+ export declare const SECURE_PROTOCOL_VERSION = 1;
11
+ /** Version stamped into the pairing QR payload. */
12
+ export declare const PAIRING_QR_VERSION = 2;
13
+ /** HKDF `info` tag used when deriving the session key. */
14
+ export declare const HKDF_INFO_TAG = "uxnan-e2ee-v1";
15
+ /** Maximum age of a pairing payload before it is rejected (5 minutes). */
16
+ export declare const MAX_PAIRING_AGE_MS = 300000;
17
+ /** Allowed clock skew during handshake validation (60 seconds). */
18
+ export declare const CLOCK_SKEW_TOLERANCE_MS = 60000;
19
+ /** Allowed clock skew during trusted reconnect (90 seconds). */
20
+ export declare const TRUSTED_RECONNECT_SKEW_MS = 90000;
21
+ /** Bridge outbound buffer cap (messages) for catch-up on reconnect. */
22
+ export declare const MAX_BRIDGE_OUTBOUND_MESSAGES = 500;
23
+ /** Bridge outbound buffer cap (bytes) for catch-up on reconnect — 10 MiB. */
24
+ export declare const MAX_BRIDGE_OUTBOUND_BYTES = 10485760;
25
+ /** Default relay endpoint. */
26
+ export declare const DEFAULT_RELAY_URL = "wss://relay.uxnan.io";
27
+ /** Default LAN port for direct WebSocket connections. */
28
+ export declare const DEFAULT_LAN_PORT = 19850;
29
+ /** JSON-RPC protocol version string. */
30
+ export declare const JSONRPC_VERSION = "2.0";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Protocol-wide constants shared by the bridge, relay and mobile app.
3
+ *
4
+ * These mirror the mobile side's `lib/core/constants/protocol_constants.dart`
5
+ * and MUST stay byte-for-byte compatible with it.
6
+ *
7
+ * Source: architecture/02a-system-architecture.md §5.9.1.
8
+ */
9
+ /** Secure transport protocol version negotiated in the handshake. */
10
+ export const SECURE_PROTOCOL_VERSION = 1;
11
+ /** Version stamped into the pairing QR payload. */
12
+ export const PAIRING_QR_VERSION = 2;
13
+ /** HKDF `info` tag used when deriving the session key. */
14
+ export const HKDF_INFO_TAG = 'uxnan-e2ee-v1';
15
+ /** Maximum age of a pairing payload before it is rejected (5 minutes). */
16
+ export const MAX_PAIRING_AGE_MS = 300_000;
17
+ /** Allowed clock skew during handshake validation (60 seconds). */
18
+ export const CLOCK_SKEW_TOLERANCE_MS = 60_000;
19
+ /** Allowed clock skew during trusted reconnect (90 seconds). */
20
+ export const TRUSTED_RECONNECT_SKEW_MS = 90_000;
21
+ /** Bridge outbound buffer cap (messages) for catch-up on reconnect. */
22
+ export const MAX_BRIDGE_OUTBOUND_MESSAGES = 500;
23
+ /** Bridge outbound buffer cap (bytes) for catch-up on reconnect — 10 MiB. */
24
+ export const MAX_BRIDGE_OUTBOUND_BYTES = 10_485_760;
25
+ /** Default relay endpoint. */
26
+ export const DEFAULT_RELAY_URL = 'wss://relay.uxnan.io';
27
+ /** Default LAN port for direct WebSocket connections. */
28
+ export const DEFAULT_LAN_PORT = 19850;
29
+ /** JSON-RPC protocol version string. */
30
+ export const JSONRPC_VERSION = '2.0';
31
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,qEAAqE;AACrE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAEzC,mDAAmD;AACnD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAEpC,0DAA0D;AAC1D,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAE7C,0EAA0E;AAC1E,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAE1C,mEAAmE;AACnE,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C,gEAAgE;AAChE,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAEhD,uEAAuE;AACvE,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,CAAC;AAEhD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,yBAAyB,GAAG,UAAU,CAAC;AAEpD,8BAA8B;AAC9B,MAAM,CAAC,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AAExD,yDAAyD;AACzD,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAEtC,wCAAwC;AACxC,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Encrypted transport envelope (AES-256-GCM).
3
+ *
4
+ * Source: architecture/02a-system-architecture.md §5.9.1 (Phase 3).
5
+ */
6
+ export interface SecureEnvelope {
7
+ kind: 'encryptedEnvelope';
8
+ sessionId: string;
9
+ /** Monotonic sequence number (replay protection). */
10
+ seq: number;
11
+ /** Per-message random nonce (hex, 12 bytes). */
12
+ nonce: string;
13
+ /** base64 AES-256-GCM ciphertext. */
14
+ ciphertext: string;
15
+ /** base64 GCM auth tag (16 bytes). */
16
+ tag: string;
17
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Encrypted transport envelope (AES-256-GCM).
3
+ *
4
+ * Source: architecture/02a-system-architecture.md §5.9.1 (Phase 3).
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../../src/e2ee/envelope.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * E2EE handshake message types.
3
+ *
4
+ * Source: architecture/02a-system-architecture.md §5.9.1.
5
+ *
6
+ * Canonical transcript encoding (the bytes that get signed) — the bridge MUST
7
+ * reproduce this byte-for-byte to interoperate with the mobile app:
8
+ *
9
+ * transcript = clientNonce || phoneEphemeralPublicKey || macEphemeralPublicKey
10
+ * || serverNonce || sessionId || keyEpoch || expiresAtForTranscript
11
+ *
12
+ * Each field is encoded in its *wire* form and UTF-8 concatenated, in order:
13
+ * - byte fields (nonces, ephemeral keys): lowercase hex
14
+ * - sessionId: the string as-is
15
+ * - integers (keyEpoch, expiresAtForTranscript): decimal string
16
+ */
17
+ import type { HandshakeMode } from '../models/session.js';
18
+ export interface ClientHello {
19
+ kind: 'clientHello';
20
+ protocolVersion: number;
21
+ sessionId: string;
22
+ handshakeMode: HandshakeMode;
23
+ phoneDeviceId: string;
24
+ /** Ed25519 identity public key (hex, 32 bytes). */
25
+ phoneIdentityPublicKey: string;
26
+ /** X25519 ephemeral public key (hex, 32 bytes). */
27
+ phoneEphemeralPublicKey: string;
28
+ /** Random nonce (hex, 32 bytes). */
29
+ clientNonce: string;
30
+ resumeState?: ResumeState;
31
+ }
32
+ export interface ResumeState {
33
+ lastAppliedBridgeOutboundSeq: number;
34
+ }
35
+ export interface ServerHello {
36
+ kind: 'serverHello';
37
+ protocolVersion: number;
38
+ sessionId: string;
39
+ handshakeMode: HandshakeMode;
40
+ macDeviceId: string;
41
+ /** Ed25519 identity public key (hex, 32 bytes). */
42
+ macIdentityPublicKey: string;
43
+ /** X25519 ephemeral public key (hex, 32 bytes). */
44
+ macEphemeralPublicKey: string;
45
+ /** Random nonce (hex, 32 bytes). */
46
+ serverNonce: string;
47
+ keyEpoch: number;
48
+ expiresAtForTranscript: number;
49
+ /** Ed25519 signature over the transcript (hex, 64 bytes). */
50
+ macSignature: string;
51
+ /** Echo of the client nonce (hex). */
52
+ clientNonce: string;
53
+ displayName: string;
54
+ }
55
+ export interface ClientAuth {
56
+ kind: 'clientAuth';
57
+ sessionId: string;
58
+ phoneDeviceId: string;
59
+ keyEpoch: number;
60
+ /** Ed25519 signature over the same transcript (hex, 64 bytes). */
61
+ phoneSignature: string;
62
+ }
63
+ export interface HandshakeReady {
64
+ kind: 'ready';
65
+ sessionId: string;
66
+ keyEpoch: number;
67
+ macDeviceId: string;
68
+ }
69
+ export type HandshakeMessage = ClientHello | ServerHello | ClientAuth | HandshakeReady;
70
+ /**
71
+ * Build the canonical transcript string that both peers sign. See the module
72
+ * docstring for the exact encoding contract.
73
+ */
74
+ export declare function buildHandshakeTranscript(fields: {
75
+ clientNonce: string;
76
+ phoneEphemeralPublicKey: string;
77
+ macEphemeralPublicKey: string;
78
+ serverNonce: string;
79
+ sessionId: string;
80
+ keyEpoch: number;
81
+ expiresAtForTranscript: number;
82
+ }): string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Build the canonical transcript string that both peers sign. See the module
3
+ * docstring for the exact encoding contract.
4
+ */
5
+ export function buildHandshakeTranscript(fields) {
6
+ return (fields.clientNonce +
7
+ fields.phoneEphemeralPublicKey +
8
+ fields.macEphemeralPublicKey +
9
+ fields.serverNonce +
10
+ fields.sessionId +
11
+ String(fields.keyEpoch) +
12
+ String(fields.expiresAtForTranscript));
13
+ }
14
+ //# sourceMappingURL=handshake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handshake.js","sourceRoot":"","sources":["../../../src/e2ee/handshake.ts"],"names":[],"mappings":"AA4EA;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAQxC;IACC,OAAO,CACL,MAAM,CAAC,WAAW;QAClB,MAAM,CAAC,uBAAuB;QAC9B,MAAM,CAAC,qBAAqB;QAC5B,MAAM,CAAC,WAAW;QAClB,MAAM,CAAC,SAAS;QAChB,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC;QACvB,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC,CACtC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,44 @@
1
+ export interface PairingPayload {
2
+ /** Payload version. Always {@link PAIRING_QR_VERSION}. */
3
+ v: number;
4
+ /**
5
+ * Relay WebSocket URL — the remote fallback transport. OPTIONAL: a LAN/Tailscale
6
+ * setup pairs over {@link hosts} alone with no hosted relay.
7
+ */
8
+ relay?: string;
9
+ /**
10
+ * Direct `host:port` addresses where the bridge's LAN server listens (the bridge's
11
+ * non-internal IPv4s — LAN and e.g. a Tailscale `100.x` address). The phone should
12
+ * try these FIRST and fall back to {@link relay}. At least one of `hosts`/`relay`
13
+ * must be present.
14
+ */
15
+ hosts?: string[];
16
+ sessionId: string;
17
+ macDeviceId: string;
18
+ /** Bridge Ed25519 identity public key (hex, 32 bytes). */
19
+ macIdentityPublicKey: string;
20
+ /** Unix epoch ms when this payload expires. */
21
+ expiresAt: number;
22
+ displayName: string;
23
+ }
24
+ export type PairingValidationError = 'invalid_json' | 'not_an_object' | 'unsupported_version' | 'missing_field' | 'missing_transport' | 'expired';
25
+ export type PairingValidationResult = {
26
+ valid: true;
27
+ payload: PairingPayload;
28
+ } | {
29
+ valid: false;
30
+ error: PairingValidationError;
31
+ detail?: string;
32
+ };
33
+ /** Serialize a pairing payload to the QR string: Base64 of the UTF-8 JSON. */
34
+ export declare function encodePairingQr(payload: PairingPayload): string;
35
+ /**
36
+ * Validate a decoded pairing payload object against the v2 contract.
37
+ *
38
+ * @param now current time in epoch ms (injected for testability)
39
+ */
40
+ export declare function validatePairingPayload(value: unknown, now: number): PairingValidationResult;
41
+ /** Parse a QR string (Base64 of UTF-8 JSON) and validate it in one step. */
42
+ export declare function parsePairingQr(qr: string, now: number): PairingValidationResult;
43
+ /** Convenience: the default expiry for a freshly generated payload. */
44
+ export declare function defaultPairingExpiry(now: number): number;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Pairing QR payload (version 2).
3
+ *
4
+ * Source: architecture/02a-system-architecture.md §5.9.1 (Phase 1) and
5
+ * uxnandesktop/architecture/02e-bridge-integration.md §5.3.
6
+ *
7
+ * The bridge GENERATES this payload; the mobile app parses it
8
+ * (`PairingPayload.fromQrString`). The wire field names below are the contract.
9
+ *
10
+ * QR string encoding: **Base64 of the UTF-8 JSON** — verified against the mobile
11
+ * `PairingPayload.fromQrString` (spec 02a §5.5.4), which does
12
+ * `base64.decode(base64.normalize(qr))` then `jsonDecode`.
13
+ */
14
+ import { MAX_PAIRING_AGE_MS, PAIRING_QR_VERSION } from '../constants.js';
15
+ const REQUIRED_STRING_FIELDS = [
16
+ 'sessionId',
17
+ 'macDeviceId',
18
+ 'macIdentityPublicKey',
19
+ 'displayName',
20
+ ];
21
+ /** Serialize a pairing payload to the QR string: Base64 of the UTF-8 JSON. */
22
+ export function encodePairingQr(payload) {
23
+ return Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64');
24
+ }
25
+ /**
26
+ * Validate a decoded pairing payload object against the v2 contract.
27
+ *
28
+ * @param now current time in epoch ms (injected for testability)
29
+ */
30
+ export function validatePairingPayload(value, now) {
31
+ if (typeof value !== 'object' || value === null) {
32
+ return { valid: false, error: 'not_an_object' };
33
+ }
34
+ const obj = value;
35
+ if (obj['v'] !== PAIRING_QR_VERSION) {
36
+ return { valid: false, error: 'unsupported_version', detail: String(obj['v']) };
37
+ }
38
+ for (const field of REQUIRED_STRING_FIELDS) {
39
+ if (typeof obj[field] !== 'string' || obj[field].length === 0) {
40
+ return { valid: false, error: 'missing_field', detail: field };
41
+ }
42
+ }
43
+ if (typeof obj['expiresAt'] !== 'number') {
44
+ return { valid: false, error: 'missing_field', detail: 'expiresAt' };
45
+ }
46
+ if (obj['expiresAt'] <= now) {
47
+ return { valid: false, error: 'expired' };
48
+ }
49
+ // Transport fields: `relay` (string) and/or `hosts` (string[]) — at least one.
50
+ const relay = obj['relay'];
51
+ if (relay !== undefined && (typeof relay !== 'string' || relay.length === 0)) {
52
+ return { valid: false, error: 'missing_field', detail: 'relay' };
53
+ }
54
+ const hosts = obj['hosts'];
55
+ if (hosts !== undefined &&
56
+ (!Array.isArray(hosts) || hosts.some((h) => typeof h !== 'string' || h.length === 0))) {
57
+ return { valid: false, error: 'missing_field', detail: 'hosts' };
58
+ }
59
+ const hasRelay = typeof relay === 'string' && relay.length > 0;
60
+ const hasHosts = Array.isArray(hosts) && hosts.length > 0;
61
+ if (!hasRelay && !hasHosts) {
62
+ return { valid: false, error: 'missing_transport' };
63
+ }
64
+ return { valid: true, payload: obj };
65
+ }
66
+ /** Parse a QR string (Base64 of UTF-8 JSON) and validate it in one step. */
67
+ export function parsePairingQr(qr, now) {
68
+ let decoded;
69
+ try {
70
+ const json = Buffer.from(qr.trim(), 'base64').toString('utf-8');
71
+ decoded = JSON.parse(json);
72
+ }
73
+ catch {
74
+ return { valid: false, error: 'invalid_json' };
75
+ }
76
+ return validatePairingPayload(decoded, now);
77
+ }
78
+ /** Convenience: the default expiry for a freshly generated payload. */
79
+ export function defaultPairingExpiry(now) {
80
+ return now + MAX_PAIRING_AGE_MS;
81
+ }
82
+ //# sourceMappingURL=pairing-payload.js.map