@runalabs/rill-cli 0.1.11 → 0.1.12
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 +4 -0
- package/dist/cli.js +505 -481
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -288,3 +288,7 @@ Progress runs through `uploading` → `queued` → `encoding` → `processing`
|
|
|
288
288
|
Run `rill connect --email you@example.com` to authorize account access or keep your anonymous recordings. Open the displayed link, sign in, and enter the code yourself. Approval allows creation and reading of your existing and new recordings for 90 days, with automatic renewal of one-hour access tokens. Revoke the connection in Rill's Credentials page to stop access and renewal. Deletion and changes to existing sharing settings are not included.
|
|
289
289
|
|
|
290
290
|
Use `rill record list` to list accessible recordings and `rill inspect <recording-id>` for private recording details. `rill inspect <share-url>` continues to work without account access. The existing `rill login` and `RILL_TOKEN` flows remain supported. Local credential storage requires macOS Keychain or Linux Secret Service.
|
|
291
|
+
|
|
292
|
+
### Check the connected account
|
|
293
|
+
|
|
294
|
+
Run `rill whoami` (or `rill --pretty whoami`) to verify your current identity. JSON output includes status, account email when available, workspace, scopes, and access expiry. It uses RILL_TOKEN when set, otherwise the saved OS credential. Existing access can renew automatically; it never creates an identity or opens an approval flow. Missing credentials return `not_connected` and exit 1. Invalid or revoked credentials return an error.
|
package/dist/cli.js
CHANGED
|
@@ -5,9 +5,389 @@ var __export = (target, all) => {
|
|
|
5
5
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
// src/agent-session.ts
|
|
9
|
+
import { hostname, homedir } from "os";
|
|
10
|
+
import { mkdir, rm, stat } from "fs/promises";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { createHash, randomBytes } from "crypto";
|
|
13
|
+
var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
14
|
+
var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
|
|
15
|
+
var AgentAuthError = class extends Error {
|
|
16
|
+
constructor(code, message) {
|
|
17
|
+
super(`${code}: ${message}`);
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function parseAuthorization(value) {
|
|
22
|
+
if (!value?.startsWith("{")) return null;
|
|
23
|
+
const saved = JSON.parse(value);
|
|
24
|
+
if (saved.version !== 1 || typeof saved.anonymous !== "boolean") throw new Error("Saved Rill authorization is invalid. Run rill connect --email <your-email>.");
|
|
25
|
+
return saved;
|
|
26
|
+
}
|
|
27
|
+
async function request(apiUrl, path, body, form = false) {
|
|
28
|
+
const url2 = new URL(apiUrl);
|
|
29
|
+
if (url2.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)) throw new Error("Agent authentication requires HTTPS.");
|
|
30
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
redirect: "error",
|
|
33
|
+
signal: AbortSignal.timeout(15e3),
|
|
34
|
+
headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
|
|
35
|
+
body: form ? new URLSearchParams(Object.entries(body).map(([key, value2]) => [key, String(value2)])) : JSON.stringify(body)
|
|
36
|
+
});
|
|
37
|
+
const value = await response.json();
|
|
38
|
+
if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function withTokens(saved, tokens) {
|
|
42
|
+
return {
|
|
43
|
+
...saved,
|
|
44
|
+
accessToken: tokens.access_token,
|
|
45
|
+
accessExpiresAt: Date.now() + tokens.expires_in * 1e3,
|
|
46
|
+
...tokens.identity_assertion ? { identityAssertion: tokens.identity_assertion, assertionExpires: tokens.assertion_expires } : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
var save = (apiUrl, value) => saveCredential(apiUrl, JSON.stringify(value));
|
|
50
|
+
var pending = /* @__PURE__ */ new Map();
|
|
51
|
+
async function loadAgentCredential(apiUrl) {
|
|
52
|
+
if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
|
|
53
|
+
const inFlight = pending.get(apiUrl);
|
|
54
|
+
if (inFlight) return inFlight;
|
|
55
|
+
const promise2 = (async () => {
|
|
56
|
+
const raw = await loadStoredCredential(apiUrl);
|
|
57
|
+
const saved = parseAuthorization(raw);
|
|
58
|
+
if (!saved) return raw;
|
|
59
|
+
if (saved.accessToken && (saved.accessExpiresAt ?? 0) > Date.now() + 6e4) return saved.accessToken;
|
|
60
|
+
if (!saved.identityAssertion && saved.anonymous && saved.registrationToken) return null;
|
|
61
|
+
if (!saved.identityAssertion) throw new Error("Connection is awaiting approval. Run rill connect --email <your-email>.");
|
|
62
|
+
const tokens = await request(apiUrl, "/oauth2/token", { grant_type: JWT_GRANT, assertion: saved.identityAssertion }, true);
|
|
63
|
+
await save(apiUrl, withTokens(saved, tokens));
|
|
64
|
+
return tokens.access_token;
|
|
65
|
+
})();
|
|
66
|
+
pending.set(apiUrl, promise2);
|
|
67
|
+
try {
|
|
68
|
+
return await promise2;
|
|
69
|
+
} finally {
|
|
70
|
+
pending.delete(apiUrl);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function ensureAnonymousCredential(apiUrl) {
|
|
74
|
+
const root = join(homedir(), ".rill", "auth-locks");
|
|
75
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
76
|
+
const lock = join(root, createHash("sha256").update(apiUrl).digest("hex"));
|
|
77
|
+
const deadline = Date.now() + 3e4;
|
|
78
|
+
while (true) {
|
|
79
|
+
try {
|
|
80
|
+
await mkdir(lock, { mode: 448 });
|
|
81
|
+
break;
|
|
82
|
+
} catch (error51) {
|
|
83
|
+
if (error51.code !== "EEXIST") throw error51;
|
|
84
|
+
const info = await stat(lock).catch(() => null);
|
|
85
|
+
if (info && Date.now() - info.mtimeMs > 12e4) {
|
|
86
|
+
await rm(lock, { recursive: true, force: true });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return await createAnonymousCredential(apiUrl);
|
|
95
|
+
} finally {
|
|
96
|
+
await rm(lock, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function createAnonymousCredential(apiUrl) {
|
|
100
|
+
const existing = await loadAgentCredential(apiUrl);
|
|
101
|
+
if (existing) return existing;
|
|
102
|
+
const stored = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
103
|
+
const initial = stored?.registrationToken ? stored : { version: 1, anonymous: true, registrationToken: randomBytes(32).toString("hex") };
|
|
104
|
+
await save(apiUrl, initial);
|
|
105
|
+
const registered = await request(apiUrl, "/agent/identity", { type: "anonymous", analytics_enabled: process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1", registration_token: initial.registrationToken, name: `${hostname()} \xB7 ${process.platform}` });
|
|
106
|
+
const saved = { ...initial, registrationToken: void 0, identityAssertion: registered.identity_assertion, assertionExpires: registered.assertion_expires, claimToken: registered.claim_token };
|
|
107
|
+
await save(apiUrl, saved);
|
|
108
|
+
process.stderr.write("Recording anonymously: three recordings, up to five minutes each. Recordings and share links expire after seven days. Run rill connect --email <your-email> to keep them.\n");
|
|
109
|
+
return await loadAgentCredential(apiUrl);
|
|
110
|
+
}
|
|
111
|
+
async function connectAgent(apiUrl, email3) {
|
|
112
|
+
if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
|
|
113
|
+
let saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
114
|
+
let claim;
|
|
115
|
+
if (saved?.claimToken && saved.anonymous) {
|
|
116
|
+
try {
|
|
117
|
+
const result = await request(apiUrl, "/agent/identity/claim", { claim_token: saved.claimToken, email: email3 });
|
|
118
|
+
claim = result.claim_attempt;
|
|
119
|
+
} catch (error51) {
|
|
120
|
+
if (!(error51 instanceof AgentAuthError) || error51.code !== "claim_expired") throw error51;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!claim) {
|
|
124
|
+
await save(apiUrl, saved ?? { version: 1, anonymous: true });
|
|
125
|
+
const result = await request(apiUrl, "/agent/identity", { type: "service_auth", login_hint: email3, name: `${hostname()} \xB7 ${process.platform}` });
|
|
126
|
+
saved = { version: 1, anonymous: true, claimToken: result.claim_token };
|
|
127
|
+
await save(apiUrl, saved);
|
|
128
|
+
claim = result.claim;
|
|
129
|
+
}
|
|
130
|
+
process.stderr.write(`Open ${claim.verification_uri}
|
|
131
|
+
Sign in as ${email3} and enter ${claim.user_code}. Approve only the request you started.
|
|
132
|
+
`);
|
|
133
|
+
const deadline = Date.now() + claim.expires_in * 1e3;
|
|
134
|
+
let interval = claim.interval;
|
|
135
|
+
while (Date.now() < deadline) {
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
|
|
137
|
+
try {
|
|
138
|
+
const tokens = await request(apiUrl, "/oauth2/token", { grant_type: CLAIM_GRANT, claim_token: saved.claimToken }, true);
|
|
139
|
+
const connected = withTokens({ version: 1, anonymous: false }, tokens);
|
|
140
|
+
await save(apiUrl, connected);
|
|
141
|
+
return { status: "authenticated", expiresAt: connected.assertionExpires, apiUrl };
|
|
142
|
+
} catch (error51) {
|
|
143
|
+
if (error51 instanceof AgentAuthError && error51.code === "expired_token") throw new AgentAuthError("expired_token", "The approval attempt expired. Run rill connect again to request a fresh code using the same saved claim token.");
|
|
144
|
+
if (error51 instanceof AgentAuthError && error51.code === "authorization_pending") continue;
|
|
145
|
+
if (error51 instanceof AgentAuthError && error51.code === "slow_down") {
|
|
146
|
+
interval += 5;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
throw error51;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again to restart the approval attempt using the same saved claim token.");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/keychain.ts
|
|
156
|
+
import { spawn } from "child_process";
|
|
157
|
+
var service = "rill";
|
|
158
|
+
async function saveCredential(apiUrl, token) {
|
|
159
|
+
if (process.platform === "darwin") {
|
|
160
|
+
await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (process.platform === "linux") {
|
|
164
|
+
await run("secret-tool", ["store", "--label=Rill agent credential", "service", service, "api-url", apiUrl], token);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
throw new Error("OS keychain storage is not supported on this platform yet. Use RILL_TOKEN.");
|
|
168
|
+
}
|
|
169
|
+
function loadCredential(apiUrl) {
|
|
170
|
+
return loadAgentCredential(apiUrl);
|
|
171
|
+
}
|
|
172
|
+
async function loadStoredCredential(apiUrl) {
|
|
173
|
+
try {
|
|
174
|
+
if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
|
|
175
|
+
if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
|
|
176
|
+
} catch (error51) {
|
|
177
|
+
const code = error51.exitCode;
|
|
178
|
+
if (process.platform === "darwin" && code === 44) return null;
|
|
179
|
+
if (process.platform === "linux" && code === 1 && !error51.message.trim()) return null;
|
|
180
|
+
throw new Error("Cannot read the OS credential store. Unlock your keychain or enable Secret Service; your identity has not been replaced.");
|
|
181
|
+
}
|
|
182
|
+
throw new Error("Local authorization requires macOS Keychain or Linux Secret Service.");
|
|
183
|
+
}
|
|
184
|
+
function run(command, args, input) {
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
187
|
+
const stdout = [];
|
|
188
|
+
const stderr = [];
|
|
189
|
+
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
190
|
+
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
191
|
+
child.once("error", reject);
|
|
192
|
+
child.once("exit", (code) => code === 0 ? resolve(Buffer.concat(stdout).toString("utf8")) : reject(Object.assign(new Error(Buffer.concat(stderr).toString("utf8").trim()), { exitCode: code })));
|
|
193
|
+
if (input) child.stdin.end(input);
|
|
194
|
+
else child.stdin.end();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/control-plane.ts
|
|
199
|
+
import { createReadStream } from "fs";
|
|
200
|
+
import { readFile, stat as stat2 } from "fs/promises";
|
|
201
|
+
import { Upload } from "tus-js-client";
|
|
202
|
+
var ControlPlaneError = class extends Error {
|
|
203
|
+
constructor(code, message, recovery, details, httpStatus) {
|
|
204
|
+
super(`${code}: ${message}${recovery ? `: ${recovery}` : ""}`);
|
|
205
|
+
this.code = code;
|
|
206
|
+
this.recovery = recovery;
|
|
207
|
+
this.details = details;
|
|
208
|
+
this.httpStatus = httpStatus;
|
|
209
|
+
this.name = "ControlPlaneError";
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
var ControlPlaneHttpError = class extends Error {
|
|
213
|
+
constructor(httpStatus) {
|
|
214
|
+
super(`Request failed (${httpStatus}).`);
|
|
215
|
+
this.httpStatus = httpStatus;
|
|
216
|
+
this.name = "ControlPlaneHttpError";
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
var ControlPlaneClient = class {
|
|
220
|
+
constructor(apiUrl, token) {
|
|
221
|
+
this.apiUrl = apiUrl;
|
|
222
|
+
this.token = token;
|
|
223
|
+
}
|
|
224
|
+
encodingCapabilities() {
|
|
225
|
+
return this.request("/api/encoding/capabilities");
|
|
226
|
+
}
|
|
227
|
+
beginEncoding(id, manifest) {
|
|
228
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}`, { method: "POST", body: JSON.stringify(manifest) });
|
|
229
|
+
}
|
|
230
|
+
encodingStatus(id) {
|
|
231
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}`);
|
|
232
|
+
}
|
|
233
|
+
uploadEncodingPart(id, part, bytes) {
|
|
234
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}/parts/${part}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "Content-Length": String(bytes.length) }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(12e4) });
|
|
235
|
+
}
|
|
236
|
+
completeEncoding(id) {
|
|
237
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}/complete`, { method: "POST" });
|
|
238
|
+
}
|
|
239
|
+
retryEncoding(id) {
|
|
240
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}/retry`, { method: "POST" });
|
|
241
|
+
}
|
|
242
|
+
createCloudRun(input) {
|
|
243
|
+
return this.request("/api/cloud/runs", { method: "POST", body: JSON.stringify(input) });
|
|
244
|
+
}
|
|
245
|
+
cloudRun(id) {
|
|
246
|
+
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}`);
|
|
247
|
+
}
|
|
248
|
+
cancelCloudRun(id) {
|
|
249
|
+
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
|
|
250
|
+
}
|
|
251
|
+
health() {
|
|
252
|
+
return this.request("/api/health", {}, false);
|
|
253
|
+
}
|
|
254
|
+
agentSession() {
|
|
255
|
+
return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
|
|
256
|
+
}
|
|
257
|
+
reportCliCommand(input) {
|
|
258
|
+
return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
|
|
259
|
+
}
|
|
260
|
+
createRecording(input) {
|
|
261
|
+
return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
|
|
262
|
+
}
|
|
263
|
+
markRecordingStarted(recordingId, input) {
|
|
264
|
+
return this.request(`/api/recordings/${recordingId}/local-started`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
body: JSON.stringify(input)
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
markRecordingStopped(recordingId, input) {
|
|
270
|
+
return this.request(`/api/recordings/${recordingId}/local-stopped`, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
body: JSON.stringify(input)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
submitRecordingFeedback(recordingId, input) {
|
|
276
|
+
return this.request(recordingId ? `/api/recordings/${recordingId}/feedback` : "/api/feedback", {
|
|
277
|
+
method: "POST",
|
|
278
|
+
body: JSON.stringify(input)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
provisionVideo(recordingId, input) {
|
|
282
|
+
return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
|
|
283
|
+
}
|
|
284
|
+
recordingStatus(recordingId) {
|
|
285
|
+
return this.request(`/api/recordings/${recordingId}`);
|
|
286
|
+
}
|
|
287
|
+
cancelRecording(recordingId, reason = "cancelled_by_agent") {
|
|
288
|
+
return this.request(`/api/recordings/${recordingId}/cancel`, {
|
|
289
|
+
method: "POST",
|
|
290
|
+
body: JSON.stringify({ reason })
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async waitUntilPlayable(recordingId, timeoutMs = 10 * 6e4) {
|
|
294
|
+
const deadline = Date.now() + timeoutMs;
|
|
295
|
+
let intervalMs = 1e3;
|
|
296
|
+
while (Date.now() < deadline) {
|
|
297
|
+
const recording = await this.recordingStatus(recordingId);
|
|
298
|
+
if (recording.status === "ready" && recording.shareUrl) {
|
|
299
|
+
return { ...recording, shareUrl: new URL(recording.shareUrl, this.apiUrl).toString() };
|
|
300
|
+
}
|
|
301
|
+
if (recording.status === "failed") throw new Error(`processing_failed: ${recording.failure_message ?? "Cloudflare Stream could not process the recording."}`);
|
|
302
|
+
process.stderr.write(`\rprocessing ${recording.status.padEnd(12)}`);
|
|
303
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
304
|
+
intervalMs = Math.min(5e3, Math.round(intervalMs * 1.4));
|
|
305
|
+
}
|
|
306
|
+
throw new Error("processing_failed: Timed out waiting for the recording to become playable. Run `rill record status <recording-id>` to continue checking.");
|
|
307
|
+
}
|
|
308
|
+
async uploadVideo(uploadUrl, filePath) {
|
|
309
|
+
const file2 = await stat2(filePath);
|
|
310
|
+
if (uploadUrl.startsWith(this.apiUrl)) {
|
|
311
|
+
const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
|
|
312
|
+
if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
await new Promise((resolve, reject) => {
|
|
316
|
+
const upload = new Upload(createReadStream(filePath), {
|
|
317
|
+
uploadUrl,
|
|
318
|
+
uploadSize: file2.size,
|
|
319
|
+
retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
|
|
320
|
+
removeFingerprintOnSuccess: true,
|
|
321
|
+
onError: reject,
|
|
322
|
+
onSuccess: () => resolve(),
|
|
323
|
+
onProgress: (sent, total) => process.stderr.write(`\ruploading ${Math.round(sent / total * 100)}%`)
|
|
324
|
+
});
|
|
325
|
+
upload.start();
|
|
326
|
+
});
|
|
327
|
+
process.stderr.write("\n");
|
|
328
|
+
}
|
|
329
|
+
async uploadDiagnostics(recordingId, filePath, summary) {
|
|
330
|
+
const body = await readFile(filePath);
|
|
331
|
+
return this.request(`/api/recordings/${recordingId}/diagnostics`, { method: "PUT", headers: { "Content-Encoding": "gzip", "Content-Type": "application/x-ndjson", "Content-Length": String(body.length), "X-Rill-Summary": JSON.stringify(summary) }, body });
|
|
332
|
+
}
|
|
333
|
+
listRecordings() {
|
|
334
|
+
return this.request("/api/recordings");
|
|
335
|
+
}
|
|
336
|
+
createDeviceAuthorization(input) {
|
|
337
|
+
return this.request("/api/device/authorizations", { method: "POST", body: JSON.stringify(input) }, false);
|
|
338
|
+
}
|
|
339
|
+
pollDeviceAuthorization(deviceCode) {
|
|
340
|
+
return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
|
|
341
|
+
}
|
|
342
|
+
inspect(shareUrl) {
|
|
343
|
+
if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
|
|
344
|
+
const url2 = new URL(shareUrl);
|
|
345
|
+
const slug = url2.pathname.split("/").filter(Boolean).at(-1);
|
|
346
|
+
if (!slug) throw new Error("The share URL does not contain a slug.");
|
|
347
|
+
return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context`, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
348
|
+
if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
|
|
349
|
+
return response.json();
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async request(path, init = {}, authenticated = true) {
|
|
353
|
+
const headers = new Headers(init.headers);
|
|
354
|
+
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
355
|
+
if (authenticated) {
|
|
356
|
+
const token = typeof this.token === "function" ? await this.token() : this.token;
|
|
357
|
+
if (!token) throw new Error("authentication_required: Run `rill login` or set RILL_TOKEN.");
|
|
358
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
359
|
+
}
|
|
360
|
+
const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
|
|
361
|
+
const value = await response.json().catch(() => null);
|
|
362
|
+
if (!response.ok && value?.error?.code && value.error.message) {
|
|
363
|
+
throw new ControlPlaneError(value.error.code, value.error.message, value.error.recovery, value.error.details, response.status);
|
|
364
|
+
}
|
|
365
|
+
if (!response.ok) throw new ControlPlaneHttpError(response.status);
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// src/whoami.ts
|
|
371
|
+
async function whoami(apiUrl) {
|
|
372
|
+
const token = await loadCredential(apiUrl);
|
|
373
|
+
if (!token) return { schemaVersion: 1, status: "not_connected", authenticated: false, apiUrl };
|
|
374
|
+
const session = await new ControlPlaneClient(apiUrl, token).agentSession();
|
|
375
|
+
return {
|
|
376
|
+
schemaVersion: 1,
|
|
377
|
+
status: session.anonymous ? "anonymous" : "connected",
|
|
378
|
+
authenticated: true,
|
|
379
|
+
apiUrl,
|
|
380
|
+
anonymous: Boolean(session.anonymous),
|
|
381
|
+
email: session.email ?? null,
|
|
382
|
+
workspaceId: session.workspaceId ?? null,
|
|
383
|
+
scopes: session.scopes,
|
|
384
|
+
expiresAt: session.expiresAt
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
8
388
|
// src/remote-encoding.ts
|
|
9
389
|
import { open, readFile as readFile2 } from "fs/promises";
|
|
10
|
-
import { join } from "path";
|
|
390
|
+
import { join as join2 } from "path";
|
|
11
391
|
|
|
12
392
|
// ../../node_modules/zod/v4/classic/external.js
|
|
13
393
|
var external_exports = {};
|
|
@@ -142,7 +522,7 @@ __export(external_exports, {
|
|
|
142
522
|
guid: () => guid2,
|
|
143
523
|
hash: () => hash,
|
|
144
524
|
hex: () => hex2,
|
|
145
|
-
hostname: () =>
|
|
525
|
+
hostname: () => hostname3,
|
|
146
526
|
httpUrl: () => httpUrl,
|
|
147
527
|
includes: () => _includes,
|
|
148
528
|
instanceof: () => _instanceof,
|
|
@@ -1553,7 +1933,7 @@ __export(regexes_exports, {
|
|
|
1553
1933
|
extendedDuration: () => extendedDuration,
|
|
1554
1934
|
guid: () => guid,
|
|
1555
1935
|
hex: () => hex,
|
|
1556
|
-
hostname: () =>
|
|
1936
|
+
hostname: () => hostname2,
|
|
1557
1937
|
html5Email: () => html5Email,
|
|
1558
1938
|
httpProtocol: () => httpProtocol,
|
|
1559
1939
|
idnEmail: () => idnEmail,
|
|
@@ -1631,7 +2011,7 @@ var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]
|
|
|
1631
2011
|
var cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
|
|
1632
2012
|
var base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
|
|
1633
2013
|
var base64url = /^[A-Za-z0-9_-]*$/;
|
|
1634
|
-
var
|
|
2014
|
+
var hostname2 = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
|
|
1635
2015
|
var domain = /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
1636
2016
|
var httpProtocol = /^https?$/;
|
|
1637
2017
|
var e164 = /^\+[1-9]\d{6,14}$/;
|
|
@@ -12503,7 +12883,7 @@ __export(schemas_exports2, {
|
|
|
12503
12883
|
guid: () => guid2,
|
|
12504
12884
|
hash: () => hash,
|
|
12505
12885
|
hex: () => hex2,
|
|
12506
|
-
hostname: () =>
|
|
12886
|
+
hostname: () => hostname3,
|
|
12507
12887
|
httpUrl: () => httpUrl,
|
|
12508
12888
|
instanceof: () => _instanceof,
|
|
12509
12889
|
int: () => int,
|
|
@@ -13123,7 +13503,7 @@ var ZodCustomStringFormat = /* @__PURE__ */ $constructor("ZodCustomStringFormat"
|
|
|
13123
13503
|
function stringFormat(format, fnOrRegex, _params = {}) {
|
|
13124
13504
|
return _stringFormat(ZodCustomStringFormat, format, fnOrRegex, _params);
|
|
13125
13505
|
}
|
|
13126
|
-
function
|
|
13506
|
+
function hostname3(_params) {
|
|
13127
13507
|
return _stringFormat(ZodCustomStringFormat, "hostname", regexes_exports.hostname, _params);
|
|
13128
13508
|
}
|
|
13129
13509
|
function hex2(_params) {
|
|
@@ -14471,244 +14851,72 @@ function convertSchema(schema, ctx) {
|
|
|
14471
14851
|
baseSchema = baseSchema.describe(schema.description);
|
|
14472
14852
|
}
|
|
14473
14853
|
return baseSchema;
|
|
14474
|
-
}
|
|
14475
|
-
function fromJSONSchema(schema, params) {
|
|
14476
|
-
if (typeof schema === "boolean") {
|
|
14477
|
-
return schema ? z.any() : z.never();
|
|
14478
|
-
}
|
|
14479
|
-
let normalized;
|
|
14480
|
-
try {
|
|
14481
|
-
normalized = JSON.parse(JSON.stringify(schema));
|
|
14482
|
-
} catch {
|
|
14483
|
-
throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
|
|
14484
|
-
}
|
|
14485
|
-
const version2 = detectVersion(normalized, params?.defaultTarget);
|
|
14486
|
-
const defs = normalized.$defs || normalized.definitions || {};
|
|
14487
|
-
const ctx = {
|
|
14488
|
-
version: version2,
|
|
14489
|
-
defs,
|
|
14490
|
-
refs: /* @__PURE__ */ new Map(),
|
|
14491
|
-
processing: /* @__PURE__ */ new Set(),
|
|
14492
|
-
rootSchema: normalized,
|
|
14493
|
-
registry: params?.registry ?? globalRegistry
|
|
14494
|
-
};
|
|
14495
|
-
return convertSchema(normalized, ctx);
|
|
14496
|
-
}
|
|
14497
|
-
|
|
14498
|
-
// ../../node_modules/zod/v4/classic/coerce.js
|
|
14499
|
-
var coerce_exports = {};
|
|
14500
|
-
__export(coerce_exports, {
|
|
14501
|
-
bigint: () => bigint3,
|
|
14502
|
-
boolean: () => boolean3,
|
|
14503
|
-
date: () => date4,
|
|
14504
|
-
number: () => number3,
|
|
14505
|
-
string: () => string3
|
|
14506
|
-
});
|
|
14507
|
-
function string3(params) {
|
|
14508
|
-
return _coercedString(ZodString, params);
|
|
14509
|
-
}
|
|
14510
|
-
function number3(params) {
|
|
14511
|
-
return _coercedNumber(ZodNumber, params);
|
|
14512
|
-
}
|
|
14513
|
-
function boolean3(params) {
|
|
14514
|
-
return _coercedBoolean(ZodBoolean, params);
|
|
14515
|
-
}
|
|
14516
|
-
function bigint3(params) {
|
|
14517
|
-
return _coercedBigint(ZodBigInt, params);
|
|
14518
|
-
}
|
|
14519
|
-
function date4(params) {
|
|
14520
|
-
return _coercedDate(ZodDate, params);
|
|
14521
|
-
}
|
|
14522
|
-
|
|
14523
|
-
// ../../node_modules/zod/v4/classic/external.js
|
|
14524
|
-
config(en_default());
|
|
14525
|
-
|
|
14526
|
-
// ../contracts/src/encoding.ts
|
|
14527
|
-
var ENCODING_PART_BYTES = 4 * 1024 * 1024;
|
|
14528
|
-
var ENCODING_MAX_BYTES = 1024 * 1024 * 1024;
|
|
14529
|
-
var frameManifestSchema = external_exports.object({
|
|
14530
|
-
version: external_exports.literal(1),
|
|
14531
|
-
durationSeconds: external_exports.number().positive().max(600),
|
|
14532
|
-
width: external_exports.number().int().min(320).max(3840),
|
|
14533
|
-
height: external_exports.number().int().min(240).max(2160),
|
|
14534
|
-
frames: external_exports.array(external_exports.object({ timestamp: external_exports.number().nonnegative().finite(), size: external_exports.number().int().positive().max(ENCODING_PART_BYTES) }).strict()).min(1).max(36e3)
|
|
14535
|
-
}).strict().superRefine((m, ctx) => {
|
|
14536
|
-
if (m.frames.reduce((n, f) => n + f.size, 0) > ENCODING_MAX_BYTES) ctx.addIssue({ code: "custom", message: "Frame upload exceeds the trial byte limit." });
|
|
14537
|
-
for (let i = 1; i < m.frames.length; i++) if (m.frames[i].timestamp < m.frames[i - 1].timestamp) ctx.addIssue({ code: "custom", message: "Frame timestamps must be ordered." });
|
|
14538
|
-
if (m.frames.at(-1).timestamp - m.frames[0].timestamp > m.durationSeconds + 1) ctx.addIssue({ code: "custom", message: "Frame timestamps exceed capture duration." });
|
|
14539
|
-
});
|
|
14540
|
-
|
|
14541
|
-
// src/control-plane.ts
|
|
14542
|
-
import { createReadStream } from "fs";
|
|
14543
|
-
import { readFile, stat } from "fs/promises";
|
|
14544
|
-
import { Upload } from "tus-js-client";
|
|
14545
|
-
var ControlPlaneError = class extends Error {
|
|
14546
|
-
constructor(code, message, recovery, details, httpStatus) {
|
|
14547
|
-
super(`${code}: ${message}${recovery ? `: ${recovery}` : ""}`);
|
|
14548
|
-
this.code = code;
|
|
14549
|
-
this.recovery = recovery;
|
|
14550
|
-
this.details = details;
|
|
14551
|
-
this.httpStatus = httpStatus;
|
|
14552
|
-
this.name = "ControlPlaneError";
|
|
14553
|
-
}
|
|
14554
|
-
};
|
|
14555
|
-
var ControlPlaneHttpError = class extends Error {
|
|
14556
|
-
constructor(httpStatus) {
|
|
14557
|
-
super(`Request failed (${httpStatus}).`);
|
|
14558
|
-
this.httpStatus = httpStatus;
|
|
14559
|
-
this.name = "ControlPlaneHttpError";
|
|
14560
|
-
}
|
|
14561
|
-
};
|
|
14562
|
-
var ControlPlaneClient = class {
|
|
14563
|
-
constructor(apiUrl, token) {
|
|
14564
|
-
this.apiUrl = apiUrl;
|
|
14565
|
-
this.token = token;
|
|
14566
|
-
}
|
|
14567
|
-
encodingCapabilities() {
|
|
14568
|
-
return this.request("/api/encoding/capabilities");
|
|
14569
|
-
}
|
|
14570
|
-
beginEncoding(id, manifest) {
|
|
14571
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}`, { method: "POST", body: JSON.stringify(manifest) });
|
|
14572
|
-
}
|
|
14573
|
-
encodingStatus(id) {
|
|
14574
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}`);
|
|
14575
|
-
}
|
|
14576
|
-
uploadEncodingPart(id, part, bytes) {
|
|
14577
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}/parts/${part}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "Content-Length": String(bytes.length) }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(12e4) });
|
|
14578
|
-
}
|
|
14579
|
-
completeEncoding(id) {
|
|
14580
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}/complete`, { method: "POST" });
|
|
14581
|
-
}
|
|
14582
|
-
retryEncoding(id) {
|
|
14583
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}/retry`, { method: "POST" });
|
|
14584
|
-
}
|
|
14585
|
-
createCloudRun(input) {
|
|
14586
|
-
return this.request("/api/cloud/runs", { method: "POST", body: JSON.stringify(input) });
|
|
14587
|
-
}
|
|
14588
|
-
cloudRun(id) {
|
|
14589
|
-
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}`);
|
|
14590
|
-
}
|
|
14591
|
-
cancelCloudRun(id) {
|
|
14592
|
-
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
|
|
14593
|
-
}
|
|
14594
|
-
health() {
|
|
14595
|
-
return this.request("/api/health", {}, false);
|
|
14596
|
-
}
|
|
14597
|
-
agentSession() {
|
|
14598
|
-
return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
|
|
14599
|
-
}
|
|
14600
|
-
reportCliCommand(input) {
|
|
14601
|
-
return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
|
|
14602
|
-
}
|
|
14603
|
-
createRecording(input) {
|
|
14604
|
-
return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
|
|
14605
|
-
}
|
|
14606
|
-
markRecordingStarted(recordingId, input) {
|
|
14607
|
-
return this.request(`/api/recordings/${recordingId}/local-started`, {
|
|
14608
|
-
method: "POST",
|
|
14609
|
-
body: JSON.stringify(input)
|
|
14610
|
-
});
|
|
14611
|
-
}
|
|
14612
|
-
markRecordingStopped(recordingId, input) {
|
|
14613
|
-
return this.request(`/api/recordings/${recordingId}/local-stopped`, {
|
|
14614
|
-
method: "POST",
|
|
14615
|
-
body: JSON.stringify(input)
|
|
14616
|
-
});
|
|
14617
|
-
}
|
|
14618
|
-
submitRecordingFeedback(recordingId, input) {
|
|
14619
|
-
return this.request(recordingId ? `/api/recordings/${recordingId}/feedback` : "/api/feedback", {
|
|
14620
|
-
method: "POST",
|
|
14621
|
-
body: JSON.stringify(input)
|
|
14622
|
-
});
|
|
14623
|
-
}
|
|
14624
|
-
provisionVideo(recordingId, input) {
|
|
14625
|
-
return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
|
|
14626
|
-
}
|
|
14627
|
-
recordingStatus(recordingId) {
|
|
14628
|
-
return this.request(`/api/recordings/${recordingId}`);
|
|
14629
|
-
}
|
|
14630
|
-
cancelRecording(recordingId, reason = "cancelled_by_agent") {
|
|
14631
|
-
return this.request(`/api/recordings/${recordingId}/cancel`, {
|
|
14632
|
-
method: "POST",
|
|
14633
|
-
body: JSON.stringify({ reason })
|
|
14634
|
-
});
|
|
14635
|
-
}
|
|
14636
|
-
async waitUntilPlayable(recordingId, timeoutMs = 10 * 6e4) {
|
|
14637
|
-
const deadline = Date.now() + timeoutMs;
|
|
14638
|
-
let intervalMs = 1e3;
|
|
14639
|
-
while (Date.now() < deadline) {
|
|
14640
|
-
const recording = await this.recordingStatus(recordingId);
|
|
14641
|
-
if (recording.status === "ready" && recording.shareUrl) {
|
|
14642
|
-
return { ...recording, shareUrl: new URL(recording.shareUrl, this.apiUrl).toString() };
|
|
14643
|
-
}
|
|
14644
|
-
if (recording.status === "failed") throw new Error(`processing_failed: ${recording.failure_message ?? "Cloudflare Stream could not process the recording."}`);
|
|
14645
|
-
process.stderr.write(`\rprocessing ${recording.status.padEnd(12)}`);
|
|
14646
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
14647
|
-
intervalMs = Math.min(5e3, Math.round(intervalMs * 1.4));
|
|
14648
|
-
}
|
|
14649
|
-
throw new Error("processing_failed: Timed out waiting for the recording to become playable. Run `rill record status <recording-id>` to continue checking.");
|
|
14650
|
-
}
|
|
14651
|
-
async uploadVideo(uploadUrl, filePath) {
|
|
14652
|
-
const file2 = await stat(filePath);
|
|
14653
|
-
if (uploadUrl.startsWith(this.apiUrl)) {
|
|
14654
|
-
const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
|
|
14655
|
-
if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
|
|
14656
|
-
return;
|
|
14657
|
-
}
|
|
14658
|
-
await new Promise((resolve, reject) => {
|
|
14659
|
-
const upload = new Upload(createReadStream(filePath), {
|
|
14660
|
-
uploadUrl,
|
|
14661
|
-
uploadSize: file2.size,
|
|
14662
|
-
retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
|
|
14663
|
-
removeFingerprintOnSuccess: true,
|
|
14664
|
-
onError: reject,
|
|
14665
|
-
onSuccess: () => resolve(),
|
|
14666
|
-
onProgress: (sent, total) => process.stderr.write(`\ruploading ${Math.round(sent / total * 100)}%`)
|
|
14667
|
-
});
|
|
14668
|
-
upload.start();
|
|
14669
|
-
});
|
|
14670
|
-
process.stderr.write("\n");
|
|
14671
|
-
}
|
|
14672
|
-
async uploadDiagnostics(recordingId, filePath, summary) {
|
|
14673
|
-
const body = await readFile(filePath);
|
|
14674
|
-
return this.request(`/api/recordings/${recordingId}/diagnostics`, { method: "PUT", headers: { "Content-Encoding": "gzip", "Content-Type": "application/x-ndjson", "Content-Length": String(body.length), "X-Rill-Summary": JSON.stringify(summary) }, body });
|
|
14675
|
-
}
|
|
14676
|
-
listRecordings() {
|
|
14677
|
-
return this.request("/api/recordings");
|
|
14678
|
-
}
|
|
14679
|
-
createDeviceAuthorization(input) {
|
|
14680
|
-
return this.request("/api/device/authorizations", { method: "POST", body: JSON.stringify(input) }, false);
|
|
14681
|
-
}
|
|
14682
|
-
pollDeviceAuthorization(deviceCode) {
|
|
14683
|
-
return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
|
|
14684
|
-
}
|
|
14685
|
-
inspect(shareUrl) {
|
|
14686
|
-
if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
|
|
14687
|
-
const url2 = new URL(shareUrl);
|
|
14688
|
-
const slug = url2.pathname.split("/").filter(Boolean).at(-1);
|
|
14689
|
-
if (!slug) throw new Error("The share URL does not contain a slug.");
|
|
14690
|
-
return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context`, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
14691
|
-
if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
|
|
14692
|
-
return response.json();
|
|
14693
|
-
});
|
|
14694
|
-
}
|
|
14695
|
-
async request(path, init = {}, authenticated = true) {
|
|
14696
|
-
const headers = new Headers(init.headers);
|
|
14697
|
-
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
14698
|
-
if (authenticated) {
|
|
14699
|
-
const token = typeof this.token === "function" ? await this.token() : this.token;
|
|
14700
|
-
if (!token) throw new Error("authentication_required: Run `rill login` or set RILL_TOKEN.");
|
|
14701
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
14702
|
-
}
|
|
14703
|
-
const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
|
|
14704
|
-
const value = await response.json().catch(() => null);
|
|
14705
|
-
if (!response.ok && value?.error?.code && value.error.message) {
|
|
14706
|
-
throw new ControlPlaneError(value.error.code, value.error.message, value.error.recovery, value.error.details, response.status);
|
|
14707
|
-
}
|
|
14708
|
-
if (!response.ok) throw new ControlPlaneHttpError(response.status);
|
|
14709
|
-
return value;
|
|
14854
|
+
}
|
|
14855
|
+
function fromJSONSchema(schema, params) {
|
|
14856
|
+
if (typeof schema === "boolean") {
|
|
14857
|
+
return schema ? z.any() : z.never();
|
|
14710
14858
|
}
|
|
14711
|
-
|
|
14859
|
+
let normalized;
|
|
14860
|
+
try {
|
|
14861
|
+
normalized = JSON.parse(JSON.stringify(schema));
|
|
14862
|
+
} catch {
|
|
14863
|
+
throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
|
|
14864
|
+
}
|
|
14865
|
+
const version2 = detectVersion(normalized, params?.defaultTarget);
|
|
14866
|
+
const defs = normalized.$defs || normalized.definitions || {};
|
|
14867
|
+
const ctx = {
|
|
14868
|
+
version: version2,
|
|
14869
|
+
defs,
|
|
14870
|
+
refs: /* @__PURE__ */ new Map(),
|
|
14871
|
+
processing: /* @__PURE__ */ new Set(),
|
|
14872
|
+
rootSchema: normalized,
|
|
14873
|
+
registry: params?.registry ?? globalRegistry
|
|
14874
|
+
};
|
|
14875
|
+
return convertSchema(normalized, ctx);
|
|
14876
|
+
}
|
|
14877
|
+
|
|
14878
|
+
// ../../node_modules/zod/v4/classic/coerce.js
|
|
14879
|
+
var coerce_exports = {};
|
|
14880
|
+
__export(coerce_exports, {
|
|
14881
|
+
bigint: () => bigint3,
|
|
14882
|
+
boolean: () => boolean3,
|
|
14883
|
+
date: () => date4,
|
|
14884
|
+
number: () => number3,
|
|
14885
|
+
string: () => string3
|
|
14886
|
+
});
|
|
14887
|
+
function string3(params) {
|
|
14888
|
+
return _coercedString(ZodString, params);
|
|
14889
|
+
}
|
|
14890
|
+
function number3(params) {
|
|
14891
|
+
return _coercedNumber(ZodNumber, params);
|
|
14892
|
+
}
|
|
14893
|
+
function boolean3(params) {
|
|
14894
|
+
return _coercedBoolean(ZodBoolean, params);
|
|
14895
|
+
}
|
|
14896
|
+
function bigint3(params) {
|
|
14897
|
+
return _coercedBigint(ZodBigInt, params);
|
|
14898
|
+
}
|
|
14899
|
+
function date4(params) {
|
|
14900
|
+
return _coercedDate(ZodDate, params);
|
|
14901
|
+
}
|
|
14902
|
+
|
|
14903
|
+
// ../../node_modules/zod/v4/classic/external.js
|
|
14904
|
+
config(en_default());
|
|
14905
|
+
|
|
14906
|
+
// ../contracts/src/encoding.ts
|
|
14907
|
+
var ENCODING_PART_BYTES = 4 * 1024 * 1024;
|
|
14908
|
+
var ENCODING_MAX_BYTES = 1024 * 1024 * 1024;
|
|
14909
|
+
var frameManifestSchema = external_exports.object({
|
|
14910
|
+
version: external_exports.literal(1),
|
|
14911
|
+
durationSeconds: external_exports.number().positive().max(600),
|
|
14912
|
+
width: external_exports.number().int().min(320).max(3840),
|
|
14913
|
+
height: external_exports.number().int().min(240).max(2160),
|
|
14914
|
+
frames: external_exports.array(external_exports.object({ timestamp: external_exports.number().nonnegative().finite(), size: external_exports.number().int().positive().max(ENCODING_PART_BYTES) }).strict()).min(1).max(36e3)
|
|
14915
|
+
}).strict().superRefine((m, ctx) => {
|
|
14916
|
+
if (m.frames.reduce((n, f) => n + f.size, 0) > ENCODING_MAX_BYTES) ctx.addIssue({ code: "custom", message: "Frame upload exceeds the trial byte limit." });
|
|
14917
|
+
for (let i = 1; i < m.frames.length; i++) if (m.frames[i].timestamp < m.frames[i - 1].timestamp) ctx.addIssue({ code: "custom", message: "Frame timestamps must be ordered." });
|
|
14918
|
+
if (m.frames.at(-1).timestamp - m.frames[0].timestamp > m.durationSeconds + 1) ctx.addIssue({ code: "custom", message: "Frame timestamps exceed capture duration." });
|
|
14919
|
+
});
|
|
14712
14920
|
|
|
14713
14921
|
// src/remote-encoding.ts
|
|
14714
14922
|
async function withFrameUploadRetry(upload, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
@@ -14728,14 +14936,14 @@ Frame upload interrupted; retrying in ${delay / 1e3}s.
|
|
|
14728
14936
|
}
|
|
14729
14937
|
}
|
|
14730
14938
|
async function submitFrameCapture(client, id, directory) {
|
|
14731
|
-
const manifest = frameManifestSchema.parse(JSON.parse(await readFile2(
|
|
14939
|
+
const manifest = frameManifestSchema.parse(JSON.parse(await readFile2(join2(directory, "capture-manifest.json"), "utf8")));
|
|
14732
14940
|
let status = await client.beginEncoding(id, manifest);
|
|
14733
14941
|
if (status.status === "failed") status = await client.retryEncoding(id);
|
|
14734
14942
|
if (status.status !== "uploading") {
|
|
14735
14943
|
if (["expired", "cancelled"].includes(status.status)) throw new Error(`Capture ${status.status}; it cannot be retried.`);
|
|
14736
14944
|
return status;
|
|
14737
14945
|
}
|
|
14738
|
-
const file2 = await open(
|
|
14946
|
+
const file2 = await open(join2(directory, "frames.bin"), "r");
|
|
14739
14947
|
try {
|
|
14740
14948
|
const total = manifest.frames.reduce((n, f) => n + f.size, 0);
|
|
14741
14949
|
if ((await file2.stat()).size !== total) throw new Error("Capture bytes do not match the manifest.");
|
|
@@ -14777,16 +14985,16 @@ import { basename, dirname as dirname2 } from "path";
|
|
|
14777
14985
|
import { readdir as readdir2, rm as rm5, stat as stat4 } from "fs/promises";
|
|
14778
14986
|
|
|
14779
14987
|
// ../recorder/src/client.ts
|
|
14780
|
-
import { request } from "http";
|
|
14781
|
-
import { spawn } from "child_process";
|
|
14988
|
+
import { request as request2 } from "http";
|
|
14989
|
+
import { spawn as spawn2 } from "child_process";
|
|
14782
14990
|
|
|
14783
14991
|
// ../recorder/src/paths.ts
|
|
14784
|
-
import { homedir, tmpdir } from "os";
|
|
14785
|
-
import { join as
|
|
14786
|
-
var rillDirectory = () => process.env.RILL_HOME ??
|
|
14787
|
-
var daemonSocketPath = () => process.env.RILL_SOCKET ??
|
|
14788
|
-
var daemonStatePath = () =>
|
|
14789
|
-
var recordingTempRoot = () => process.env.RILL_TEMP ??
|
|
14992
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
14993
|
+
import { join as join3 } from "path";
|
|
14994
|
+
var rillDirectory = () => process.env.RILL_HOME ?? join3(homedir2(), ".rill");
|
|
14995
|
+
var daemonSocketPath = () => process.env.RILL_SOCKET ?? join3(rillDirectory(), "recorder-v2.sock");
|
|
14996
|
+
var daemonStatePath = () => join3(rillDirectory(), "sessions-v2.json");
|
|
14997
|
+
var recordingTempRoot = () => process.env.RILL_TEMP ?? join3(tmpdir(), "rill-recordings");
|
|
14790
14998
|
|
|
14791
14999
|
// ../recorder/src/client.ts
|
|
14792
15000
|
async function ensureDaemon(cliEntryPath) {
|
|
@@ -14795,7 +15003,7 @@ async function ensureDaemon(cliEntryPath) {
|
|
|
14795
15003
|
return;
|
|
14796
15004
|
} catch {
|
|
14797
15005
|
}
|
|
14798
|
-
const child =
|
|
15006
|
+
const child = spawn2(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env });
|
|
14799
15007
|
child.unref();
|
|
14800
15008
|
const deadline = Date.now() + 1e4;
|
|
14801
15009
|
while (Date.now() < deadline) {
|
|
@@ -14820,7 +15028,7 @@ function recordingStatus(recordingId) {
|
|
|
14820
15028
|
function callDaemon(method, path, body) {
|
|
14821
15029
|
return new Promise((resolve, reject) => {
|
|
14822
15030
|
const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
|
|
14823
|
-
const outgoing =
|
|
15031
|
+
const outgoing = request2({ socketPath: daemonSocketPath(), method, path, headers: payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : void 0 }, (incoming) => {
|
|
14824
15032
|
const chunks = [];
|
|
14825
15033
|
incoming.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
14826
15034
|
incoming.on("end", () => {
|
|
@@ -14840,17 +15048,17 @@ function callDaemon(method, path, body) {
|
|
|
14840
15048
|
}
|
|
14841
15049
|
|
|
14842
15050
|
// ../recorder/src/retention.ts
|
|
14843
|
-
import { readdir, readFile as readFile3, rm } from "fs/promises";
|
|
14844
|
-
import { join as
|
|
15051
|
+
import { readdir, readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
15052
|
+
import { join as join4 } from "path";
|
|
14845
15053
|
async function purgeExpiredRemoteCaptures(root, now = Date.now()) {
|
|
14846
15054
|
const removed = [];
|
|
14847
15055
|
for (const id of await readdir(root).catch(() => [])) {
|
|
14848
15056
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
14849
|
-
const directory =
|
|
15057
|
+
const directory = join4(root, id);
|
|
14850
15058
|
try {
|
|
14851
|
-
const marker = JSON.parse(await readFile3(
|
|
15059
|
+
const marker = JSON.parse(await readFile3(join4(directory, "remote-capture.json"), "utf8"));
|
|
14852
15060
|
if (typeof marker.expiresAt !== "number" || marker.expiresAt > now) continue;
|
|
14853
|
-
await
|
|
15061
|
+
await rm2(directory, { recursive: true, force: true });
|
|
14854
15062
|
removed.push(id);
|
|
14855
15063
|
} catch {
|
|
14856
15064
|
}
|
|
@@ -14861,7 +15069,7 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
14861
15069
|
for (const id of await readdir(root).catch(() => [])) {
|
|
14862
15070
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
14863
15071
|
try {
|
|
14864
|
-
const marker = JSON.parse(await readFile3(
|
|
15072
|
+
const marker = JSON.parse(await readFile3(join4(root, id, "remote-capture.json"), "utf8"));
|
|
14865
15073
|
if (typeof marker.expiresAt === "number" && Number.isFinite(marker.expiresAt)) return true;
|
|
14866
15074
|
} catch {
|
|
14867
15075
|
}
|
|
@@ -14871,8 +15079,8 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
14871
15079
|
|
|
14872
15080
|
// ../recorder/src/daemon.ts
|
|
14873
15081
|
import { createServer as createServer2 } from "http";
|
|
14874
|
-
import { mkdir as
|
|
14875
|
-
import { dirname, join as
|
|
15082
|
+
import { mkdir as mkdir5, readFile as readFile5, unlink, writeFile as writeFile4, rename, stat as stat3 } from "fs/promises";
|
|
15083
|
+
import { dirname, join as join8 } from "path";
|
|
14876
15084
|
|
|
14877
15085
|
// ../contracts/src/recording.ts
|
|
14878
15086
|
var cursorAppearanceSchema = external_exports.object({
|
|
@@ -15033,10 +15241,10 @@ async function attachCursorOverlay(page, cdp, input) {
|
|
|
15033
15241
|
|
|
15034
15242
|
// ../recorder/src/session.ts
|
|
15035
15243
|
import { chromium } from "playwright-core";
|
|
15036
|
-
import { spawn as
|
|
15244
|
+
import { spawn as spawn4 } from "child_process";
|
|
15037
15245
|
import { createServer } from "net";
|
|
15038
|
-
import { mkdir as
|
|
15039
|
-
import { join as
|
|
15246
|
+
import { mkdir as mkdir4, mkdtemp, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
15247
|
+
import { join as join7 } from "path";
|
|
15040
15248
|
import { tmpdir as tmpdir2 } from "os";
|
|
15041
15249
|
|
|
15042
15250
|
// ../recorder/src/bundle.ts
|
|
@@ -15116,9 +15324,9 @@ function redactJson(value, depth = 0) {
|
|
|
15116
15324
|
}
|
|
15117
15325
|
|
|
15118
15326
|
// ../recorder/src/encoding.ts
|
|
15119
|
-
import { open as open2, writeFile, mkdir } from "fs/promises";
|
|
15120
|
-
import { spawn as
|
|
15121
|
-
import { join as
|
|
15327
|
+
import { open as open2, writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
15328
|
+
import { spawn as spawn3 } from "child_process";
|
|
15329
|
+
import { join as join5 } from "path";
|
|
15122
15330
|
var active = 0;
|
|
15123
15331
|
var waiting = [];
|
|
15124
15332
|
function encodingConcurrency() {
|
|
@@ -15148,11 +15356,11 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15148
15356
|
lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
|
|
15149
15357
|
}
|
|
15150
15358
|
lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
|
|
15151
|
-
await writeFile(
|
|
15359
|
+
await writeFile(join5(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
|
|
15152
15360
|
const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
|
|
15153
15361
|
if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
|
|
15154
15362
|
await new Promise((resolve, reject) => {
|
|
15155
|
-
const child =
|
|
15363
|
+
const child = spawn3(process.env.RILL_FFMPEG ?? "ffmpeg", ["-hide_banner", "-loglevel", "error", "-protocol_whitelist", "file", "-f", "concat", "-safe", "1", "-i", "frames.ffconcat", "-vsync", "vfr", "-pix_fmt", "yuv420p", "-c:v", "libvpx-vp9", "-deadline", "good", "-cpu-used", "4", "-threads", String(threads), "-b:v", "0", "-crf", "32", "-y", output], { cwd: directory, stdio: ["ignore", "ignore", "pipe"] });
|
|
15156
15364
|
let stderr = "";
|
|
15157
15365
|
child.stderr.on("data", (chunk) => {
|
|
15158
15366
|
stderr = (stderr + String(chunk)).slice(-2e3);
|
|
@@ -15177,8 +15385,8 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15177
15385
|
}
|
|
15178
15386
|
|
|
15179
15387
|
// ../recorder/src/screencast.ts
|
|
15180
|
-
import { mkdir as
|
|
15181
|
-
import { join as
|
|
15388
|
+
import { mkdir as mkdir3, writeFile as writeFile2, readFile as readFile4, open as open3 } from "fs/promises";
|
|
15389
|
+
import { join as join6 } from "path";
|
|
15182
15390
|
var ScreencastRecorder = class {
|
|
15183
15391
|
constructor(session, directory, width, height) {
|
|
15184
15392
|
this.session = session;
|
|
@@ -15193,7 +15401,7 @@ var ScreencastRecorder = class {
|
|
|
15193
15401
|
frameBytes = 0;
|
|
15194
15402
|
captureError;
|
|
15195
15403
|
async start() {
|
|
15196
|
-
await
|
|
15404
|
+
await mkdir3(this.directory, { recursive: true, mode: 448 });
|
|
15197
15405
|
this.session.on("Page.screencastFrame", (payload) => {
|
|
15198
15406
|
const bytes = Buffer.from(payload.data, "base64");
|
|
15199
15407
|
if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
|
|
@@ -15202,7 +15410,7 @@ var ScreencastRecorder = class {
|
|
|
15202
15410
|
return;
|
|
15203
15411
|
}
|
|
15204
15412
|
this.frameBytes += bytes.length;
|
|
15205
|
-
const path =
|
|
15413
|
+
const path = join6(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
|
|
15206
15414
|
this.frameCounter += 1;
|
|
15207
15415
|
const write = writeFile2(path, bytes, { mode: 384 }).then(() => {
|
|
15208
15416
|
this.frames.push({ path, timestamp: payload.metadata.timestamp ?? Date.now() / 1e3 });
|
|
@@ -15226,7 +15434,7 @@ var ScreencastRecorder = class {
|
|
|
15226
15434
|
if (!this.frames.length) throw new Error("No video frames were captured.");
|
|
15227
15435
|
const ordered = this.frames.toSorted((a, b) => a.timestamp - b.timestamp);
|
|
15228
15436
|
const frames = [];
|
|
15229
|
-
const bundle = await open3(
|
|
15437
|
+
const bundle = await open3(join6(this.directory, "frames.bin"), "w", 384);
|
|
15230
15438
|
try {
|
|
15231
15439
|
for (const frame of ordered) {
|
|
15232
15440
|
const bytes = await readFile4(frame.path);
|
|
@@ -15237,7 +15445,7 @@ var ScreencastRecorder = class {
|
|
|
15237
15445
|
await bundle.close();
|
|
15238
15446
|
}
|
|
15239
15447
|
const manifest = frameManifestSchema.parse({ version: 1, durationSeconds: Math.min(600, expectedDurationSeconds), width: this.width, height: this.height, frames });
|
|
15240
|
-
await writeFile2(
|
|
15448
|
+
await writeFile2(join6(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
|
|
15241
15449
|
if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath);
|
|
15242
15450
|
return { frameCount: this.frames.length, outputPath };
|
|
15243
15451
|
}
|
|
@@ -15286,11 +15494,11 @@ var RecordingSession = class _RecordingSession {
|
|
|
15286
15494
|
static async start(options) {
|
|
15287
15495
|
const width = options.width ?? 1280;
|
|
15288
15496
|
const height = options.height ?? 720;
|
|
15289
|
-
await
|
|
15290
|
-
if (options.encoding === "remote") await writeFile3(
|
|
15497
|
+
await mkdir4(options.outputDirectory, { recursive: true, mode: 448 });
|
|
15498
|
+
if (options.encoding === "remote") await writeFile3(join7(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
|
|
15291
15499
|
const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer });
|
|
15292
15500
|
const cdpSession = await runtime.context.newCDPSession(runtime.page);
|
|
15293
|
-
const screencast = new ScreencastRecorder(cdpSession,
|
|
15501
|
+
const screencast = new ScreencastRecorder(cdpSession, join7(options.outputDirectory, "frames"), width, height);
|
|
15294
15502
|
const session = new _RecordingSession(options, { ...runtime, screencast });
|
|
15295
15503
|
try {
|
|
15296
15504
|
await session.attachDiagnostics(options.captureNetworkBodies ?? false);
|
|
@@ -15322,8 +15530,8 @@ var RecordingSession = class _RecordingSession {
|
|
|
15322
15530
|
if (this.maxTimer) clearTimeout(this.maxTimer);
|
|
15323
15531
|
const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
|
|
15324
15532
|
const outputDirectory = this.outputDirectory;
|
|
15325
|
-
const videoPath =
|
|
15326
|
-
const diagnosticsPath =
|
|
15533
|
+
const videoPath = join7(outputDirectory, "recording.webm");
|
|
15534
|
+
const diagnosticsPath = join7(outputDirectory, "diagnostics-v1.ndjson.gz");
|
|
15327
15535
|
const bundle = {
|
|
15328
15536
|
schemaVersion: 1,
|
|
15329
15537
|
capturePolicyVersion: "safe-default-v1",
|
|
@@ -15346,13 +15554,13 @@ var RecordingSession = class _RecordingSession {
|
|
|
15346
15554
|
if (this.browserProcess) {
|
|
15347
15555
|
await this.browser.close().catch(() => void 0);
|
|
15348
15556
|
await terminateBrowser(this.browserProcess);
|
|
15349
|
-
if (this.profileDirectory) await
|
|
15557
|
+
if (this.profileDirectory) await rm3(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15350
15558
|
}
|
|
15351
15559
|
}
|
|
15352
15560
|
const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
|
|
15353
15561
|
const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
|
|
15354
15562
|
const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
|
|
15355
|
-
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory:
|
|
15563
|
+
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory: join7(outputDirectory, "frames") } : {}, diagnosticsPath, durationSeconds, stoppedReason: reason, diagnostics: { consoleErrors, failedRequests, httpErrors, truncated: this.truncated, bodyCaptureEnabled: this.captureNetworkBodies } };
|
|
15356
15564
|
}
|
|
15357
15565
|
captureNetworkBodies = false;
|
|
15358
15566
|
encoding = "local";
|
|
@@ -15464,7 +15672,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15464
15672
|
};
|
|
15465
15673
|
async function launchManaged(options) {
|
|
15466
15674
|
const port = await getAvailablePort();
|
|
15467
|
-
const profileDirectory = await mkdtemp(
|
|
15675
|
+
const profileDirectory = await mkdtemp(join7(tmpdir2(), "rill-chrome-"));
|
|
15468
15676
|
const executable = findChromeExecutable();
|
|
15469
15677
|
const args = [
|
|
15470
15678
|
`--remote-debugging-port=${port}`,
|
|
@@ -15482,7 +15690,7 @@ async function launchManaged(options) {
|
|
|
15482
15690
|
"about:blank"
|
|
15483
15691
|
];
|
|
15484
15692
|
const env = options.proxyServer ? Object.fromEntries(Object.entries(process.env).filter(([key]) => ["PATH", "HOME", "TMPDIR", "DISPLAY", "LANG", "LD_LIBRARY_PATH", "XDG_RUNTIME_DIR"].includes(key))) : process.env;
|
|
15485
|
-
const browserProcess =
|
|
15693
|
+
const browserProcess = spawn4(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, env });
|
|
15486
15694
|
const cdpUrl = `http://127.0.0.1:${port}`;
|
|
15487
15695
|
try {
|
|
15488
15696
|
await waitForCdp(cdpUrl, browserProcess);
|
|
@@ -15493,7 +15701,7 @@ async function launchManaged(options) {
|
|
|
15493
15701
|
return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
|
|
15494
15702
|
} catch (error51) {
|
|
15495
15703
|
await terminateBrowser(browserProcess);
|
|
15496
|
-
await
|
|
15704
|
+
await rm3(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15497
15705
|
throw error51;
|
|
15498
15706
|
}
|
|
15499
15707
|
}
|
|
@@ -15544,8 +15752,8 @@ var sessions = /* @__PURE__ */ new Map();
|
|
|
15544
15752
|
var lastActivity = Date.now();
|
|
15545
15753
|
async function runDaemon() {
|
|
15546
15754
|
const socketPath = daemonSocketPath();
|
|
15547
|
-
await
|
|
15548
|
-
await
|
|
15755
|
+
await mkdir5(dirname(socketPath), { recursive: true, mode: 448 });
|
|
15756
|
+
await mkdir5(recordingTempRoot(), { recursive: true, mode: 448 });
|
|
15549
15757
|
await restoreRecoverableSessions();
|
|
15550
15758
|
await purgeExpiredCaptures();
|
|
15551
15759
|
await unlink(socketPath).catch((error51) => {
|
|
@@ -15579,7 +15787,7 @@ async function route(request3, response) {
|
|
|
15579
15787
|
try {
|
|
15580
15788
|
const input = await readJson(request3);
|
|
15581
15789
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
|
|
15582
|
-
const outputDirectory =
|
|
15790
|
+
const outputDirectory = join8(recordingTempRoot(), input.recordingId);
|
|
15583
15791
|
const session = await RecordingSession.start({ ...input, outputDirectory });
|
|
15584
15792
|
const status = session.status();
|
|
15585
15793
|
const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, startedAt: status.startedAt }, state: "recording" };
|
|
@@ -15657,7 +15865,7 @@ async function purgeExpiredCaptures() {
|
|
|
15657
15865
|
const removed = await purgeExpiredRemoteCaptures(recordingTempRoot());
|
|
15658
15866
|
for (const id of removed) sessions.delete(id);
|
|
15659
15867
|
let changed = removed.length > 0;
|
|
15660
|
-
for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await
|
|
15868
|
+
for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat3(entry.result.frameDirectory).catch(() => null)) {
|
|
15661
15869
|
sessions.delete(id);
|
|
15662
15870
|
changed = true;
|
|
15663
15871
|
}
|
|
@@ -15694,196 +15902,6 @@ function cliFailureCode(error51) {
|
|
|
15694
15902
|
return message.match(/^([a-z][a-z0-9_]{0,79}):/)?.[1] ?? "unexpected_error";
|
|
15695
15903
|
}
|
|
15696
15904
|
|
|
15697
|
-
// src/agent-session.ts
|
|
15698
|
-
import { hostname as hostname3, homedir as homedir2 } from "os";
|
|
15699
|
-
import { mkdir as mkdir5, rm as rm4, stat as stat3 } from "fs/promises";
|
|
15700
|
-
import { join as join8 } from "path";
|
|
15701
|
-
import { createHash, randomBytes } from "crypto";
|
|
15702
|
-
var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
15703
|
-
var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
|
|
15704
|
-
var AgentAuthError = class extends Error {
|
|
15705
|
-
constructor(code, message) {
|
|
15706
|
-
super(`${code}: ${message}`);
|
|
15707
|
-
this.code = code;
|
|
15708
|
-
}
|
|
15709
|
-
};
|
|
15710
|
-
function parseAuthorization(value) {
|
|
15711
|
-
if (!value?.startsWith("{")) return null;
|
|
15712
|
-
const saved = JSON.parse(value);
|
|
15713
|
-
if (saved.version !== 1 || typeof saved.anonymous !== "boolean") throw new Error("Saved Rill authorization is invalid. Run rill connect --email <your-email>.");
|
|
15714
|
-
return saved;
|
|
15715
|
-
}
|
|
15716
|
-
async function request2(apiUrl, path, body, form = false) {
|
|
15717
|
-
const url2 = new URL(apiUrl);
|
|
15718
|
-
if (url2.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)) throw new Error("Agent authentication requires HTTPS.");
|
|
15719
|
-
const response = await fetch(`${apiUrl}${path}`, {
|
|
15720
|
-
method: "POST",
|
|
15721
|
-
redirect: "error",
|
|
15722
|
-
signal: AbortSignal.timeout(15e3),
|
|
15723
|
-
headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
|
|
15724
|
-
body: form ? new URLSearchParams(Object.entries(body).map(([key, value2]) => [key, String(value2)])) : JSON.stringify(body)
|
|
15725
|
-
});
|
|
15726
|
-
const value = await response.json();
|
|
15727
|
-
if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
|
|
15728
|
-
return value;
|
|
15729
|
-
}
|
|
15730
|
-
function withTokens(saved, tokens) {
|
|
15731
|
-
return {
|
|
15732
|
-
...saved,
|
|
15733
|
-
accessToken: tokens.access_token,
|
|
15734
|
-
accessExpiresAt: Date.now() + tokens.expires_in * 1e3,
|
|
15735
|
-
...tokens.identity_assertion ? { identityAssertion: tokens.identity_assertion, assertionExpires: tokens.assertion_expires } : {}
|
|
15736
|
-
};
|
|
15737
|
-
}
|
|
15738
|
-
var save = (apiUrl, value) => saveCredential(apiUrl, JSON.stringify(value));
|
|
15739
|
-
var pending = /* @__PURE__ */ new Map();
|
|
15740
|
-
async function loadAgentCredential(apiUrl) {
|
|
15741
|
-
if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
|
|
15742
|
-
const inFlight = pending.get(apiUrl);
|
|
15743
|
-
if (inFlight) return inFlight;
|
|
15744
|
-
const promise2 = (async () => {
|
|
15745
|
-
const raw = await loadStoredCredential(apiUrl);
|
|
15746
|
-
const saved = parseAuthorization(raw);
|
|
15747
|
-
if (!saved) return raw;
|
|
15748
|
-
if (saved.accessToken && (saved.accessExpiresAt ?? 0) > Date.now() + 6e4) return saved.accessToken;
|
|
15749
|
-
if (!saved.identityAssertion && saved.anonymous && saved.registrationToken) return null;
|
|
15750
|
-
if (!saved.identityAssertion) throw new Error("Connection is awaiting approval. Run rill connect --email <your-email>.");
|
|
15751
|
-
const tokens = await request2(apiUrl, "/oauth2/token", { grant_type: JWT_GRANT, assertion: saved.identityAssertion }, true);
|
|
15752
|
-
await save(apiUrl, withTokens(saved, tokens));
|
|
15753
|
-
return tokens.access_token;
|
|
15754
|
-
})();
|
|
15755
|
-
pending.set(apiUrl, promise2);
|
|
15756
|
-
try {
|
|
15757
|
-
return await promise2;
|
|
15758
|
-
} finally {
|
|
15759
|
-
pending.delete(apiUrl);
|
|
15760
|
-
}
|
|
15761
|
-
}
|
|
15762
|
-
async function ensureAnonymousCredential(apiUrl) {
|
|
15763
|
-
const root = join8(homedir2(), ".rill", "auth-locks");
|
|
15764
|
-
await mkdir5(root, { recursive: true, mode: 448 });
|
|
15765
|
-
const lock = join8(root, createHash("sha256").update(apiUrl).digest("hex"));
|
|
15766
|
-
const deadline = Date.now() + 3e4;
|
|
15767
|
-
while (true) {
|
|
15768
|
-
try {
|
|
15769
|
-
await mkdir5(lock, { mode: 448 });
|
|
15770
|
-
break;
|
|
15771
|
-
} catch (error51) {
|
|
15772
|
-
if (error51.code !== "EEXIST") throw error51;
|
|
15773
|
-
const info = await stat3(lock).catch(() => null);
|
|
15774
|
-
if (info && Date.now() - info.mtimeMs > 12e4) {
|
|
15775
|
-
await rm4(lock, { recursive: true, force: true });
|
|
15776
|
-
continue;
|
|
15777
|
-
}
|
|
15778
|
-
if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
|
|
15779
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
15780
|
-
}
|
|
15781
|
-
}
|
|
15782
|
-
try {
|
|
15783
|
-
return await createAnonymousCredential(apiUrl);
|
|
15784
|
-
} finally {
|
|
15785
|
-
await rm4(lock, { recursive: true, force: true });
|
|
15786
|
-
}
|
|
15787
|
-
}
|
|
15788
|
-
async function createAnonymousCredential(apiUrl) {
|
|
15789
|
-
const existing = await loadAgentCredential(apiUrl);
|
|
15790
|
-
if (existing) return existing;
|
|
15791
|
-
const stored = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
15792
|
-
const initial = stored?.registrationToken ? stored : { version: 1, anonymous: true, registrationToken: randomBytes(32).toString("hex") };
|
|
15793
|
-
await save(apiUrl, initial);
|
|
15794
|
-
const registered = await request2(apiUrl, "/agent/identity", { type: "anonymous", analytics_enabled: process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1", registration_token: initial.registrationToken, name: `${hostname3()} \xB7 ${process.platform}` });
|
|
15795
|
-
const saved = { ...initial, registrationToken: void 0, identityAssertion: registered.identity_assertion, assertionExpires: registered.assertion_expires, claimToken: registered.claim_token };
|
|
15796
|
-
await save(apiUrl, saved);
|
|
15797
|
-
process.stderr.write("Recording anonymously: three recordings, up to five minutes each. Recordings and share links expire after seven days. Run rill connect --email <your-email> to keep them.\n");
|
|
15798
|
-
return await loadAgentCredential(apiUrl);
|
|
15799
|
-
}
|
|
15800
|
-
async function connectAgent(apiUrl, email3) {
|
|
15801
|
-
if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
|
|
15802
|
-
let saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
15803
|
-
let claim;
|
|
15804
|
-
if (saved?.claimToken && saved.anonymous) {
|
|
15805
|
-
try {
|
|
15806
|
-
const result = await request2(apiUrl, "/agent/identity/claim", { claim_token: saved.claimToken, email: email3 });
|
|
15807
|
-
claim = result.claim_attempt;
|
|
15808
|
-
} catch (error51) {
|
|
15809
|
-
if (!(error51 instanceof AgentAuthError) || error51.code !== "claim_expired") throw error51;
|
|
15810
|
-
}
|
|
15811
|
-
}
|
|
15812
|
-
if (!claim) {
|
|
15813
|
-
await save(apiUrl, saved ?? { version: 1, anonymous: true });
|
|
15814
|
-
const result = await request2(apiUrl, "/agent/identity", { type: "service_auth", login_hint: email3, name: `${hostname3()} \xB7 ${process.platform}` });
|
|
15815
|
-
saved = { version: 1, anonymous: true, claimToken: result.claim_token };
|
|
15816
|
-
await save(apiUrl, saved);
|
|
15817
|
-
claim = result.claim;
|
|
15818
|
-
}
|
|
15819
|
-
process.stderr.write(`Open ${claim.verification_uri}
|
|
15820
|
-
Sign in as ${email3} and enter ${claim.user_code}. Approve only the request you started.
|
|
15821
|
-
`);
|
|
15822
|
-
const deadline = Date.now() + claim.expires_in * 1e3;
|
|
15823
|
-
let interval = claim.interval;
|
|
15824
|
-
while (Date.now() < deadline) {
|
|
15825
|
-
await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
|
|
15826
|
-
try {
|
|
15827
|
-
const tokens = await request2(apiUrl, "/oauth2/token", { grant_type: CLAIM_GRANT, claim_token: saved.claimToken }, true);
|
|
15828
|
-
const connected = withTokens({ version: 1, anonymous: false }, tokens);
|
|
15829
|
-
await save(apiUrl, connected);
|
|
15830
|
-
return { status: "authenticated", expiresAt: connected.assertionExpires, apiUrl };
|
|
15831
|
-
} catch (error51) {
|
|
15832
|
-
if (error51 instanceof AgentAuthError && error51.code === "expired_token") throw new AgentAuthError("expired_token", "The approval attempt expired. Run rill connect again to request a fresh code using the same saved claim token.");
|
|
15833
|
-
if (error51 instanceof AgentAuthError && error51.code === "authorization_pending") continue;
|
|
15834
|
-
if (error51 instanceof AgentAuthError && error51.code === "slow_down") {
|
|
15835
|
-
interval += 5;
|
|
15836
|
-
continue;
|
|
15837
|
-
}
|
|
15838
|
-
throw error51;
|
|
15839
|
-
}
|
|
15840
|
-
}
|
|
15841
|
-
throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again to restart the approval attempt using the same saved claim token.");
|
|
15842
|
-
}
|
|
15843
|
-
|
|
15844
|
-
// src/keychain.ts
|
|
15845
|
-
import { spawn as spawn4 } from "child_process";
|
|
15846
|
-
var service = "rill";
|
|
15847
|
-
async function saveCredential(apiUrl, token) {
|
|
15848
|
-
if (process.platform === "darwin") {
|
|
15849
|
-
await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
|
|
15850
|
-
return;
|
|
15851
|
-
}
|
|
15852
|
-
if (process.platform === "linux") {
|
|
15853
|
-
await run("secret-tool", ["store", "--label=Rill agent credential", "service", service, "api-url", apiUrl], token);
|
|
15854
|
-
return;
|
|
15855
|
-
}
|
|
15856
|
-
throw new Error("OS keychain storage is not supported on this platform yet. Use RILL_TOKEN.");
|
|
15857
|
-
}
|
|
15858
|
-
function loadCredential(apiUrl) {
|
|
15859
|
-
return loadAgentCredential(apiUrl);
|
|
15860
|
-
}
|
|
15861
|
-
async function loadStoredCredential(apiUrl) {
|
|
15862
|
-
try {
|
|
15863
|
-
if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
|
|
15864
|
-
if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
|
|
15865
|
-
} catch (error51) {
|
|
15866
|
-
const code = error51.exitCode;
|
|
15867
|
-
if (process.platform === "darwin" && code === 44) return null;
|
|
15868
|
-
if (process.platform === "linux" && code === 1 && !error51.message.trim()) return null;
|
|
15869
|
-
throw new Error("Cannot read the OS credential store. Unlock your keychain or enable Secret Service; your identity has not been replaced.");
|
|
15870
|
-
}
|
|
15871
|
-
throw new Error("Local authorization requires macOS Keychain or Linux Secret Service.");
|
|
15872
|
-
}
|
|
15873
|
-
function run(command, args, input) {
|
|
15874
|
-
return new Promise((resolve, reject) => {
|
|
15875
|
-
const child = spawn4(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
15876
|
-
const stdout = [];
|
|
15877
|
-
const stderr = [];
|
|
15878
|
-
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
15879
|
-
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
15880
|
-
child.once("error", reject);
|
|
15881
|
-
child.once("exit", (code) => code === 0 ? resolve(Buffer.concat(stdout).toString("utf8")) : reject(Object.assign(new Error(Buffer.concat(stderr).toString("utf8").trim()), { exitCode: code })));
|
|
15882
|
-
if (input) child.stdin.end(input);
|
|
15883
|
-
else child.stdin.end();
|
|
15884
|
-
});
|
|
15885
|
-
}
|
|
15886
|
-
|
|
15887
15905
|
// src/output.ts
|
|
15888
15906
|
function emit(value, pretty = false) {
|
|
15889
15907
|
process.stdout.write(`${JSON.stringify(value, null, pretty ? 2 : void 0)}
|
|
@@ -16187,6 +16205,12 @@ async function authorize(quiet = false) {
|
|
|
16187
16205
|
}
|
|
16188
16206
|
throw new Error("Device authorization expired.");
|
|
16189
16207
|
}
|
|
16208
|
+
program.command("whoami").description("Show the current verified agent identity without starting sign-in.").action(async () => {
|
|
16209
|
+
const options = program.opts();
|
|
16210
|
+
const identity = await whoami(options.apiUrl);
|
|
16211
|
+
emit(identity, options.pretty);
|
|
16212
|
+
if (!identity.authenticated) process.exitCode = 1;
|
|
16213
|
+
});
|
|
16190
16214
|
program.command("connect").description("Connect this agent once, with renewable account access.").requiredOption("--email <email>", "Email of the Rill account approving access").action(async (input) => {
|
|
16191
16215
|
const options = program.opts();
|
|
16192
16216
|
emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email) }, options.pretty);
|