@dp-pcs/ogp 0.9.0 → 0.10.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/cli/agent-comms.d.ts +1 -1
- package/dist/cli/agent-comms.d.ts.map +1 -1
- package/dist/cli/agent-comms.js +9 -2
- package/dist/cli/agent-comms.js.map +1 -1
- package/dist/cli/config.d.ts +1 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +81 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/federation.d.ts +47 -3
- package/dist/cli/federation.d.ts.map +1 -1
- package/dist/cli/federation.js +219 -62
- package/dist/cli/federation.js.map +1 -1
- package/dist/cli/tunnel.d.ts +7 -1
- package/dist/cli/tunnel.d.ts.map +1 -1
- package/dist/cli/tunnel.js +12 -3
- package/dist/cli/tunnel.js.map +1 -1
- package/dist/cli.js +63 -20
- package/dist/cli.js.map +1 -1
- package/dist/daemon/agent-comms.d.ts +9 -0
- package/dist/daemon/agent-comms.d.ts.map +1 -1
- package/dist/daemon/agent-comms.js +65 -0
- package/dist/daemon/agent-comms.js.map +1 -1
- package/dist/daemon/relay-client.d.ts +16 -0
- package/dist/daemon/relay-client.d.ts.map +1 -0
- package/dist/daemon/relay-client.js +312 -0
- package/dist/daemon/relay-client.js.map +1 -0
- package/dist/daemon/rendezvous.d.ts +56 -2
- package/dist/daemon/rendezvous.d.ts.map +1 -1
- package/dist/daemon/rendezvous.js +99 -4
- package/dist/daemon/rendezvous.js.map +1 -1
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +140 -26
- package/dist/daemon/server.js.map +1 -1
- package/dist/shared/config.d.ts +28 -0
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +7 -0
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/meta-config.d.ts.map +1 -1
- package/dist/shared/meta-config.js +23 -9
- package/dist/shared/meta-config.js.map +1 -1
- package/dist/shared/relay-protocol.d.ts +91 -0
- package/dist/shared/relay-protocol.d.ts.map +1 -0
- package/dist/shared/relay-protocol.js +55 -0
- package/dist/shared/relay-protocol.js.map +1 -0
- package/docs/CLI-REFERENCE.md +38 -0
- package/docs/TRANSPORT-MODES-DESIGN.md +164 -0
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** The opaque, end-to-end-signed unit the relay forwards verbatim. Identical to
|
|
2
|
+
* the JSON body posted to POST /federation/message in direct mode. */
|
|
3
|
+
export interface RelayFrame {
|
|
4
|
+
message: unknown;
|
|
5
|
+
messageStr: string;
|
|
6
|
+
signature: string;
|
|
7
|
+
}
|
|
8
|
+
/** Whether a daemon's socket is a persistent receiver (registered in the relay
|
|
9
|
+
* routing table) or a transient sender (opened only to await a response leg). */
|
|
10
|
+
export type RelayRole = 'receiver' | 'sender';
|
|
11
|
+
/** Error codes the relay may return in an `error` frame. */
|
|
12
|
+
export type RelayErrorCode = 'peer-not-connected' | 'unauthorized' | 'bad-frame' | 'payload-too-large' | 'challenge-expired';
|
|
13
|
+
/** Max accepted frame size. Envelopes are small; this caps abuse. */
|
|
14
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
15
|
+
/** App-level heartbeat period (must stay < ALB 60s idle timeout). */
|
|
16
|
+
export declare const HEARTBEAT_MS = 35000;
|
|
17
|
+
/** How long a server-issued auth challenge stays valid. */
|
|
18
|
+
export declare const CHALLENGE_TTL_MS = 10000;
|
|
19
|
+
/** (1) S→C, on socket open. */
|
|
20
|
+
export interface ChallengeFrame {
|
|
21
|
+
type: 'challenge';
|
|
22
|
+
challengeId: string;
|
|
23
|
+
nonce: string;
|
|
24
|
+
serverTime: string;
|
|
25
|
+
}
|
|
26
|
+
/** (2) C→S. payloadStr/signature = signCanonical({pubkey, challengeId, nonce, role}). */
|
|
27
|
+
export interface AuthFrame {
|
|
28
|
+
type: 'auth';
|
|
29
|
+
pubkey: string;
|
|
30
|
+
challengeId: string;
|
|
31
|
+
payloadStr: string;
|
|
32
|
+
signature: string;
|
|
33
|
+
}
|
|
34
|
+
/** (3) S→C success. */
|
|
35
|
+
export interface AuthOkFrame {
|
|
36
|
+
type: 'auth-ok';
|
|
37
|
+
pubkey: string;
|
|
38
|
+
}
|
|
39
|
+
/** (3) S→C failure (socket closes after). */
|
|
40
|
+
export interface AuthErrFrame {
|
|
41
|
+
type: 'auth-err';
|
|
42
|
+
reason: string;
|
|
43
|
+
}
|
|
44
|
+
/** (4) C→S deliver request / (5) S→C forwarded deliver.
|
|
45
|
+
* `to` is set by the sender; `from` is added by the relay for logging only
|
|
46
|
+
* (never trusted — the signed `frame` is the trust unit). */
|
|
47
|
+
export interface DeliverFrame {
|
|
48
|
+
type: 'deliver';
|
|
49
|
+
reqId: string;
|
|
50
|
+
to?: string;
|
|
51
|
+
from?: string;
|
|
52
|
+
frame: RelayFrame;
|
|
53
|
+
}
|
|
54
|
+
/** (6) C→S / (7) S→C response leg. `result` is the recipient's MessageResponse. */
|
|
55
|
+
export interface ResponseFrame {
|
|
56
|
+
type: 'response';
|
|
57
|
+
reqId: string;
|
|
58
|
+
result: unknown;
|
|
59
|
+
}
|
|
60
|
+
/** (8) S→C error. */
|
|
61
|
+
export interface ErrorFrame {
|
|
62
|
+
type: 'error';
|
|
63
|
+
reqId?: string;
|
|
64
|
+
code: RelayErrorCode;
|
|
65
|
+
message: string;
|
|
66
|
+
}
|
|
67
|
+
/** (9) app-level keepalive, either direction. */
|
|
68
|
+
export interface PingFrame {
|
|
69
|
+
type: 'ping';
|
|
70
|
+
}
|
|
71
|
+
export interface PongFrame {
|
|
72
|
+
type: 'pong';
|
|
73
|
+
}
|
|
74
|
+
export type RelayClientFrame = AuthFrame | DeliverFrame | ResponseFrame | PingFrame | PongFrame;
|
|
75
|
+
export type RelayServerFrame = ChallengeFrame | AuthOkFrame | AuthErrFrame | DeliverFrame | ResponseFrame | ErrorFrame | PingFrame | PongFrame;
|
|
76
|
+
export type RelayAnyFrame = RelayClientFrame | RelayServerFrame;
|
|
77
|
+
/** Parse a raw WS text frame into a typed object, or null if not a `{type}` object. */
|
|
78
|
+
export declare function parseFrame(raw: string): RelayAnyFrame | null;
|
|
79
|
+
export declare function isChallengeFrame(f: RelayAnyFrame): f is ChallengeFrame;
|
|
80
|
+
export declare function isAuthFrame(f: RelayAnyFrame): f is AuthFrame;
|
|
81
|
+
export declare function isDeliverFrame(f: RelayAnyFrame): f is DeliverFrame;
|
|
82
|
+
export declare function isResponseFrame(f: RelayAnyFrame): f is ResponseFrame;
|
|
83
|
+
/** The canonical payload a daemon signs to answer an auth challenge. The nonce
|
|
84
|
+
* is INSIDE the signed bytes so the signature covers it (replay resistance). */
|
|
85
|
+
export interface AuthChallengePayload {
|
|
86
|
+
pubkey: string;
|
|
87
|
+
challengeId: string;
|
|
88
|
+
nonce: string;
|
|
89
|
+
role: RelayRole;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=relay-protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay-protocol.d.ts","sourceRoot":"","sources":["../../src/shared/relay-protocol.ts"],"names":[],"mappings":"AAUA;uEACuE;AACvE,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;kFACkF;AAClF,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE9C,4DAA4D;AAC5D,MAAM,MAAM,cAAc,GACtB,oBAAoB,GACpB,cAAc,GACd,WAAW,GACX,mBAAmB,GACnB,mBAAmB,CAAC;AAExB,qEAAqE;AACrE,eAAO,MAAM,eAAe,QAAa,CAAC;AAE1C,qEAAqE;AACrE,eAAO,MAAM,YAAY,QAAS,CAAC;AAEnC,2DAA2D;AAC3D,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAIvC,+BAA+B;AAC/B,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,yFAAyF;AACzF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,uBAAuB;AACvB,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,6CAA6C;AAC7C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;8DAE8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,qBAAqB;AACrB,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,iDAAiD;AACjD,MAAM,WAAW,SAAS;IAAG,IAAI,EAAE,MAAM,CAAC;CAAE;AAC5C,MAAM,WAAW,SAAS;IAAG,IAAI,EAAE,MAAM,CAAC;CAAE;AAE5C,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,YAAY,GAAG,aAAa,GAAG,SAAS,GAAG,SAAS,CAAC;AAChG,MAAM,MAAM,gBAAgB,GACxB,cAAc,GAAG,WAAW,GAAG,YAAY,GAAG,YAAY,GAAG,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AACpH,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;AAIhE,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAW5D;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,CAAC,IAAI,cAAc,CAItE;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,aAAa,GAAG,CAAC,IAAI,SAAS,CAO5D;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,aAAa,GAAG,CAAC,IAAI,YAAY,CAOlE;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,aAAa,GAAG,CAAC,IAAI,aAAa,CAEpE;AAED;iFACiF;AACjF,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;CACjB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Relay transport wire protocol (bd-b7em Phase 2).
|
|
2
|
+
//
|
|
3
|
+
// Frames are JSON text over a WebSocket between a daemon and the relay server.
|
|
4
|
+
// The relay is UNTRUSTED: it routes opaque `frame` payloads (the exact body a
|
|
5
|
+
// daemon would POST to /federation/message) by recipient pubkey and never
|
|
6
|
+
// inspects or forges them. End-to-end Ed25519 lives entirely inside `frame`.
|
|
7
|
+
//
|
|
8
|
+
// Auth proves pubkey ownership via a server-issued challenge nonce signed with
|
|
9
|
+
// signCanonical (src/shared/signing.ts). See docs/TRANSPORT-MODES-DESIGN.md.
|
|
10
|
+
/** Max accepted frame size. Envelopes are small; this caps abuse. */
|
|
11
|
+
export const MAX_FRAME_BYTES = 256 * 1024;
|
|
12
|
+
/** App-level heartbeat period (must stay < ALB 60s idle timeout). */
|
|
13
|
+
export const HEARTBEAT_MS = 35_000;
|
|
14
|
+
/** How long a server-issued auth challenge stays valid. */
|
|
15
|
+
export const CHALLENGE_TTL_MS = 10_000;
|
|
16
|
+
// ── Parsing / guards ─────────────────────────────────────────────────────────
|
|
17
|
+
/** Parse a raw WS text frame into a typed object, or null if not a `{type}` object. */
|
|
18
|
+
export function parseFrame(raw) {
|
|
19
|
+
let obj;
|
|
20
|
+
try {
|
|
21
|
+
obj = JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (!obj || typeof obj !== 'object' || typeof obj.type !== 'string') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return obj;
|
|
30
|
+
}
|
|
31
|
+
export function isChallengeFrame(f) {
|
|
32
|
+
return f.type === 'challenge'
|
|
33
|
+
&& typeof f.challengeId === 'string'
|
|
34
|
+
&& typeof f.nonce === 'string';
|
|
35
|
+
}
|
|
36
|
+
export function isAuthFrame(f) {
|
|
37
|
+
const a = f;
|
|
38
|
+
return f.type === 'auth'
|
|
39
|
+
&& typeof a.pubkey === 'string'
|
|
40
|
+
&& typeof a.challengeId === 'string'
|
|
41
|
+
&& typeof a.payloadStr === 'string'
|
|
42
|
+
&& typeof a.signature === 'string';
|
|
43
|
+
}
|
|
44
|
+
export function isDeliverFrame(f) {
|
|
45
|
+
const d = f;
|
|
46
|
+
return f.type === 'deliver'
|
|
47
|
+
&& typeof d.reqId === 'string'
|
|
48
|
+
&& !!d.frame && typeof d.frame === 'object'
|
|
49
|
+
&& typeof d.frame.messageStr === 'string'
|
|
50
|
+
&& typeof d.frame.signature === 'string';
|
|
51
|
+
}
|
|
52
|
+
export function isResponseFrame(f) {
|
|
53
|
+
return f.type === 'response' && typeof f.reqId === 'string';
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=relay-protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay-protocol.js","sourceRoot":"","sources":["../../src/shared/relay-protocol.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAC9E,0EAA0E;AAC1E,6EAA6E;AAC7E,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAsB7E,qEAAqE;AACrE,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,GAAG,IAAI,CAAC;AAE1C,qEAAqE;AACrE,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AAEnC,2DAA2D;AAC3D,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAoEvC,gFAAgF;AAEhF,uFAAuF;AACvF,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAQ,GAA0B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC5F,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,GAAoB,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,CAAgB;IAC/C,OAAO,CAAC,CAAC,IAAI,KAAK,WAAW;WACxB,OAAQ,CAAoB,CAAC,WAAW,KAAK,QAAQ;WACrD,OAAQ,CAAoB,CAAC,KAAK,KAAK,QAAQ,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAgB;IAC1C,MAAM,CAAC,GAAG,CAAc,CAAC;IACzB,OAAO,CAAC,CAAC,IAAI,KAAK,MAAM;WACnB,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;WAC5B,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ;WACjC,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ;WAChC,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,CAAgB;IAC7C,MAAM,CAAC,GAAG,CAAiB,CAAC;IAC5B,OAAO,CAAC,CAAC,IAAI,KAAK,SAAS;WACtB,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;WAC3B,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;WACxC,OAAO,CAAC,CAAC,KAAK,CAAC,UAAU,KAAK,QAAQ;WACtC,OAAO,CAAC,CAAC,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,CAAgB;IAC9C,OAAO,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,OAAQ,CAAmB,CAAC,KAAK,KAAK,QAAQ,CAAC;AACjF,CAAC"}
|
package/docs/CLI-REFERENCE.md
CHANGED
|
@@ -5,6 +5,7 @@ Complete command-line reference for OGP (Open Gateway Protocol).
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
7
|
- [Global Options](#global-options)
|
|
8
|
+
- [Environment Variables](#environment-variables)
|
|
8
9
|
- [Setup and Configuration](#setup-and-configuration)
|
|
9
10
|
- [Daemon Management](#daemon-management)
|
|
10
11
|
- [Federation Commands](#federation-commands)
|
|
@@ -49,6 +50,10 @@ ogp --for all status
|
|
|
49
50
|
- If multiple frameworks are configured and no default is set, prompts interactively
|
|
50
51
|
- If a default framework is set, uses default (can override with `--for`)
|
|
51
52
|
- `--for all` runs command on all enabled frameworks and aggregates output
|
|
53
|
+
- If the meta-registry is empty (no `ogp setup` was run) but `OGP_HOME` is set,
|
|
54
|
+
`--for <framework>` honors `OGP_HOME` and prints a warning instead of failing.
|
|
55
|
+
This is the common case in server/container deployments where the daemon is
|
|
56
|
+
launched by setting `OGP_HOME` directly. See [Environment Variables](#environment-variables).
|
|
52
57
|
|
|
53
58
|
### --help, -h
|
|
54
59
|
|
|
@@ -70,6 +75,39 @@ Shows OGP version.
|
|
|
70
75
|
ogp --version
|
|
71
76
|
```
|
|
72
77
|
|
|
78
|
+
## Environment Variables
|
|
79
|
+
|
|
80
|
+
OGP does **not** hardcode its storage location. The default is `~/.ogp`, but
|
|
81
|
+
every path is resolved at runtime and can be overridden via environment
|
|
82
|
+
variables. This is the supported way to run OGP in containers, on servers
|
|
83
|
+
(ECS/nginx), or anywhere `$HOME` is not where you want config to live — no code
|
|
84
|
+
changes or recompilation required.
|
|
85
|
+
|
|
86
|
+
| Variable | Overrides | Default |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| `OGP_HOME` | Per-framework data dir: `config.json`, `peers.json`, identity, keychain. **The daemon only needs this.** | `~/.ogp` |
|
|
89
|
+
| `OGP_META_HOME` | The multi-framework meta-registry (`config.json` listing frameworks for `--for`). | `~/.ogp-meta` |
|
|
90
|
+
|
|
91
|
+
**Server / container deployment (recommended):**
|
|
92
|
+
```bash
|
|
93
|
+
# Point both the daemon and the CLI at the same writable location.
|
|
94
|
+
export OGP_HOME=/data/ogp # or e.g. /home/node/.openclaw/.ogp
|
|
95
|
+
ogp start --background
|
|
96
|
+
ogp peers list # CLI inherits OGP_HOME, talks to the daemon
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Common pitfall:** If the daemon is launched with `OGP_HOME` set but you then
|
|
100
|
+
run `ogp --for openclaw ...`, the CLI looks up `openclaw` in the meta-registry
|
|
101
|
+
(`OGP_META_HOME`), which was never populated by an interactive `ogp setup`.
|
|
102
|
+
As of this version, `--for` falls back to `OGP_HOME` with a warning instead of
|
|
103
|
+
erroring. The clean fix is to **not pass `--for` on a single-home server** — just
|
|
104
|
+
export `OGP_HOME` and let every `ogp` invocation inherit it.
|
|
105
|
+
|
|
106
|
+
**$HOME mismatch:** If the daemon and CLI run under different users/`$HOME`
|
|
107
|
+
values (common in containers), the meta-registry can silently "not exist" for
|
|
108
|
+
one of them. Set `OGP_META_HOME` (and `OGP_HOME`) explicitly so both processes
|
|
109
|
+
resolve the same paths.
|
|
110
|
+
|
|
73
111
|
## Setup and Configuration
|
|
74
112
|
|
|
75
113
|
### ogp setup
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Transport Modes Design
|
|
2
|
+
|
|
3
|
+
**Status:** 🚧 Proposed — under review. The rendezvous registration schema change is adjacent to the Ed25519 trust model and should be reviewed carefully before merge.
|
|
4
|
+
|
|
5
|
+
**Date:** 2026-06-08
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
OGP's biggest adoption deterrent is the tunnel requirement. Today peers deliver
|
|
10
|
+
Ed25519-signed envelopes by **direct HTTP POST to each other's public
|
|
11
|
+
`gatewayUrl`**, and the rendezvous server is a pubkey→address directory that
|
|
12
|
+
hands back a raw `ip:port`. Even *with* rendezvous, a peer must be publicly
|
|
13
|
+
reachable — which is exactly the job the cloudflared/ngrok tunnel does.
|
|
14
|
+
|
|
15
|
+
The tunnel is **not a transport requirement — it's a reachability hack.** We want
|
|
16
|
+
to make reachability a *user choice*, so people who don't want to run a tunnel can
|
|
17
|
+
opt into a relay instead, without forcing anyone already on tunnels to change.
|
|
18
|
+
|
|
19
|
+
## Key insight that makes this safe
|
|
20
|
+
|
|
21
|
+
**OGP messages are already Ed25519-signed end-to-end.** Any relay/transport in the
|
|
22
|
+
middle is *untrusted by design* — it can see envelope metadata but cannot forge,
|
|
23
|
+
alter, or impersonate. This radically lowers the security bar for the transport
|
|
24
|
+
layer and makes "route through a relay" perfectly acceptable. This single fact is
|
|
25
|
+
what makes a pluggable transport possible without weakening the trust model.
|
|
26
|
+
|
|
27
|
+
## Goal
|
|
28
|
+
|
|
29
|
+
A per-user, per-daemon `transport` setting that lets each operator choose how their
|
|
30
|
+
daemon is reached, defaulting to **today's behavior** so no existing setup breaks:
|
|
31
|
+
|
|
32
|
+
- **`direct`** (default) — public `gatewayUrl` via tunnel / public IP / port-forward. Exactly what works today. Zero change for current users.
|
|
33
|
+
- **`relay`** — daemon holds a persistent outbound WebSocket to a relay; messages route peer→relay→peer. No inbound port, no tunnel. User picks *which* relay (ours by default, or a self-hosted one).
|
|
34
|
+
- **`iroh`** — daemon uses Iroh (QUIC). ~90% of pairs get a direct P2P connection; the rest fall back through a relay. E2E-encrypted regardless. Self-hostable relay. The deliberate pilot.
|
|
35
|
+
|
|
36
|
+
## User experience
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Default — nothing changes for existing users
|
|
40
|
+
ogp config get transport.mode # => direct
|
|
41
|
+
|
|
42
|
+
# Opt into relay (no tunnel needed)
|
|
43
|
+
ogp config set transport.mode relay
|
|
44
|
+
ogp config set transport.relay.url wss://relay.example.com/relay # default if omitted
|
|
45
|
+
|
|
46
|
+
# Power user / privacy: self-hosted relay
|
|
47
|
+
ogp config set transport.relay.url wss://relay.mycorp.internal
|
|
48
|
+
|
|
49
|
+
# Pilot iroh
|
|
50
|
+
ogp config set transport.mode iroh
|
|
51
|
+
ogp config set transport.iroh.relayUrl https://my-dedicated-relay # omit = public dev relays
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A mixed fleet works: each peer advertises *how* to reach it, and senders branch on
|
|
55
|
+
the **receiver's** advertised transport (see schema change below).
|
|
56
|
+
|
|
57
|
+
## Config shape
|
|
58
|
+
|
|
59
|
+
Add a `transport` block to `OGPConfig` (`src/shared/config.ts`). Absent ⇒ `direct`.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
export type TransportMode = 'direct' | 'relay' | 'iroh';
|
|
63
|
+
|
|
64
|
+
export interface TransportConfig {
|
|
65
|
+
mode: TransportMode; // default 'direct'
|
|
66
|
+
relay?: {
|
|
67
|
+
url: string; // websocket relay endpoint; default wss://<rendezvous>/relay
|
|
68
|
+
};
|
|
69
|
+
iroh?: {
|
|
70
|
+
relayUrl?: string; // dedicated/self-hosted iroh relay; omit = public dev relays
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// OGPConfig:
|
|
75
|
+
// transport?: TransportConfig;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## The keystone change — rendezvous advertises *how*, not just *where*
|
|
79
|
+
|
|
80
|
+
This is the one structural change everything else depends on, and the part that
|
|
81
|
+
**must be reviewed carefully before merge** (it sits next to the signed
|
|
82
|
+
registration / trust model).
|
|
83
|
+
|
|
84
|
+
Today the rendezvous `/register` stores `{ pubkey, ip, port, lastSeen }` and
|
|
85
|
+
`/peer/:pubkey` returns `{ pubkey, ip, port, lastSeen }`. We extend the registered
|
|
86
|
+
record with a **transport descriptor** so a sender knows which path to use:
|
|
87
|
+
|
|
88
|
+
```jsonc
|
|
89
|
+
// direct (default / backward compatible — missing descriptor ⇒ direct)
|
|
90
|
+
{ "transport": "direct", "gatewayUrl": "https://peer.example.com" }
|
|
91
|
+
|
|
92
|
+
// relay
|
|
93
|
+
{ "transport": "relay", "relayUrl": "wss://relay.example.com/relay" }
|
|
94
|
+
|
|
95
|
+
// iroh
|
|
96
|
+
{ "transport": "iroh", "nodeId": "<iroh-node-id>", "relayUrl": "https://..." }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Rules:
|
|
100
|
+
- **Additive & backward compatible.** A registration with no descriptor is treated
|
|
101
|
+
as `direct` with the existing `ip:port` / `gatewayUrl`. Old daemons keep working.
|
|
102
|
+
- The descriptor rides **inside the existing signed registration envelope**
|
|
103
|
+
(`signCanonical` over the inner payload) so the rendezvous can't be tricked into
|
|
104
|
+
advertising a transport the keyholder didn't choose. **This is the trust-model
|
|
105
|
+
touchpoint — review carefully before merge.**
|
|
106
|
+
- Senders branch delivery on the peer's advertised `transport`, not their own.
|
|
107
|
+
|
|
108
|
+
## Relay server (mode = `relay`)
|
|
109
|
+
|
|
110
|
+
Extends the existing rendezvous service (Node/Express on ECS Fargate). No new box.
|
|
111
|
+
|
|
112
|
+
- New `wss://<rendezvous>/relay` endpoint. Each daemon opens a **persistent
|
|
113
|
+
outbound** WebSocket and authenticates by signing a challenge with its Ed25519
|
|
114
|
+
key (proving pubkey ownership, same property as `/register`).
|
|
115
|
+
- Routing table: `pubkey → live socket`. To deliver, sender pushes
|
|
116
|
+
`{ to: pubkeyB, envelope }`; server forwards down B's socket. Optional
|
|
117
|
+
store-and-forward queue for offline peers, flushed on reconnect.
|
|
118
|
+
- **ECS/ALB gotcha (confirmed):** ALB idle timeout defaults to 60s, max 4000s. We
|
|
119
|
+
**must** send app-level WS ping/heartbeat (~30–50s) or idle sockets get dropped.
|
|
120
|
+
- **Horizontal scale:** one Node process handles tens of thousands of idle
|
|
121
|
+
sockets; past one Fargate task we need a shared routing table (Redis pub/sub) so
|
|
122
|
+
a message arriving on task-1 reaches a socket on task-2.
|
|
123
|
+
- Server stays **untrusted** — it sees envelopes but can't forge them (E2E Ed25519).
|
|
124
|
+
|
|
125
|
+
## Iroh (mode = `iroh`) — pilot notes
|
|
126
|
+
|
|
127
|
+
- QUIC, node-ID addressing. Relays do (1) NAT-traversal coordination and (2)
|
|
128
|
+
encrypted fallback; **relays cannot read traffic** (E2E). ~9/10 pairs get a
|
|
129
|
+
direct connection; traversal is deterministic once it works for a pair.
|
|
130
|
+
- Relays are **stateless/disposable** (no DB, no state migration, reconnect to any).
|
|
131
|
+
- **Cost/effort caveats:** Iroh is Rust; OGP is Node. Official **NAPI Node bindings**
|
|
132
|
+
exist (`iroh` npm), so it's a native-addon dependency with per-platform prebuilds,
|
|
133
|
+
not a full rewrite — but it *is* a new native dep for a daemon that ships daily.
|
|
134
|
+
Public relays are **dev/test only** (rate-limited, no SLA); production = dedicated
|
|
135
|
+
relays (self-hosted open-source binary from n0-computer, or n0's paid Iroh
|
|
136
|
+
Services). Addressing changes from URL → node ID, so the delivery path is rewired.
|
|
137
|
+
- Treat as **opt-in pilot**, not default. Once the transport descriptor exists,
|
|
138
|
+
iroh is "just another transport value," not a rewrite-or-nothing bet.
|
|
139
|
+
|
|
140
|
+
Sources: https://docs.iroh.computer/concepts/relays ,
|
|
141
|
+
https://docs.aws.amazon.com/elasticloadbalancing/latest/application/edit-load-balancer-attributes.html
|
|
142
|
+
|
|
143
|
+
## Phased build plan
|
|
144
|
+
|
|
145
|
+
1. **Transport descriptor in register/lookup** (additive, backward compatible;
|
|
146
|
+
missing ⇒ `direct`). Unblocks everything, low risk. **Schema/trust touchpoint —
|
|
147
|
+
review carefully before merge.**
|
|
148
|
+
2. **`relay` mode** — WebSocket through rendezvous + signed-challenge auth +
|
|
149
|
+
ALB heartbeat. Lowest code; kills the tunnel requirement; opt-in dogfooding.
|
|
150
|
+
3. **`iroh` mode pilot** — once relay is proven, add iroh as another descriptor
|
|
151
|
+
value behind a dedicated/self-hosted relay.
|
|
152
|
+
|
|
153
|
+
## Hard rules / guardrails
|
|
154
|
+
|
|
155
|
+
- **Default stays `direct`** — never silently change how existing users' traffic moves.
|
|
156
|
+
- **No auto-deploy** of the rendezvous/relay server — changes are proposed and reviewed, not shipped automatically.
|
|
157
|
+
- **Registration-schema + relay-auth code requires careful review before merge** — it's adjacent to the Ed25519 trust model, which is the product.
|
|
158
|
+
- E2E Ed25519 signing is preserved in **every** mode; the relay is always untrusted.
|
|
159
|
+
|
|
160
|
+
## Non-goals (for now)
|
|
161
|
+
|
|
162
|
+
- Replacing direct mode. It stays as the privacy/no-third-party path.
|
|
163
|
+
- Picking iroh vs websocket-relay as the *single* answer. The point is choice +
|
|
164
|
+
real-world dogfooding before committing a default beyond `direct`.
|
package/package.json
CHANGED