@ratio-app/cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +504 -0
- package/bin/run.mjs +5 -0
- package/dist/commands/__test__/throw.d.ts +25 -0
- package/dist/commands/__test__/throw.js +238 -0
- package/dist/commands/__test__/throw.js.map +1 -0
- package/dist/commands/app/create.d.ts +55 -0
- package/dist/commands/app/create.js +609 -0
- package/dist/commands/app/create.js.map +1 -0
- package/dist/commands/app/deploy.d.ts +14 -0
- package/dist/commands/app/deploy.js +179 -0
- package/dist/commands/app/deploy.js.map +1 -0
- package/dist/commands/app/dev.d.ts +28 -0
- package/dist/commands/app/dev.js +701 -0
- package/dist/commands/app/dev.js.map +1 -0
- package/dist/commands/app/generate.d.ts +14 -0
- package/dist/commands/app/generate.js +179 -0
- package/dist/commands/app/generate.js.map +1 -0
- package/dist/commands/app/info.d.ts +14 -0
- package/dist/commands/app/info.js +179 -0
- package/dist/commands/app/info.js.map +1 -0
- package/dist/commands/app/link.d.ts +118 -0
- package/dist/commands/app/link.js +1120 -0
- package/dist/commands/app/link.js.map +1 -0
- package/dist/commands/auth/login.d.ts +43 -0
- package/dist/commands/auth/login.js +987 -0
- package/dist/commands/auth/login.js.map +1 -0
- package/dist/commands/auth/logout.d.ts +15 -0
- package/dist/commands/auth/logout.js +486 -0
- package/dist/commands/auth/logout.js.map +1 -0
- package/dist/commands/auth/whoami.d.ts +16 -0
- package/dist/commands/auth/whoami.js +531 -0
- package/dist/commands/auth/whoami.js.map +1 -0
- package/dist/commands/dev/listen.d.ts +84 -0
- package/dist/commands/dev/listen.js +1187 -0
- package/dist/commands/dev/listen.js.map +1 -0
- package/dist/commands/dev/trigger.d.ts +88 -0
- package/dist/commands/dev/trigger.js +729 -0
- package/dist/commands/dev/trigger.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +617 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth/authorize-code-exchange.d.ts +50 -0
- package/dist/lib/auth/authorize-code-exchange.js +114 -0
- package/dist/lib/auth/authorize-code-exchange.js.map +1 -0
- package/dist/lib/auth/authorize-url.d.ts +42 -0
- package/dist/lib/auth/authorize-url.js +35 -0
- package/dist/lib/auth/authorize-url.js.map +1 -0
- package/dist/lib/auth/browser.d.ts +16 -0
- package/dist/lib/auth/browser.js +48 -0
- package/dist/lib/auth/browser.js.map +1 -0
- package/dist/lib/auth/config.d.ts +41 -0
- package/dist/lib/auth/config.js +21 -0
- package/dist/lib/auth/config.js.map +1 -0
- package/dist/lib/auth/http-refresher.d.ts +30 -0
- package/dist/lib/auth/http-refresher.js +103 -0
- package/dist/lib/auth/http-refresher.js.map +1 -0
- package/dist/lib/auth/identity.d.ts +26 -0
- package/dist/lib/auth/identity.js +10 -0
- package/dist/lib/auth/identity.js.map +1 -0
- package/dist/lib/auth/index.d.ts +13 -0
- package/dist/lib/auth/index.js +527 -0
- package/dist/lib/auth/index.js.map +1 -0
- package/dist/lib/auth/loopback-server.d.ts +65 -0
- package/dist/lib/auth/loopback-server.js +245 -0
- package/dist/lib/auth/loopback-server.js.map +1 -0
- package/dist/lib/auth/manual-prompt.d.ts +17 -0
- package/dist/lib/auth/manual-prompt.js +20 -0
- package/dist/lib/auth/manual-prompt.js.map +1 -0
- package/dist/lib/auth/pkce.d.ts +24 -0
- package/dist/lib/auth/pkce.js +15 -0
- package/dist/lib/auth/pkce.js.map +1 -0
- package/dist/lib/auth/state.d.ts +20 -0
- package/dist/lib/auth/state.js +19 -0
- package/dist/lib/auth/state.js.map +1 -0
- package/dist/lib/auth/verbose-trace.d.ts +25 -0
- package/dist/lib/auth/verbose-trace.js +17 -0
- package/dist/lib/auth/verbose-trace.js.map +1 -0
- package/dist/lib/credentials/clock.d.ts +18 -0
- package/dist/lib/credentials/clock.js +10 -0
- package/dist/lib/credentials/clock.js.map +1 -0
- package/dist/lib/credentials/index.d.ts +5 -0
- package/dist/lib/credentials/index.js +340 -0
- package/dist/lib/credentials/index.js.map +1 -0
- package/dist/lib/credentials/path.d.ts +26 -0
- package/dist/lib/credentials/path.js +32 -0
- package/dist/lib/credentials/path.js.map +1 -0
- package/dist/lib/credentials/refresher.d.ts +26 -0
- package/dist/lib/credentials/refresher.js +34 -0
- package/dist/lib/credentials/refresher.js.map +1 -0
- package/dist/lib/credentials/schema.d.ts +138 -0
- package/dist/lib/credentials/schema.js +85 -0
- package/dist/lib/credentials/schema.js.map +1 -0
- package/dist/lib/credentials/store.d.ts +102 -0
- package/dist/lib/credentials/store.js +337 -0
- package/dist/lib/credentials/store.js.map +1 -0
- package/dist/lib/credentials/types.d.ts +65 -0
- package/dist/lib/credentials/types.js +1 -0
- package/dist/lib/credentials/types.js.map +1 -0
- package/dist/lib/dev-store-gate.d.ts +66 -0
- package/dist/lib/dev-store-gate.js +15 -0
- package/dist/lib/dev-store-gate.js.map +1 -0
- package/dist/lib/errors.d.ts +60 -0
- package/dist/lib/errors.js +101 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/manifest/index.d.ts +2 -0
- package/dist/lib/manifest/index.js +144 -0
- package/dist/lib/manifest/index.js.map +1 -0
- package/dist/lib/manifest/read-manifest.d.ts +32 -0
- package/dist/lib/manifest/read-manifest.js +87 -0
- package/dist/lib/manifest/read-manifest.js.map +1 -0
- package/dist/lib/manifest/write-client-id.d.ts +33 -0
- package/dist/lib/manifest/write-client-id.js +94 -0
- package/dist/lib/manifest/write-client-id.js.map +1 -0
- package/dist/lib/messages.d.ts +15 -0
- package/dist/lib/messages.js +16 -0
- package/dist/lib/messages.js.map +1 -0
- package/dist/lib/orchestrator/child-runner.d.ts +140 -0
- package/dist/lib/orchestrator/child-runner.js +471 -0
- package/dist/lib/orchestrator/child-runner.js.map +1 -0
- package/dist/lib/preview/index.d.ts +11 -0
- package/dist/lib/preview/index.js +17 -0
- package/dist/lib/preview/index.js.map +1 -0
- package/dist/lib/preview/types.d.ts +16 -0
- package/dist/lib/preview/types.js +11 -0
- package/dist/lib/preview/types.js.map +1 -0
- package/dist/lib/ratio-command.d.ts +52 -0
- package/dist/lib/ratio-command.js +141 -0
- package/dist/lib/ratio-command.js.map +1 -0
- package/dist/lib/relay/index.d.ts +7 -0
- package/dist/lib/relay/index.js +362 -0
- package/dist/lib/relay/index.js.map +1 -0
- package/dist/lib/relay/relay-client.d.ts +91 -0
- package/dist/lib/relay/relay-client.js +362 -0
- package/dist/lib/relay/relay-client.js.map +1 -0
- package/dist/lib/relay/types.d.ts +71 -0
- package/dist/lib/relay/types.js +1 -0
- package/dist/lib/relay/types.js.map +1 -0
- package/dist/lib/render-error.d.ts +35 -0
- package/dist/lib/render-error.js +115 -0
- package/dist/lib/render-error.js.map +1 -0
- package/dist/lib/verbose-flag.d.ts +12 -0
- package/dist/lib/verbose-flag.js +17 -0
- package/dist/lib/verbose-flag.js.map +1 -0
- package/dist/render-CrHGqTPH.d.ts +115 -0
- package/dist/templates/.gitkeep +0 -0
- package/dist/templates/minimal/.env.example.tmpl +4 -0
- package/dist/templates/minimal/README.md.tmpl +26 -0
- package/dist/templates/minimal/gitignore +5 -0
- package/dist/templates/minimal/manifest.json +5 -0
- package/dist/templates/minimal/package.json.tmpl +25 -0
- package/dist/templates/minimal/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/minimal/src/index.ts.tmpl +26 -0
- package/dist/templates/minimal/src/install.ts.tmpl +37 -0
- package/dist/templates/minimal/src/webhooks.ts.tmpl +57 -0
- package/dist/templates/minimal/tsconfig.json +17 -0
- package/dist/templates/serverless/README.md.tmpl +28 -0
- package/dist/templates/serverless/gitignore +6 -0
- package/dist/templates/serverless/manifest.json +5 -0
- package/dist/templates/serverless/package.json.tmpl +22 -0
- package/dist/templates/serverless/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/serverless/src/index.ts.tmpl +20 -0
- package/dist/templates/serverless/src/install.ts.tmpl +20 -0
- package/dist/templates/serverless/src/webhooks.ts.tmpl +36 -0
- package/dist/templates/serverless/tsconfig.json +18 -0
- package/dist/templates/serverless/wrangler.toml.tmpl +9 -0
- package/dist/templates/with-admin-ui/.env.example.tmpl +4 -0
- package/dist/templates/with-admin-ui/README.md.tmpl +28 -0
- package/dist/templates/with-admin-ui/gitignore +6 -0
- package/dist/templates/with-admin-ui/manifest.json +5 -0
- package/dist/templates/with-admin-ui/package.json.tmpl +31 -0
- package/dist/templates/with-admin-ui/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/with-admin-ui/src/admin/app/globals.css +19 -0
- package/dist/templates/with-admin-ui/src/admin/app/layout.tsx.tmpl +13 -0
- package/dist/templates/with-admin-ui/src/admin/app/page.tsx.tmpl +8 -0
- package/dist/templates/with-admin-ui/src/admin/next-env.d.ts.tmpl +5 -0
- package/dist/templates/with-admin-ui/src/admin/next.config.mjs +3 -0
- package/dist/templates/with-admin-ui/src/admin/tsconfig.json +20 -0
- package/dist/templates/with-admin-ui/src/index.ts.tmpl +26 -0
- package/dist/templates/with-admin-ui/src/install.ts.tmpl +37 -0
- package/dist/templates/with-admin-ui/src/webhooks.ts.tmpl +57 -0
- package/dist/templates/with-admin-ui/tsconfig.json +17 -0
- package/package.json +78 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { RelayClientOptions, RelayEvent } from './types.js';
|
|
2
|
+
import '../credentials/store.js';
|
|
3
|
+
import '../credentials/clock.js';
|
|
4
|
+
import '../credentials/refresher.js';
|
|
5
|
+
import '../credentials/types.js';
|
|
6
|
+
import '../auth/verbose-trace.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* WebSocket relay tunnel client.
|
|
10
|
+
*
|
|
11
|
+
* Responsibilities:
|
|
12
|
+
* - Connect to the relay stream endpoint with `Authorization: Bearer`
|
|
13
|
+
* at the HTTP handshake (the token never appears in the URL). The
|
|
14
|
+
* server authenticates the upgrade by reading the `sessionId` query
|
|
15
|
+
* parameter, so every connect URL carries `?sessionId=<id>`.
|
|
16
|
+
* - Accept the server-issued stream URL, which may already carry the
|
|
17
|
+
* canonical `?sessionId=` parameter; it must match the session the
|
|
18
|
+
* client was constructed with, and no other query parameter is
|
|
19
|
+
* allowed — the client owns the connect query string.
|
|
20
|
+
* - Track the monotonic per-session sequence number; discard
|
|
21
|
+
* duplicates delivered during a resume replay.
|
|
22
|
+
* - Auto-reconnect on abnormal closes with exponential backoff
|
|
23
|
+
* (capped) and multiplicative jitter, resuming from the last seen
|
|
24
|
+
* sequence via the `resumeCursor` query parameter (sent alongside
|
|
25
|
+
* `sessionId`).
|
|
26
|
+
* - Surface a server-declared resume gap as a typed
|
|
27
|
+
* RelayResumeGapError — recovery policy belongs to the caller.
|
|
28
|
+
* - Deliver events through a pull-based AsyncIterable with a bounded
|
|
29
|
+
* local buffer; a stalled consumer fails fast instead of growing
|
|
30
|
+
* memory without limit.
|
|
31
|
+
*
|
|
32
|
+
* Uses the `ws` package (not the runtime's built-in WebSocket, which
|
|
33
|
+
* is only available as a stable global from Node 22 — this package
|
|
34
|
+
* supports Node 18+).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maximum number of undelivered events held for a slow consumer.
|
|
39
|
+
* Exceeding it rejects the consumer's next pull and closes the
|
|
40
|
+
* connection — the client never buffers without bound.
|
|
41
|
+
*/
|
|
42
|
+
declare const LOCAL_BUFFER_MAX = 1000;
|
|
43
|
+
declare class RelayClient {
|
|
44
|
+
private readonly wsUrl;
|
|
45
|
+
private readonly options;
|
|
46
|
+
private readonly scheduler;
|
|
47
|
+
private readonly random;
|
|
48
|
+
private readonly trace;
|
|
49
|
+
private ws;
|
|
50
|
+
private lastSeq;
|
|
51
|
+
private reconnectAttempt;
|
|
52
|
+
private reconnectTimer;
|
|
53
|
+
private readonly queue;
|
|
54
|
+
private readonly waiters;
|
|
55
|
+
/** First fatal error; once set, every consumer pull rejects with it. */
|
|
56
|
+
private failure;
|
|
57
|
+
/** Clean end of stream (normal close or caller-initiated close()). */
|
|
58
|
+
private ended;
|
|
59
|
+
private userClosed;
|
|
60
|
+
private started;
|
|
61
|
+
private pendingConnect;
|
|
62
|
+
constructor(options: RelayClientOptions);
|
|
63
|
+
/**
|
|
64
|
+
* Opens the first connection. Resolves when the socket is open;
|
|
65
|
+
* rejects with a typed error on authentication rejection or when the
|
|
66
|
+
* stream fails before ever becoming established. May only be called
|
|
67
|
+
* once per client instance.
|
|
68
|
+
*/
|
|
69
|
+
connect(): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Pull-based event stream. Single-consumer: the iterator shares the
|
|
72
|
+
* client's internal queue. Ends when close() is called or the server
|
|
73
|
+
* closes cleanly; rejects with a typed error on fatal conditions.
|
|
74
|
+
*/
|
|
75
|
+
events(): AsyncIterable<RelayEvent>;
|
|
76
|
+
/**
|
|
77
|
+
* Graceful shutdown: cancels any pending reconnect, closes the
|
|
78
|
+
* socket with a normal-closure code, and completes the iterator.
|
|
79
|
+
*/
|
|
80
|
+
close(): Promise<void>;
|
|
81
|
+
private openSocket;
|
|
82
|
+
private handleMessage;
|
|
83
|
+
private deliver;
|
|
84
|
+
private handleClose;
|
|
85
|
+
private scheduleReconnect;
|
|
86
|
+
private fail;
|
|
87
|
+
private endClean;
|
|
88
|
+
private nextEvent;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { LOCAL_BUFFER_MAX, RelayClient };
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// src/lib/relay/relay-client.ts
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
|
|
4
|
+
// src/lib/errors.ts
|
|
5
|
+
var RatioCLIError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
exitCode;
|
|
8
|
+
hint;
|
|
9
|
+
docsUrl;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
super(opts.message);
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
this.code = opts.code;
|
|
14
|
+
this.exitCode = opts.exitCode;
|
|
15
|
+
if (opts.hint !== void 0) this.hint = opts.hint;
|
|
16
|
+
if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var RelayConnectError = class extends RatioCLIError {
|
|
20
|
+
constructor(message, hint = "Verify the tunnel URL and that the auth-server is reachable.") {
|
|
21
|
+
super({ code: "RELAY_CONNECT", exitCode: 6, message, hint });
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var ValidationError = class extends RatioCLIError {
|
|
25
|
+
constructor(message, hint) {
|
|
26
|
+
const opts = {
|
|
27
|
+
code: "VALIDATION_ERROR",
|
|
28
|
+
exitCode: 8,
|
|
29
|
+
message
|
|
30
|
+
};
|
|
31
|
+
if (hint !== void 0) opts.hint = hint;
|
|
32
|
+
super(opts);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var RelayResumeGapError = class extends RatioCLIError {
|
|
36
|
+
constructor(message, hint = "Reconnect from the current cursor; older events are outside the buffer window.") {
|
|
37
|
+
super({ code: "RELAY_RESUME_GAP", exitCode: 9, message, hint });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/lib/relay/relay-client.ts
|
|
42
|
+
var LOCAL_BUFFER_MAX = 1e3;
|
|
43
|
+
var BACKOFF_BASE_MS = 100;
|
|
44
|
+
var BACKOFF_CAP_MS = 3e4;
|
|
45
|
+
var CLOSE_CODE_AUTH_REJECTED = 4401;
|
|
46
|
+
var CLOSE_CODE_NORMAL = 1e3;
|
|
47
|
+
var defaultScheduler = (fn, ms) => {
|
|
48
|
+
const handle = setTimeout(fn, ms);
|
|
49
|
+
return { cancel: () => clearTimeout(handle) };
|
|
50
|
+
};
|
|
51
|
+
var RelayClient = class {
|
|
52
|
+
wsUrl;
|
|
53
|
+
options;
|
|
54
|
+
scheduler;
|
|
55
|
+
random;
|
|
56
|
+
trace;
|
|
57
|
+
ws = null;
|
|
58
|
+
lastSeq = -1;
|
|
59
|
+
reconnectAttempt = 0;
|
|
60
|
+
reconnectTimer = null;
|
|
61
|
+
queue = [];
|
|
62
|
+
waiters = [];
|
|
63
|
+
/** First fatal error; once set, every consumer pull rejects with it. */
|
|
64
|
+
failure = null;
|
|
65
|
+
/** Clean end of stream (normal close or caller-initiated close()). */
|
|
66
|
+
ended = false;
|
|
67
|
+
userClosed = false;
|
|
68
|
+
started = false;
|
|
69
|
+
pendingConnect = null;
|
|
70
|
+
constructor(options) {
|
|
71
|
+
if (options.wsUrl === "") {
|
|
72
|
+
throw new ValidationError("Relay stream URL must not be empty.");
|
|
73
|
+
}
|
|
74
|
+
if (options.sessionId === "") {
|
|
75
|
+
throw new ValidationError("Relay session id must not be empty.");
|
|
76
|
+
}
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = new URL(options.wsUrl);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new ValidationError(
|
|
82
|
+
"Relay stream URL is not a valid URL.",
|
|
83
|
+
"The server returned a malformed stream endpoint \u2014 retry creating the session."
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
for (const [name, value] of parsed.searchParams.entries()) {
|
|
87
|
+
if (name === "sessionId") {
|
|
88
|
+
if (value !== options.sessionId) {
|
|
89
|
+
throw new ValidationError(
|
|
90
|
+
"Relay stream URL carries a session id that does not match the created session.",
|
|
91
|
+
"The server and client disagree on the session \u2014 retry creating the session."
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw new ValidationError(
|
|
97
|
+
"Relay stream URL must not carry query parameters other than the session id.",
|
|
98
|
+
"The client builds the connection query string itself."
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
this.options = options;
|
|
102
|
+
this.wsUrl = `${parsed.origin}${parsed.pathname}`;
|
|
103
|
+
this.scheduler = options.scheduler ?? defaultScheduler;
|
|
104
|
+
this.random = options.random ?? Math.random;
|
|
105
|
+
this.trace = options.tracer ?? (() => {
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Opens the first connection. Resolves when the socket is open;
|
|
110
|
+
* rejects with a typed error on authentication rejection or when the
|
|
111
|
+
* stream fails before ever becoming established. May only be called
|
|
112
|
+
* once per client instance.
|
|
113
|
+
*/
|
|
114
|
+
async connect() {
|
|
115
|
+
if (this.started) {
|
|
116
|
+
throw new ValidationError("connect() may only be called once.");
|
|
117
|
+
}
|
|
118
|
+
this.started = true;
|
|
119
|
+
await new Promise((resolve, reject) => {
|
|
120
|
+
this.pendingConnect = { resolve, reject };
|
|
121
|
+
void this.openSocket();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Pull-based event stream. Single-consumer: the iterator shares the
|
|
126
|
+
* client's internal queue. Ends when close() is called or the server
|
|
127
|
+
* closes cleanly; rejects with a typed error on fatal conditions.
|
|
128
|
+
*/
|
|
129
|
+
events() {
|
|
130
|
+
return {
|
|
131
|
+
[Symbol.asyncIterator]: () => ({
|
|
132
|
+
next: () => this.nextEvent(),
|
|
133
|
+
return: async () => {
|
|
134
|
+
await this.close();
|
|
135
|
+
return { done: true, value: void 0 };
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Graceful shutdown: cancels any pending reconnect, closes the
|
|
142
|
+
* socket with a normal-closure code, and completes the iterator.
|
|
143
|
+
*/
|
|
144
|
+
async close() {
|
|
145
|
+
this.userClosed = true;
|
|
146
|
+
if (this.reconnectTimer !== null) {
|
|
147
|
+
this.reconnectTimer.cancel();
|
|
148
|
+
this.reconnectTimer = null;
|
|
149
|
+
}
|
|
150
|
+
const ws = this.ws;
|
|
151
|
+
if (ws === null || ws.readyState === WebSocket.CLOSED) {
|
|
152
|
+
this.endClean();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await new Promise((resolve) => {
|
|
156
|
+
ws.once("close", () => resolve());
|
|
157
|
+
ws.close(CLOSE_CODE_NORMAL);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// ── internal state machine ────────────────────────────────────────
|
|
161
|
+
async openSocket() {
|
|
162
|
+
let token;
|
|
163
|
+
try {
|
|
164
|
+
token = await this.options.credentialStore.getValidAccessToken(
|
|
165
|
+
this.options.profileName
|
|
166
|
+
);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
this.fail(err instanceof Error ? err : new RelayConnectError(
|
|
169
|
+
"Could not obtain an access token for the relay connection."
|
|
170
|
+
));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const query = new URLSearchParams({ sessionId: this.options.sessionId });
|
|
174
|
+
if (this.lastSeq !== -1) {
|
|
175
|
+
query.set("resumeCursor", String(this.lastSeq));
|
|
176
|
+
}
|
|
177
|
+
const url = `${this.wsUrl}?${query.toString()}`;
|
|
178
|
+
this.trace(
|
|
179
|
+
`relay connect: session=${this.options.sessionId} cursor=${this.lastSeq}`
|
|
180
|
+
);
|
|
181
|
+
const ws = new WebSocket(url, {
|
|
182
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
183
|
+
});
|
|
184
|
+
this.ws = ws;
|
|
185
|
+
ws.on("open", () => {
|
|
186
|
+
this.reconnectAttempt = 0;
|
|
187
|
+
this.trace("relay connected");
|
|
188
|
+
if (this.pendingConnect !== null) {
|
|
189
|
+
this.pendingConnect.resolve();
|
|
190
|
+
this.pendingConnect = null;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
ws.on("message", (data) => {
|
|
194
|
+
this.handleMessage(String(data));
|
|
195
|
+
});
|
|
196
|
+
ws.on("error", (err) => {
|
|
197
|
+
this.trace(`relay socket error: ${err.message}`);
|
|
198
|
+
});
|
|
199
|
+
ws.on("close", (code) => {
|
|
200
|
+
this.handleClose(code);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
handleMessage(raw) {
|
|
204
|
+
let parsed;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(raw);
|
|
207
|
+
} catch {
|
|
208
|
+
this.trace("relay frame dropped: not valid JSON");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
212
|
+
this.trace("relay frame dropped: not an object");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const frame = parsed;
|
|
216
|
+
if (frame["kind"] === "control") {
|
|
217
|
+
if (frame["type"] === "resume-gap") {
|
|
218
|
+
this.trace("relay resume gap reported by server");
|
|
219
|
+
this.fail(
|
|
220
|
+
new RelayResumeGapError(
|
|
221
|
+
"The server could not replay all missed events: the resume cursor is older than the replay window."
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.trace("relay control frame ignored: unknown type");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (frame["kind"] === "webhook" && typeof frame["seq"] === "number") {
|
|
230
|
+
const seq = frame["seq"];
|
|
231
|
+
if (seq <= this.lastSeq) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.lastSeq = seq;
|
|
235
|
+
this.deliver({
|
|
236
|
+
kind: "webhook",
|
|
237
|
+
seq,
|
|
238
|
+
timestamp: typeof frame["timestamp"] === "number" ? new Date(frame["timestamp"]).toISOString() : "",
|
|
239
|
+
payload: frame["event"]
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.trace("relay frame dropped: unrecognized envelope");
|
|
244
|
+
}
|
|
245
|
+
deliver(event) {
|
|
246
|
+
const waiter = this.waiters.shift();
|
|
247
|
+
if (waiter !== void 0) {
|
|
248
|
+
waiter.resolve({ done: false, value: event });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (this.queue.length >= LOCAL_BUFFER_MAX) {
|
|
252
|
+
this.trace("relay local buffer overflow \u2014 closing connection");
|
|
253
|
+
this.fail(
|
|
254
|
+
new RelayConnectError(
|
|
255
|
+
"Local buffer overflow \u2014 consumer stalled",
|
|
256
|
+
"Slow down the consumer or reduce upstream event rate."
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.queue.push(event);
|
|
262
|
+
}
|
|
263
|
+
handleClose(code) {
|
|
264
|
+
this.ws = null;
|
|
265
|
+
if (this.failure !== null) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (this.userClosed || code === CLOSE_CODE_NORMAL) {
|
|
269
|
+
this.trace(`relay closed cleanly (code ${code})`);
|
|
270
|
+
this.endClean();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (code === CLOSE_CODE_AUTH_REJECTED) {
|
|
274
|
+
this.trace("relay rejected the connection: authentication failed");
|
|
275
|
+
this.fail(
|
|
276
|
+
new RelayConnectError(
|
|
277
|
+
"The relay rejected the connection: authentication failed (close code 4401).",
|
|
278
|
+
"Run 'ratio auth login' and retry."
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.scheduleReconnect(code);
|
|
284
|
+
}
|
|
285
|
+
scheduleReconnect(closeCode) {
|
|
286
|
+
this.reconnectAttempt += 1;
|
|
287
|
+
const base = Math.min(
|
|
288
|
+
2 ** this.reconnectAttempt * BACKOFF_BASE_MS,
|
|
289
|
+
BACKOFF_CAP_MS
|
|
290
|
+
);
|
|
291
|
+
const delay = base * (0.75 + this.random() * 0.5);
|
|
292
|
+
this.trace(
|
|
293
|
+
`relay reconnect attempt ${this.reconnectAttempt} in ${Math.round(delay)}ms (close code ${closeCode})`
|
|
294
|
+
);
|
|
295
|
+
this.reconnectTimer = this.scheduler(() => {
|
|
296
|
+
this.reconnectTimer = null;
|
|
297
|
+
void this.openSocket();
|
|
298
|
+
}, delay);
|
|
299
|
+
}
|
|
300
|
+
fail(err) {
|
|
301
|
+
if (this.failure !== null) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.failure = err;
|
|
305
|
+
if (this.reconnectTimer !== null) {
|
|
306
|
+
this.reconnectTimer.cancel();
|
|
307
|
+
this.reconnectTimer = null;
|
|
308
|
+
}
|
|
309
|
+
if (this.pendingConnect !== null) {
|
|
310
|
+
this.pendingConnect.reject(err);
|
|
311
|
+
this.pendingConnect = null;
|
|
312
|
+
}
|
|
313
|
+
let waiter = this.waiters.shift();
|
|
314
|
+
while (waiter !== void 0) {
|
|
315
|
+
waiter.reject(err);
|
|
316
|
+
waiter = this.waiters.shift();
|
|
317
|
+
}
|
|
318
|
+
const ws = this.ws;
|
|
319
|
+
if (ws !== null && ws.readyState !== WebSocket.CLOSED) {
|
|
320
|
+
ws.close(CLOSE_CODE_NORMAL);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
endClean() {
|
|
324
|
+
if (this.ended) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.ended = true;
|
|
328
|
+
if (this.pendingConnect !== null) {
|
|
329
|
+
this.pendingConnect.reject(
|
|
330
|
+
new RelayConnectError(
|
|
331
|
+
"The relay connection closed before it was established."
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
this.pendingConnect = null;
|
|
335
|
+
}
|
|
336
|
+
let waiter = this.waiters.shift();
|
|
337
|
+
while (waiter !== void 0) {
|
|
338
|
+
waiter.resolve({ done: true, value: void 0 });
|
|
339
|
+
waiter = this.waiters.shift();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
nextEvent() {
|
|
343
|
+
if (this.failure !== null) {
|
|
344
|
+
return Promise.reject(this.failure);
|
|
345
|
+
}
|
|
346
|
+
const queued = this.queue.shift();
|
|
347
|
+
if (queued !== void 0) {
|
|
348
|
+
return Promise.resolve({ done: false, value: queued });
|
|
349
|
+
}
|
|
350
|
+
if (this.ended) {
|
|
351
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
352
|
+
}
|
|
353
|
+
return new Promise((resolve, reject) => {
|
|
354
|
+
this.waiters.push({ resolve, reject });
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
export {
|
|
359
|
+
LOCAL_BUFFER_MAX,
|
|
360
|
+
RelayClient
|
|
361
|
+
};
|
|
362
|
+
//# sourceMappingURL=relay-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/relay/relay-client.ts","../../../src/lib/errors.ts"],"sourcesContent":["/**\n * WebSocket relay tunnel client.\n *\n * Responsibilities:\n * - Connect to the relay stream endpoint with `Authorization: Bearer`\n * at the HTTP handshake (the token never appears in the URL). The\n * server authenticates the upgrade by reading the `sessionId` query\n * parameter, so every connect URL carries `?sessionId=<id>`.\n * - Accept the server-issued stream URL, which may already carry the\n * canonical `?sessionId=` parameter; it must match the session the\n * client was constructed with, and no other query parameter is\n * allowed — the client owns the connect query string.\n * - Track the monotonic per-session sequence number; discard\n * duplicates delivered during a resume replay.\n * - Auto-reconnect on abnormal closes with exponential backoff\n * (capped) and multiplicative jitter, resuming from the last seen\n * sequence via the `resumeCursor` query parameter (sent alongside\n * `sessionId`).\n * - Surface a server-declared resume gap as a typed\n * RelayResumeGapError — recovery policy belongs to the caller.\n * - Deliver events through a pull-based AsyncIterable with a bounded\n * local buffer; a stalled consumer fails fast instead of growing\n * memory without limit.\n *\n * Uses the `ws` package (not the runtime's built-in WebSocket, which\n * is only available as a stable global from Node 22 — this package\n * supports Node 18+).\n */\n\nimport WebSocket from 'ws';\nimport {\n RelayConnectError,\n RelayResumeGapError,\n ValidationError,\n} from '../errors.js';\nimport type {\n RelayClientOptions,\n RelayEvent,\n RelayRandom,\n RelayScheduler,\n ResumeCursor,\n} from './types.js';\n\n/**\n * Maximum number of undelivered events held for a slow consumer.\n * Exceeding it rejects the consumer's next pull and closes the\n * connection — the client never buffers without bound.\n */\nexport const LOCAL_BUFFER_MAX = 1000;\n\n/** Base backoff unit; attempt n waits min(2^n * 100, 30_000) ms. */\nconst BACKOFF_BASE_MS = 100;\n/** Backoff ceiling: 30_000 ms (30 seconds). */\nconst BACKOFF_CAP_MS = 30_000;\n\n/** Close code the server uses to reject an unauthenticated socket. */\nconst CLOSE_CODE_AUTH_REJECTED = 4401;\n/** Normal-closure code — clean shutdown, never reconnected. */\nconst CLOSE_CODE_NORMAL = 1000;\n\nconst defaultScheduler: RelayScheduler = (fn, ms) => {\n const handle = setTimeout(fn, ms);\n return { cancel: () => clearTimeout(handle) };\n};\n\ninterface Waiter {\n resolve: (result: IteratorResult<RelayEvent>) => void;\n reject: (err: Error) => void;\n}\n\nexport class RelayClient {\n private readonly wsUrl: string;\n private readonly options: RelayClientOptions;\n private readonly scheduler: RelayScheduler;\n private readonly random: RelayRandom;\n private readonly trace: (msg: string) => void;\n\n private ws: WebSocket | null = null;\n private lastSeq: ResumeCursor = -1;\n private reconnectAttempt = 0;\n private reconnectTimer: { cancel: () => void } | null = null;\n\n private readonly queue: RelayEvent[] = [];\n private readonly waiters: Waiter[] = [];\n /** First fatal error; once set, every consumer pull rejects with it. */\n private failure: Error | null = null;\n /** Clean end of stream (normal close or caller-initiated close()). */\n private ended = false;\n private userClosed = false;\n private started = false;\n\n private pendingConnect: {\n resolve: () => void;\n reject: (err: Error) => void;\n } | null = null;\n\n constructor(options: RelayClientOptions) {\n if (options.wsUrl === '') {\n throw new ValidationError('Relay stream URL must not be empty.');\n }\n if (options.sessionId === '') {\n throw new ValidationError('Relay session id must not be empty.');\n }\n let parsed: URL;\n try {\n parsed = new URL(options.wsUrl);\n } catch {\n throw new ValidationError(\n 'Relay stream URL is not a valid URL.',\n 'The server returned a malformed stream endpoint — retry creating the session.',\n );\n }\n for (const [name, value] of parsed.searchParams.entries()) {\n if (name === 'sessionId') {\n if (value !== options.sessionId) {\n throw new ValidationError(\n 'Relay stream URL carries a session id that does not match the created session.',\n 'The server and client disagree on the session — retry creating the session.',\n );\n }\n continue;\n }\n throw new ValidationError(\n 'Relay stream URL must not carry query parameters other than the session id.',\n 'The client builds the connection query string itself.',\n );\n }\n this.options = options;\n // Canonical base: origin + path only. The connect query string\n // (sessionId, and resumeCursor on resume) is built per attempt.\n this.wsUrl = `${parsed.origin}${parsed.pathname}`;\n this.scheduler = options.scheduler ?? defaultScheduler;\n this.random = options.random ?? Math.random;\n this.trace = options.tracer ?? (() => {});\n }\n\n /**\n * Opens the first connection. Resolves when the socket is open;\n * rejects with a typed error on authentication rejection or when the\n * stream fails before ever becoming established. May only be called\n * once per client instance.\n */\n async connect(): Promise<void> {\n if (this.started) {\n throw new ValidationError('connect() may only be called once.');\n }\n this.started = true;\n await new Promise<void>((resolve, reject) => {\n this.pendingConnect = { resolve, reject };\n void this.openSocket();\n });\n }\n\n /**\n * Pull-based event stream. Single-consumer: the iterator shares the\n * client's internal queue. Ends when close() is called or the server\n * closes cleanly; rejects with a typed error on fatal conditions.\n */\n events(): AsyncIterable<RelayEvent> {\n return {\n [Symbol.asyncIterator]: () => ({\n next: () => this.nextEvent(),\n return: async (): Promise<IteratorResult<RelayEvent>> => {\n await this.close();\n return { done: true, value: undefined };\n },\n }),\n };\n }\n\n /**\n * Graceful shutdown: cancels any pending reconnect, closes the\n * socket with a normal-closure code, and completes the iterator.\n */\n async close(): Promise<void> {\n this.userClosed = true;\n if (this.reconnectTimer !== null) {\n this.reconnectTimer.cancel();\n this.reconnectTimer = null;\n }\n const ws = this.ws;\n if (ws === null || ws.readyState === WebSocket.CLOSED) {\n this.endClean();\n return;\n }\n await new Promise<void>((resolve) => {\n ws.once('close', () => resolve());\n ws.close(CLOSE_CODE_NORMAL);\n });\n }\n\n // ── internal state machine ────────────────────────────────────────\n\n private async openSocket(): Promise<void> {\n let token: string;\n try {\n token = await this.options.credentialStore.getValidAccessToken(\n this.options.profileName,\n );\n } catch (err) {\n // Credential-store failures are already typed; surface as fatal.\n this.fail(err instanceof Error ? err : new RelayConnectError(\n 'Could not obtain an access token for the relay connection.',\n ));\n return;\n }\n\n // The server authenticates the upgrade via the sessionId query\n // parameter — every connect carries it; a resume adds the cursor.\n const query = new URLSearchParams({ sessionId: this.options.sessionId });\n if (this.lastSeq !== -1) {\n query.set('resumeCursor', String(this.lastSeq));\n }\n const url = `${this.wsUrl}?${query.toString()}`;\n\n this.trace(\n `relay connect: session=${this.options.sessionId} cursor=${this.lastSeq}`,\n );\n\n const ws = new WebSocket(url, {\n headers: { Authorization: `Bearer ${token}` },\n });\n this.ws = ws;\n\n ws.on('open', () => {\n this.reconnectAttempt = 0;\n this.trace('relay connected');\n if (this.pendingConnect !== null) {\n this.pendingConnect.resolve();\n this.pendingConnect = null;\n }\n });\n ws.on('message', (data) => {\n this.handleMessage(String(data));\n });\n ws.on('error', (err) => {\n // The matching 'close' event drives the state machine; this\n // listener only prevents an unhandled-error crash.\n this.trace(`relay socket error: ${err.message}`);\n });\n ws.on('close', (code) => {\n this.handleClose(code);\n });\n }\n\n private handleMessage(raw: string): void {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n this.trace('relay frame dropped: not valid JSON');\n return;\n }\n if (typeof parsed !== 'object' || parsed === null) {\n this.trace('relay frame dropped: not an object');\n return;\n }\n const frame = parsed as Record<string, unknown>;\n\n if (frame['kind'] === 'control') {\n if (frame['type'] === 'resume-gap') {\n this.trace('relay resume gap reported by server');\n this.fail(\n new RelayResumeGapError(\n 'The server could not replay all missed events: the resume cursor is older than the replay window.',\n ),\n );\n return;\n }\n this.trace('relay control frame ignored: unknown type');\n return;\n }\n\n if (frame['kind'] === 'webhook' && typeof frame['seq'] === 'number') {\n const seq = frame['seq'];\n if (seq <= this.lastSeq) {\n // Duplicate delivered during a resume replay — discard.\n return;\n }\n this.lastSeq = seq;\n this.deliver({\n kind: 'webhook',\n seq,\n timestamp:\n typeof frame['timestamp'] === 'number'\n ? new Date(frame['timestamp']).toISOString()\n : '',\n payload: frame['event'],\n });\n return;\n }\n\n this.trace('relay frame dropped: unrecognized envelope');\n }\n\n private deliver(event: RelayEvent): void {\n const waiter = this.waiters.shift();\n if (waiter !== undefined) {\n waiter.resolve({ done: false, value: event });\n return;\n }\n if (this.queue.length >= LOCAL_BUFFER_MAX) {\n this.trace('relay local buffer overflow — closing connection');\n this.fail(\n new RelayConnectError(\n 'Local buffer overflow — consumer stalled',\n 'Slow down the consumer or reduce upstream event rate.',\n ),\n );\n return;\n }\n this.queue.push(event);\n }\n\n private handleClose(code: number): void {\n this.ws = null;\n\n if (this.failure !== null) {\n // Already failed (resume gap, overflow, auth) — nothing to do.\n return;\n }\n\n if (this.userClosed || code === CLOSE_CODE_NORMAL) {\n this.trace(`relay closed cleanly (code ${code})`);\n this.endClean();\n return;\n }\n\n if (code === CLOSE_CODE_AUTH_REJECTED) {\n this.trace('relay rejected the connection: authentication failed');\n this.fail(\n new RelayConnectError(\n 'The relay rejected the connection: authentication failed (close code 4401).',\n \"Run 'ratio auth login' and retry.\",\n ),\n );\n return;\n }\n\n this.scheduleReconnect(code);\n }\n\n private scheduleReconnect(closeCode: number): void {\n this.reconnectAttempt += 1;\n const base = Math.min(\n 2 ** this.reconnectAttempt * BACKOFF_BASE_MS,\n BACKOFF_CAP_MS,\n );\n // Multiplicative jitter in [0.75, 1.25] avoids reconnect stampedes.\n const delay = base * (0.75 + this.random() * 0.5);\n this.trace(\n `relay reconnect attempt ${this.reconnectAttempt} in ${Math.round(delay)}ms (close code ${closeCode})`,\n );\n this.reconnectTimer = this.scheduler(() => {\n this.reconnectTimer = null;\n void this.openSocket();\n }, delay);\n }\n\n private fail(err: Error): void {\n if (this.failure !== null) {\n return;\n }\n this.failure = err;\n // A failed stream must never reconnect — drop any pending timer.\n if (this.reconnectTimer !== null) {\n this.reconnectTimer.cancel();\n this.reconnectTimer = null;\n }\n if (this.pendingConnect !== null) {\n this.pendingConnect.reject(err);\n this.pendingConnect = null;\n }\n let waiter = this.waiters.shift();\n while (waiter !== undefined) {\n waiter.reject(err);\n waiter = this.waiters.shift();\n }\n const ws = this.ws;\n if (ws !== null && ws.readyState !== WebSocket.CLOSED) {\n ws.close(CLOSE_CODE_NORMAL);\n }\n }\n\n private endClean(): void {\n if (this.ended) {\n return;\n }\n this.ended = true;\n if (this.pendingConnect !== null) {\n // Closed before ever opening — the stream never established.\n this.pendingConnect.reject(\n new RelayConnectError(\n 'The relay connection closed before it was established.',\n ),\n );\n this.pendingConnect = null;\n }\n let waiter = this.waiters.shift();\n while (waiter !== undefined) {\n waiter.resolve({ done: true, value: undefined });\n waiter = this.waiters.shift();\n }\n }\n\n private nextEvent(): Promise<IteratorResult<RelayEvent>> {\n if (this.failure !== null) {\n return Promise.reject(this.failure);\n }\n const queued = this.queue.shift();\n if (queued !== undefined) {\n return Promise.resolve({ done: false, value: queued });\n }\n if (this.ended) {\n return Promise.resolve({ done: true, value: undefined });\n }\n return new Promise<IteratorResult<RelayEvent>>((resolve, reject) => {\n this.waiters.push({ resolve, reject });\n });\n }\n}\n","/**\n * Error hierarchy for the Ratio CLI. Every error thrown from a command's\n * run() is expected to be an instance of RatioCLIError so the global\n * render-error handler can map it to a deterministic stderr line and exit\n * code. Uncaught Error subclasses render as \"INTERNAL: <msg>\" with exit 1.\n *\n * No import from @oclif/core — this file is dependency-free so tests\n * can import it without oclif overhead.\n */\n\nexport class RatioCLIError extends Error {\n readonly code: string;\n readonly exitCode: number;\n readonly hint?: string;\n readonly docsUrl?: string;\n\n constructor(opts: {\n code: string;\n exitCode: number;\n message: string;\n hint?: string;\n docsUrl?: string;\n }) {\n super(opts.message);\n this.name = new.target.name;\n this.code = opts.code;\n this.exitCode = opts.exitCode;\n if (opts.hint !== undefined) this.hint = opts.hint;\n if (opts.docsUrl !== undefined) this.docsUrl = opts.docsUrl;\n }\n}\n\n/** Coming-soon stub UX. Renders on stdout with exit 0. */\nexport class ComingSoonError extends RatioCLIError {\n constructor(message: string) {\n super({ code: 'COMING_SOON', exitCode: 0, message });\n }\n}\n\n/** User has no credentials and command requires auth. */\nexport class NotLoggedInError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to sign in.\",\n ) {\n super({ code: 'NOT_LOGGED_IN', exitCode: 3, message, hint });\n }\n}\n\nexport class TokenExpiredError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to refresh your session.\",\n ) {\n super({ code: 'TOKEN_EXPIRED', exitCode: 3, message, hint });\n }\n}\n\nexport class ScopeDeniedError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'SCOPE_DENIED',\n exitCode: 4,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class NetworkError extends RatioCLIError {\n constructor(message: string, hint: string = 'Check your network and retry.') {\n super({ code: 'NETWORK_ERROR', exitCode: 5, message, hint });\n }\n}\n\nexport class RelayConnectError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Verify the tunnel URL and that the auth-server is reachable.',\n ) {\n super({ code: 'RELAY_CONNECT', exitCode: 6, message, hint });\n }\n}\n\nexport class TemplateExistsError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Choose a different name or remove the existing directory.',\n ) {\n super({ code: 'TEMPLATE_EXISTS', exitCode: 7, message, hint });\n }\n}\n\nexport class ValidationError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'VALIDATION_ERROR',\n exitCode: 8,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class RelayResumeGapError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Reconnect from the current cursor; older events are outside the buffer window.',\n ) {\n super({ code: 'RELAY_RESUME_GAP', exitCode: 9, message, hint });\n }\n}\n\nexport class OrchestratorSpawnError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Could not start your app's dev command — check that the command in ratio.config.jsonc runs from this directory.\",\n ) {\n super({ code: 'ORCHESTRATOR_SPAWN', exitCode: 10, message, hint });\n }\n}\n\nexport class OrchestratorDrainTimeoutError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Your dev command did not exit cleanly within 5 seconds — it was forcibly terminated.',\n ) {\n super({ code: 'ORCHESTRATOR_DRAIN', exitCode: 11, message, hint });\n }\n}\n\nexport function isRatioCLIError(err: unknown): err is RatioCLIError {\n return err instanceof RatioCLIError;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { CredentialStore } from '../credentials/store.js';
|
|
2
|
+
import { VerboseTracer } from '../auth/verbose-trace.js';
|
|
3
|
+
import '../credentials/clock.js';
|
|
4
|
+
import '../credentials/refresher.js';
|
|
5
|
+
import '../credentials/types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Public types for the relay tunnel client.
|
|
9
|
+
*
|
|
10
|
+
* The relay client maintains a persistent WebSocket to the platform's
|
|
11
|
+
* dev-session relay endpoint and surfaces inbound events to the caller
|
|
12
|
+
* as an AsyncIterable. All timing (reconnect backoff) and randomness
|
|
13
|
+
* (jitter) sources are injectable so tests run fully deterministic.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The last sequence number successfully received, used to resume a
|
|
18
|
+
* dropped connection without event loss. `-1` means "never received"
|
|
19
|
+
* (cold connect — no resume cursor is sent).
|
|
20
|
+
*/
|
|
21
|
+
type ResumeCursor = number;
|
|
22
|
+
/**
|
|
23
|
+
* Event surfaced to consumers, mapped from the server's wire frame
|
|
24
|
+
* (wire `event` → `payload`, epoch-ms `timestamp` → ISO string).
|
|
25
|
+
* `kind` is the envelope discriminator ("webhook" today; the envelope
|
|
26
|
+
* reserves room for future kinds without a wire break). `seq` is
|
|
27
|
+
* monotonic per session.
|
|
28
|
+
*/
|
|
29
|
+
interface RelayEvent {
|
|
30
|
+
kind: 'webhook' | 'control';
|
|
31
|
+
seq: number;
|
|
32
|
+
timestamp: string;
|
|
33
|
+
payload: unknown;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Injected timer seam. Returns a handle whose `cancel()` prevents the
|
|
37
|
+
* callback from firing. Defaults to `setTimeout`/`clearTimeout`.
|
|
38
|
+
*/
|
|
39
|
+
type RelayScheduler = (fn: () => void, ms: number) => {
|
|
40
|
+
cancel: () => void;
|
|
41
|
+
};
|
|
42
|
+
/** Injected randomness seam for backoff jitter. Defaults to `Math.random`. */
|
|
43
|
+
type RelayRandom = () => number;
|
|
44
|
+
interface RelayClientOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Dev-session identifier the stream belongs to. Sent as the
|
|
47
|
+
* `sessionId` query parameter on every connect — the server
|
|
48
|
+
* authenticates the WebSocket upgrade with it.
|
|
49
|
+
*/
|
|
50
|
+
readonly sessionId: string;
|
|
51
|
+
/**
|
|
52
|
+
* Full WSS endpoint for the session stream, e.g.
|
|
53
|
+
* "wss://relay.example.com/dev/relay". The server-issued form may
|
|
54
|
+
* carry `?sessionId=<id>`; it must match `sessionId` above. Any
|
|
55
|
+
* other query parameter is rejected — the client builds the connect
|
|
56
|
+
* query string (`sessionId`, plus `resumeCursor` on resume) itself.
|
|
57
|
+
*/
|
|
58
|
+
readonly wsUrl: string;
|
|
59
|
+
/** Credential store used to obtain a valid Bearer access token. */
|
|
60
|
+
readonly credentialStore: CredentialStore;
|
|
61
|
+
/** Optional profile forwarded to the credential store. */
|
|
62
|
+
readonly profileName?: string;
|
|
63
|
+
/** Timer seam for reconnect scheduling (tests inject a fake). */
|
|
64
|
+
readonly scheduler?: RelayScheduler;
|
|
65
|
+
/** Randomness seam for backoff jitter (tests inject a constant). */
|
|
66
|
+
readonly random?: RelayRandom;
|
|
67
|
+
/** Verbose diagnostics sink. Never receives secret material. */
|
|
68
|
+
readonly tracer?: VerboseTracer;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type { RelayClientOptions, RelayEvent, RelayRandom, RelayScheduler, ResumeCursor };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
interface RenderInput {
|
|
2
|
+
err: unknown;
|
|
3
|
+
verbose: boolean;
|
|
4
|
+
json: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface RenderOutput {
|
|
7
|
+
/** Text to write to stdout. Empty string means nothing to write. */
|
|
8
|
+
stdout: string;
|
|
9
|
+
/** Text to write to stderr. Empty string means nothing to write. */
|
|
10
|
+
stderr: string;
|
|
11
|
+
exitCode: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Pure rendering function — no side effects, no process.exit, no I/O.
|
|
15
|
+
* The caller (RatioCommand.catch) is responsible for writing output and exiting.
|
|
16
|
+
*
|
|
17
|
+
* Rendering rules (all branches):
|
|
18
|
+
*
|
|
19
|
+
* | Error shape | --json | --verbose | stdout | stderr | exit |
|
|
20
|
+
* |----------------------|--------|-----------|--------------------------------|----------------------------------|------|
|
|
21
|
+
* | ComingSoonError | false | any | err.message + '\n' | '' | 0 |
|
|
22
|
+
* | ComingSoonError | true | any | '{"ok":true,"data":null}\n' | '' | 0 |
|
|
23
|
+
* | RatioCLIError >0 | false | false | '' | code: msg [+ hint] [+ docsUrl] | N |
|
|
24
|
+
* | RatioCLIError >0 | false | true | '' | above + stack | N |
|
|
25
|
+
* | RatioCLIError >0 | true | false | JSON envelope | '' | N |
|
|
26
|
+
* | RatioCLIError >0 | true | true | JSON envelope | stack only | N |
|
|
27
|
+
* | oclif ParserError | false | any | '' | VALIDATION_ERROR: msg [+stack] | 8 |
|
|
28
|
+
* | oclif ParserError | true | any | JSON VALIDATION_ERROR envelope | stack if verbose | 8 |
|
|
29
|
+
* | Plain Error / other | false | false | '' | INTERNAL: msg\n | 1 |
|
|
30
|
+
* | Plain Error / other | false | true | '' | INTERNAL: msg\n + stack\n | 1 |
|
|
31
|
+
* | Plain Error / other | true | any | JSON INTERNAL envelope | stack if verbose | 1 |
|
|
32
|
+
*/
|
|
33
|
+
declare function renderError(input: RenderInput): RenderOutput;
|
|
34
|
+
|
|
35
|
+
export { type RenderInput, type RenderOutput, renderError };
|