@relayfile/sdk 0.5.3 → 0.6.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 +62 -1
- package/dist/cloud-login.d.ts +27 -0
- package/dist/cloud-login.js +290 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/mount-harness.d.ts +52 -0
- package/dist/mount-harness.js +394 -0
- package/dist/setup-errors.d.ts +44 -0
- package/dist/setup-errors.js +79 -0
- package/dist/setup-types.d.ts +85 -0
- package/dist/setup-types.js +8 -0
- package/dist/setup.d.ts +75 -0
- package/dist/setup.js +612 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +7 -2
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { RelayFileApiError } from "./errors.js";
|
|
6
|
+
import { RelayFileClient } from "./client.js";
|
|
7
|
+
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
8
|
+
export class MountHarnessPermissionError extends Error {
|
|
9
|
+
code = "permission_denied";
|
|
10
|
+
path;
|
|
11
|
+
scopes;
|
|
12
|
+
constructor(targetPath, scopes) {
|
|
13
|
+
super(`Write denied for ${targetPath}: invited agent is read-only in the harness.`);
|
|
14
|
+
this.name = "MountHarnessPermissionError";
|
|
15
|
+
this.path = targetPath;
|
|
16
|
+
this.scopes = [...scopes];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function readMountHarnessConfig(env = process.env) {
|
|
20
|
+
const baseUrl = readRequiredEnv(env, "RELAYFILE_BASE_URL");
|
|
21
|
+
const token = readRequiredEnv(env, "RELAYFILE_TOKEN");
|
|
22
|
+
const workspaceId = readRequiredEnv(env, "RELAYFILE_WORKSPACE");
|
|
23
|
+
const localDir = readRequiredEnv(env, "RELAYFILE_LOCAL_DIR");
|
|
24
|
+
const remotePath = normalizeRemotePath(env.RELAYFILE_REMOTE_PATH ?? "/");
|
|
25
|
+
const mode = env.RELAYFILE_MOUNT_MODE === "fuse" ? "fuse" : "poll";
|
|
26
|
+
return {
|
|
27
|
+
baseUrl,
|
|
28
|
+
token,
|
|
29
|
+
workspaceId,
|
|
30
|
+
remotePath,
|
|
31
|
+
localDir: path.resolve(localDir),
|
|
32
|
+
mode,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function startMountHarness(options = {}) {
|
|
36
|
+
const harness = new RelayfileMountHarness(options);
|
|
37
|
+
await harness.start();
|
|
38
|
+
return harness;
|
|
39
|
+
}
|
|
40
|
+
class RelayfileMountHarness {
|
|
41
|
+
config;
|
|
42
|
+
localDir;
|
|
43
|
+
client;
|
|
44
|
+
pollIntervalMs;
|
|
45
|
+
scopes;
|
|
46
|
+
onEvent;
|
|
47
|
+
timer;
|
|
48
|
+
stopped = false;
|
|
49
|
+
syncInFlight;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.config = readMountHarnessConfig(options.env);
|
|
52
|
+
this.localDir = this.config.localDir;
|
|
53
|
+
this.pollIntervalMs = normalizePollIntervalMs(options.pollIntervalMs);
|
|
54
|
+
this.scopes = [...(options.scopes ?? [])];
|
|
55
|
+
this.onEvent = options.onEvent;
|
|
56
|
+
this.client =
|
|
57
|
+
options.client ??
|
|
58
|
+
new RelayFileClient({
|
|
59
|
+
baseUrl: this.config.baseUrl,
|
|
60
|
+
token: this.config.token,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async start() {
|
|
64
|
+
await mkdir(this.localDir, { recursive: true });
|
|
65
|
+
this.emit({
|
|
66
|
+
type: "started",
|
|
67
|
+
localDir: this.localDir,
|
|
68
|
+
remotePath: this.config.remotePath,
|
|
69
|
+
workspaceId: this.config.workspaceId,
|
|
70
|
+
});
|
|
71
|
+
await this.syncOnce();
|
|
72
|
+
this.scheduleNextSync();
|
|
73
|
+
}
|
|
74
|
+
async syncOnce() {
|
|
75
|
+
if (this.stopped) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!this.syncInFlight) {
|
|
79
|
+
this.syncInFlight = this.performSyncOnce().finally(() => {
|
|
80
|
+
this.syncInFlight = undefined;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
await this.syncInFlight;
|
|
84
|
+
}
|
|
85
|
+
async writeLocalFile(relativePath, content) {
|
|
86
|
+
const normalizedRelativePath = normalizeRelativePath(relativePath);
|
|
87
|
+
const scopedTargetPath = joinRemotePath(this.config.remotePath, normalizedRelativePath);
|
|
88
|
+
if (!this.scopes.includes("fs:write")) {
|
|
89
|
+
const error = new MountHarnessPermissionError(scopedTargetPath, this.scopes);
|
|
90
|
+
this.emit({
|
|
91
|
+
type: "permission.denied",
|
|
92
|
+
path: scopedTargetPath,
|
|
93
|
+
scopes: [...this.scopes],
|
|
94
|
+
});
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
const localPath = path.join(this.localDir, normalizedRelativePath);
|
|
98
|
+
await mkdir(path.dirname(localPath), { recursive: true });
|
|
99
|
+
await writeFile(localPath, content, "utf8");
|
|
100
|
+
}
|
|
101
|
+
async stop() {
|
|
102
|
+
if (this.stopped) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.stopped = true;
|
|
106
|
+
if (this.timer) {
|
|
107
|
+
clearTimeout(this.timer);
|
|
108
|
+
this.timer = undefined;
|
|
109
|
+
}
|
|
110
|
+
if (this.syncInFlight) {
|
|
111
|
+
await this.syncInFlight;
|
|
112
|
+
}
|
|
113
|
+
this.emit({
|
|
114
|
+
type: "stopped",
|
|
115
|
+
localDir: this.localDir,
|
|
116
|
+
remotePath: this.config.remotePath,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async performSyncOnce() {
|
|
120
|
+
await mkdir(this.localDir, { recursive: true });
|
|
121
|
+
const desiredFiles = new Map();
|
|
122
|
+
const desiredDirectories = new Set([""]);
|
|
123
|
+
await this.collectRemoteTree(this.config.remotePath, desiredFiles, desiredDirectories);
|
|
124
|
+
for (const relativeDir of [...desiredDirectories].sort((left, right) => left.localeCompare(right))) {
|
|
125
|
+
if (relativeDir === "") {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
await mkdir(path.join(this.localDir, relativeDir), { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
for (const [relativePath, content] of [...desiredFiles.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
131
|
+
const localPath = path.join(this.localDir, relativePath);
|
|
132
|
+
await mkdir(path.dirname(localPath), { recursive: true });
|
|
133
|
+
const currentContent = await readUtf8IfExists(localPath);
|
|
134
|
+
if (currentContent !== content) {
|
|
135
|
+
await writeFile(localPath, content, "utf8");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await pruneLocalEntries(this.localDir, desiredFiles, desiredDirectories);
|
|
139
|
+
this.emit({
|
|
140
|
+
type: "sync.completed",
|
|
141
|
+
localDir: this.localDir,
|
|
142
|
+
remotePath: this.config.remotePath,
|
|
143
|
+
files: desiredFiles.size,
|
|
144
|
+
directories: desiredDirectories.size,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async collectRemoteTree(remotePath, desiredFiles, desiredDirectories) {
|
|
148
|
+
const tree = await this.client.listTree(this.config.workspaceId, {
|
|
149
|
+
path: remotePath,
|
|
150
|
+
depth: 1,
|
|
151
|
+
});
|
|
152
|
+
for (const entry of tree.entries) {
|
|
153
|
+
const relativePath = relativeRemotePath(this.config.remotePath, entry.path);
|
|
154
|
+
if (relativePath === "") {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (entry.type === "dir") {
|
|
158
|
+
desiredDirectories.add(relativePath);
|
|
159
|
+
await this.collectRemoteTree(entry.path, desiredFiles, desiredDirectories);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
desiredDirectories.add(path.posix.dirname(relativePath) === "." ? "" : path.posix.dirname(relativePath));
|
|
163
|
+
const file = await this.client.readFile(this.config.workspaceId, entry.path);
|
|
164
|
+
desiredFiles.set(relativePath, decodeContent(file.content, file.encoding));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
scheduleNextSync() {
|
|
168
|
+
if (this.stopped) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.timer = setTimeout(() => {
|
|
172
|
+
void this.syncOnce()
|
|
173
|
+
.catch((error) => {
|
|
174
|
+
console.error(`mount-harness sync failed: ${formatErrorMessage(error)}`);
|
|
175
|
+
})
|
|
176
|
+
.finally(() => {
|
|
177
|
+
this.scheduleNextSync();
|
|
178
|
+
});
|
|
179
|
+
}, this.pollIntervalMs);
|
|
180
|
+
}
|
|
181
|
+
emit(event) {
|
|
182
|
+
this.onEvent?.(event);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function pruneLocalEntries(rootDir, desiredFiles, desiredDirectories) {
|
|
186
|
+
if (!(await exists(rootDir))) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const entries = await walkLocalTree(rootDir);
|
|
190
|
+
for (const entry of entries.sort((left, right) => right.relativePath.localeCompare(left.relativePath))) {
|
|
191
|
+
if (entry.relativePath === "") {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const absolutePath = path.join(rootDir, entry.relativePath);
|
|
195
|
+
if (entry.kind === "file") {
|
|
196
|
+
if (!desiredFiles.has(entry.relativePath)) {
|
|
197
|
+
await rm(absolutePath, { force: true });
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (!desiredDirectories.has(entry.relativePath)) {
|
|
202
|
+
await rm(absolutePath, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function walkLocalTree(rootDir, currentDir = rootDir, entries = []) {
|
|
207
|
+
const children = await readdir(currentDir, { withFileTypes: true });
|
|
208
|
+
for (const child of children) {
|
|
209
|
+
const absolutePath = path.join(currentDir, child.name);
|
|
210
|
+
const relativePath = normalizeRelativePath(path.relative(rootDir, absolutePath));
|
|
211
|
+
if (child.isDirectory()) {
|
|
212
|
+
entries.push({ relativePath, kind: "dir" });
|
|
213
|
+
await walkLocalTree(rootDir, absolutePath, entries);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
entries.push({ relativePath, kind: "file" });
|
|
217
|
+
}
|
|
218
|
+
return entries;
|
|
219
|
+
}
|
|
220
|
+
async function readUtf8IfExists(filePath) {
|
|
221
|
+
try {
|
|
222
|
+
return await readFile(filePath, "utf8");
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error.code === "ENOENT") {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function exists(targetPath) {
|
|
232
|
+
try {
|
|
233
|
+
await stat(targetPath);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (error.code === "ENOENT") {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function decodeContent(content, encoding) {
|
|
244
|
+
if (encoding === "base64") {
|
|
245
|
+
return Buffer.from(content, "base64").toString("utf8");
|
|
246
|
+
}
|
|
247
|
+
return content;
|
|
248
|
+
}
|
|
249
|
+
function joinRemotePath(rootPath, relativePath) {
|
|
250
|
+
return normalizeRemotePath(`${normalizeRemotePath(rootPath).replace(/\/$/, "")}/${relativePath}`.replace(/\/{2,}/g, "/"));
|
|
251
|
+
}
|
|
252
|
+
function relativeRemotePath(rootPath, targetPath) {
|
|
253
|
+
const normalizedRoot = normalizeRemotePath(rootPath);
|
|
254
|
+
const normalizedTarget = normalizeRemotePath(targetPath);
|
|
255
|
+
if (normalizedRoot === normalizedTarget) {
|
|
256
|
+
return "";
|
|
257
|
+
}
|
|
258
|
+
if (normalizedRoot === "/") {
|
|
259
|
+
return normalizeRelativePath(normalizedTarget.slice(1));
|
|
260
|
+
}
|
|
261
|
+
if (!normalizedTarget.startsWith(`${normalizedRoot}/`)) {
|
|
262
|
+
throw new Error(`Remote path ${normalizedTarget} is outside root ${normalizedRoot}.`);
|
|
263
|
+
}
|
|
264
|
+
return normalizeRelativePath(normalizedTarget.slice(normalizedRoot.length + 1));
|
|
265
|
+
}
|
|
266
|
+
function normalizeRemotePath(input) {
|
|
267
|
+
const trimmed = input.trim();
|
|
268
|
+
const normalized = path.posix.normalize(trimmed === "" ? "/" : trimmed);
|
|
269
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
270
|
+
}
|
|
271
|
+
function normalizeRelativePath(input) {
|
|
272
|
+
const normalized = path.posix.normalize(input.replace(/\\/g, "/"));
|
|
273
|
+
if (normalized === "." || normalized === "") {
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
return normalized.replace(/^\/+/, "");
|
|
277
|
+
}
|
|
278
|
+
function normalizePollIntervalMs(value) {
|
|
279
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
280
|
+
return DEFAULT_POLL_INTERVAL_MS;
|
|
281
|
+
}
|
|
282
|
+
return Math.floor(value);
|
|
283
|
+
}
|
|
284
|
+
function readRequiredEnv(env, key) {
|
|
285
|
+
const value = env[key];
|
|
286
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
287
|
+
throw new Error(`Missing required mount env: ${key}`);
|
|
288
|
+
}
|
|
289
|
+
return value.trim();
|
|
290
|
+
}
|
|
291
|
+
function formatErrorMessage(error) {
|
|
292
|
+
if (error instanceof Error) {
|
|
293
|
+
return error.message;
|
|
294
|
+
}
|
|
295
|
+
return String(error);
|
|
296
|
+
}
|
|
297
|
+
export async function runMountHarnessCli(argv = process.argv.slice(2), env = process.env) {
|
|
298
|
+
const options = parseCliArgs(argv);
|
|
299
|
+
const harness = await startMountHarness({
|
|
300
|
+
env,
|
|
301
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
302
|
+
scopes: options.scopes,
|
|
303
|
+
onEvent: (event) => {
|
|
304
|
+
console.log(JSON.stringify(event));
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
if (options.once) {
|
|
308
|
+
await harness.stop();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const stopAndExit = async () => {
|
|
312
|
+
await harness.stop();
|
|
313
|
+
};
|
|
314
|
+
const signalHandler = () => {
|
|
315
|
+
void stopAndExit()
|
|
316
|
+
.then(() => {
|
|
317
|
+
process.exitCode = 0;
|
|
318
|
+
})
|
|
319
|
+
.finally(() => {
|
|
320
|
+
process.removeListener("SIGINT", signalHandler);
|
|
321
|
+
process.removeListener("SIGTERM", signalHandler);
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
process.on("SIGINT", signalHandler);
|
|
325
|
+
process.on("SIGTERM", signalHandler);
|
|
326
|
+
await new Promise((resolve) => {
|
|
327
|
+
process.on("beforeExit", () => {
|
|
328
|
+
resolve();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function parseCliArgs(argv) {
|
|
333
|
+
const parsed = {
|
|
334
|
+
once: false,
|
|
335
|
+
};
|
|
336
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
337
|
+
const arg = argv[index];
|
|
338
|
+
if (arg === "--once") {
|
|
339
|
+
parsed.once = true;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (arg === "--poll-interval-ms") {
|
|
343
|
+
const rawValue = argv[index + 1];
|
|
344
|
+
if (!rawValue) {
|
|
345
|
+
throw new Error("Missing value for --poll-interval-ms");
|
|
346
|
+
}
|
|
347
|
+
parsed.pollIntervalMs = Number.parseInt(rawValue, 10);
|
|
348
|
+
index += 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (arg === "--scopes") {
|
|
352
|
+
const rawValue = argv[index + 1];
|
|
353
|
+
if (!rawValue) {
|
|
354
|
+
throw new Error("Missing value for --scopes");
|
|
355
|
+
}
|
|
356
|
+
parsed.scopes = rawValue
|
|
357
|
+
.split(",")
|
|
358
|
+
.map((scope) => scope.trim())
|
|
359
|
+
.filter(Boolean);
|
|
360
|
+
index += 1;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
364
|
+
}
|
|
365
|
+
return parsed;
|
|
366
|
+
}
|
|
367
|
+
function isDirectExecution() {
|
|
368
|
+
const entry = process.argv[1];
|
|
369
|
+
if (!entry) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
373
|
+
}
|
|
374
|
+
if (isDirectExecution()) {
|
|
375
|
+
void runMountHarnessCli().catch((error) => {
|
|
376
|
+
const status = error instanceof MountHarnessPermissionError
|
|
377
|
+
? {
|
|
378
|
+
code: error.code,
|
|
379
|
+
path: error.path,
|
|
380
|
+
scopes: error.scopes,
|
|
381
|
+
}
|
|
382
|
+
: error instanceof RelayFileApiError
|
|
383
|
+
? {
|
|
384
|
+
code: error.code,
|
|
385
|
+
status: error.status,
|
|
386
|
+
message: error.message,
|
|
387
|
+
}
|
|
388
|
+
: {
|
|
389
|
+
message: formatErrorMessage(error),
|
|
390
|
+
};
|
|
391
|
+
console.error(JSON.stringify({ type: "error", ...status }));
|
|
392
|
+
process.exitCode = 1;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { WorkspaceIntegrationProvider } from "./setup-types.js";
|
|
2
|
+
export declare class RelayfileSetupError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(message: string, code: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class CloudApiError extends RelayfileSetupError {
|
|
7
|
+
readonly httpStatus: number;
|
|
8
|
+
readonly httpBody: unknown;
|
|
9
|
+
constructor(httpStatus: number, httpBody: unknown, message?: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class MalformedCloudResponseError extends RelayfileSetupError {
|
|
12
|
+
readonly field: string;
|
|
13
|
+
readonly response: unknown;
|
|
14
|
+
constructor(field: string, response: unknown);
|
|
15
|
+
}
|
|
16
|
+
export declare class CloudTimeoutError extends RelayfileSetupError {
|
|
17
|
+
readonly operation: string;
|
|
18
|
+
readonly timeoutMs: number;
|
|
19
|
+
constructor(operation: string, timeoutMs: number);
|
|
20
|
+
}
|
|
21
|
+
export declare class IntegrationConnectionTimeoutError extends RelayfileSetupError {
|
|
22
|
+
readonly provider: WorkspaceIntegrationProvider;
|
|
23
|
+
readonly connectionId: string;
|
|
24
|
+
readonly elapsedMs: number;
|
|
25
|
+
readonly timeoutMs: number;
|
|
26
|
+
constructor(input: {
|
|
27
|
+
provider: WorkspaceIntegrationProvider;
|
|
28
|
+
connectionId: string;
|
|
29
|
+
elapsedMs: number;
|
|
30
|
+
timeoutMs: number;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export declare class CloudAbortError extends RelayfileSetupError {
|
|
34
|
+
readonly operation: string;
|
|
35
|
+
constructor(operation: string);
|
|
36
|
+
}
|
|
37
|
+
export declare class UnknownProviderError extends RelayfileSetupError {
|
|
38
|
+
readonly provider: string;
|
|
39
|
+
constructor(provider: string);
|
|
40
|
+
}
|
|
41
|
+
export declare class MissingConnectionIdError extends RelayfileSetupError {
|
|
42
|
+
readonly provider: WorkspaceIntegrationProvider;
|
|
43
|
+
constructor(provider: WorkspaceIntegrationProvider);
|
|
44
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export class RelayfileSetupError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class CloudApiError extends RelayfileSetupError {
|
|
10
|
+
httpStatus;
|
|
11
|
+
httpBody;
|
|
12
|
+
constructor(httpStatus, httpBody, message) {
|
|
13
|
+
super(message ??
|
|
14
|
+
readCloudErrorMessage(httpBody) ??
|
|
15
|
+
`Cloud API request failed with status ${httpStatus}.`, "cloud_api_error");
|
|
16
|
+
this.httpStatus = httpStatus;
|
|
17
|
+
this.httpBody = httpBody;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class MalformedCloudResponseError extends RelayfileSetupError {
|
|
21
|
+
field;
|
|
22
|
+
response;
|
|
23
|
+
constructor(field, response) {
|
|
24
|
+
super(`Cloud API response is missing required field "${field}".`, "malformed_cloud_response");
|
|
25
|
+
this.field = field;
|
|
26
|
+
this.response = response;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class CloudTimeoutError extends RelayfileSetupError {
|
|
30
|
+
operation;
|
|
31
|
+
timeoutMs;
|
|
32
|
+
constructor(operation, timeoutMs) {
|
|
33
|
+
super(`Timed out while waiting for ${operation} after ${timeoutMs}ms.`, "cloud_timeout_error");
|
|
34
|
+
this.operation = operation;
|
|
35
|
+
this.timeoutMs = timeoutMs;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class IntegrationConnectionTimeoutError extends RelayfileSetupError {
|
|
39
|
+
provider;
|
|
40
|
+
connectionId;
|
|
41
|
+
elapsedMs;
|
|
42
|
+
timeoutMs;
|
|
43
|
+
constructor(input) {
|
|
44
|
+
super(`Timed out waiting for ${input.provider} connection "${input.connectionId}" after ${input.elapsedMs}ms.`, "integration_connection_timeout");
|
|
45
|
+
this.provider = input.provider;
|
|
46
|
+
this.connectionId = input.connectionId;
|
|
47
|
+
this.elapsedMs = input.elapsedMs;
|
|
48
|
+
this.timeoutMs = input.timeoutMs;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export class CloudAbortError extends RelayfileSetupError {
|
|
52
|
+
operation;
|
|
53
|
+
constructor(operation) {
|
|
54
|
+
super(`Aborted while waiting for ${operation}.`, "cloud_abort_error");
|
|
55
|
+
this.operation = operation;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export class UnknownProviderError extends RelayfileSetupError {
|
|
59
|
+
provider;
|
|
60
|
+
constructor(provider) {
|
|
61
|
+
super(`Unknown workspace integration provider "${provider}".`, "unknown_provider");
|
|
62
|
+
this.provider = provider;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export class MissingConnectionIdError extends RelayfileSetupError {
|
|
66
|
+
provider;
|
|
67
|
+
constructor(provider) {
|
|
68
|
+
super(`No connectionId is available for provider "${provider}".`, "missing_connection_id");
|
|
69
|
+
this.provider = provider;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function readCloudErrorMessage(httpBody) {
|
|
73
|
+
if (!httpBody || typeof httpBody !== "object" || Array.isArray(httpBody)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const data = httpBody;
|
|
77
|
+
const message = data.message ?? data.error;
|
|
78
|
+
return typeof message === "string" && message.trim() !== "" ? message : undefined;
|
|
79
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { AccessTokenProvider } from "./client.js";
|
|
2
|
+
export declare const WORKSPACE_INTEGRATION_PROVIDERS: readonly ["github", "slack-sage", "slack-my-senior-dev", "slack-nightcto", "notion", "linear"];
|
|
3
|
+
export type WorkspaceIntegrationProvider = (typeof WORKSPACE_INTEGRATION_PROVIDERS)[number];
|
|
4
|
+
export interface WorkspacePermissions {
|
|
5
|
+
readonly?: string[];
|
|
6
|
+
ignored?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface RelayfileSetupRetryOptions {
|
|
9
|
+
maxRetries: number;
|
|
10
|
+
baseDelayMs: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RelayfileSetupOptions {
|
|
13
|
+
cloudApiUrl?: string;
|
|
14
|
+
accessToken?: AccessTokenProvider;
|
|
15
|
+
requestTimeoutMs?: number;
|
|
16
|
+
retry?: RelayfileSetupRetryOptions;
|
|
17
|
+
}
|
|
18
|
+
export interface CreateWorkspaceOptions {
|
|
19
|
+
name?: string;
|
|
20
|
+
permissions?: WorkspacePermissions;
|
|
21
|
+
agentName?: string;
|
|
22
|
+
scopes?: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface JoinWorkspaceOptions {
|
|
25
|
+
agentName?: string;
|
|
26
|
+
scopes?: string[];
|
|
27
|
+
permissions?: WorkspacePermissions;
|
|
28
|
+
}
|
|
29
|
+
export interface WorkspaceInfo {
|
|
30
|
+
workspaceId: string;
|
|
31
|
+
relayfileUrl: string;
|
|
32
|
+
relaycastApiKey: string;
|
|
33
|
+
relaycastBaseUrl?: string;
|
|
34
|
+
createdAt?: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
wsUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ConnectIntegrationOptions {
|
|
39
|
+
connectionId?: string;
|
|
40
|
+
allowedIntegrations?: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface ConnectIntegrationResult {
|
|
43
|
+
connectLink: string | null;
|
|
44
|
+
sessionToken: string | null;
|
|
45
|
+
expiresAt: string | null;
|
|
46
|
+
alreadyConnected: boolean;
|
|
47
|
+
connectionId: string;
|
|
48
|
+
}
|
|
49
|
+
export interface WaitForConnectionOptions {
|
|
50
|
+
connectionId?: string;
|
|
51
|
+
pollIntervalMs?: number;
|
|
52
|
+
/**
|
|
53
|
+
* @deprecated Use pollIntervalMs. This alias will be removed in a future
|
|
54
|
+
* minor release.
|
|
55
|
+
*/
|
|
56
|
+
intervalMs?: number;
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
onPoll?: (elapsed: number) => void;
|
|
60
|
+
}
|
|
61
|
+
export interface WorkspaceMountEnvOptions {
|
|
62
|
+
localDir?: string;
|
|
63
|
+
remotePath?: string;
|
|
64
|
+
mode?: "poll" | "fuse";
|
|
65
|
+
relaycastBaseUrl?: string;
|
|
66
|
+
}
|
|
67
|
+
export type WorkspaceMountEnv = Record<string, string>;
|
|
68
|
+
export interface AgentWorkspaceInviteOptions {
|
|
69
|
+
agentName?: string;
|
|
70
|
+
scopes?: string[];
|
|
71
|
+
relaycastBaseUrl?: string;
|
|
72
|
+
includeRelayfileToken?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface AgentWorkspaceInvite {
|
|
75
|
+
workspaceId: string;
|
|
76
|
+
cloudApiUrl: string;
|
|
77
|
+
relayfileUrl: string;
|
|
78
|
+
relaycastApiKey: string;
|
|
79
|
+
relaycastBaseUrl: string;
|
|
80
|
+
agentName: string;
|
|
81
|
+
scopes: string[];
|
|
82
|
+
relayfileToken?: string;
|
|
83
|
+
createdAt?: string;
|
|
84
|
+
name?: string;
|
|
85
|
+
}
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { RelayFileClient, type AccessTokenProvider } from "./client.js";
|
|
2
|
+
import { type RelayfileCloudLoginOptions, type RelayfileCloudTokenSet, type RelayfileCloudTokenSetupOptions } from "./cloud-login.js";
|
|
3
|
+
import { type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type RelayfileSetupOptions, type WaitForConnectionOptions, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspacePermissions } from "./setup-types.js";
|
|
4
|
+
export { RELAYFILE_SDK_VERSION } from "./version.js";
|
|
5
|
+
interface JoinWorkspaceResponse {
|
|
6
|
+
workspaceId?: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
relayfileUrl?: string;
|
|
9
|
+
wsUrl?: string;
|
|
10
|
+
relaycastApiKey?: string;
|
|
11
|
+
relaycastBaseUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
type ValidatedJoinWorkspaceResponse = Required<Pick<JoinWorkspaceResponse, "workspaceId" | "token" | "relayfileUrl" | "wsUrl" | "relaycastApiKey">> & Pick<JoinWorkspaceResponse, "relaycastBaseUrl">;
|
|
14
|
+
interface NormalizedJoinWorkspaceOptions {
|
|
15
|
+
agentName: string;
|
|
16
|
+
scopes: string[];
|
|
17
|
+
permissions?: WorkspacePermissions;
|
|
18
|
+
}
|
|
19
|
+
interface CloudRequestOptions {
|
|
20
|
+
operation: string;
|
|
21
|
+
method: string;
|
|
22
|
+
path: string;
|
|
23
|
+
body?: unknown;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
tokenProvider?: AccessTokenProvider;
|
|
27
|
+
}
|
|
28
|
+
interface WorkspaceHandleOptions {
|
|
29
|
+
setup: RelayfileSetup;
|
|
30
|
+
info: WorkspaceInfo;
|
|
31
|
+
token: string;
|
|
32
|
+
joinOptions: NormalizedJoinWorkspaceOptions;
|
|
33
|
+
}
|
|
34
|
+
export declare class RelayfileSetup {
|
|
35
|
+
private readonly cloudApiUrl;
|
|
36
|
+
private readonly accessToken?;
|
|
37
|
+
private readonly requestTimeoutMs;
|
|
38
|
+
private readonly retryOptions;
|
|
39
|
+
static login(options?: RelayfileCloudLoginOptions): Promise<RelayfileSetup>;
|
|
40
|
+
static fromCloudTokens(tokens: RelayfileCloudTokenSet, options?: RelayfileCloudTokenSetupOptions): RelayfileSetup;
|
|
41
|
+
constructor(options?: RelayfileSetupOptions);
|
|
42
|
+
createWorkspace(options?: CreateWorkspaceOptions): Promise<WorkspaceHandle>;
|
|
43
|
+
joinWorkspace(workspaceId: string, options?: JoinWorkspaceOptions): Promise<WorkspaceHandle>;
|
|
44
|
+
joinWorkspaceResponse(workspaceId: string, options: NormalizedJoinWorkspaceOptions): Promise<ValidatedJoinWorkspaceResponse>;
|
|
45
|
+
requestJson(options: CloudRequestOptions): Promise<unknown>;
|
|
46
|
+
getCloudApiUrl(): string;
|
|
47
|
+
}
|
|
48
|
+
export declare class WorkspaceHandle {
|
|
49
|
+
readonly info: WorkspaceInfo;
|
|
50
|
+
readonly workspaceId: string;
|
|
51
|
+
private readonly _setup;
|
|
52
|
+
private readonly _joinOptions;
|
|
53
|
+
private readonly _pendingConnections;
|
|
54
|
+
private _token;
|
|
55
|
+
private _tokenIssuedAt;
|
|
56
|
+
private _client?;
|
|
57
|
+
private _refreshPromise?;
|
|
58
|
+
constructor(options: WorkspaceHandleOptions);
|
|
59
|
+
client(): RelayFileClient;
|
|
60
|
+
connectIntegration(provider: WorkspaceIntegrationProvider, options?: ConnectIntegrationOptions): Promise<ConnectIntegrationResult>;
|
|
61
|
+
connectNotion(options?: Omit<ConnectIntegrationOptions, "allowedIntegrations">): Promise<ConnectIntegrationResult>;
|
|
62
|
+
waitForConnection(provider: WorkspaceIntegrationProvider, options?: WaitForConnectionOptions): Promise<void>;
|
|
63
|
+
waitForNotion(options?: WaitForConnectionOptions): Promise<void>;
|
|
64
|
+
isConnected(provider: WorkspaceIntegrationProvider, connectionId: string): Promise<boolean>;
|
|
65
|
+
disconnectIntegration(provider: WorkspaceIntegrationProvider, _connectionId?: string): Promise<void>;
|
|
66
|
+
getToken(): string;
|
|
67
|
+
mountEnv(options?: WorkspaceMountEnvOptions): WorkspaceMountEnv;
|
|
68
|
+
agentInvite(options?: AgentWorkspaceInviteOptions): AgentWorkspaceInvite;
|
|
69
|
+
refreshToken(): Promise<void>;
|
|
70
|
+
private performRefreshToken;
|
|
71
|
+
private getOrRefreshToken;
|
|
72
|
+
private resolveConnectionId;
|
|
73
|
+
private getConnectionStatus;
|
|
74
|
+
private resolveRelaycastBaseUrl;
|
|
75
|
+
}
|