@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
package/dist/setup.js
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { RelayFileClient } from "./client.js";
|
|
2
|
+
import { createRelayfileCloudAccessTokenProvider, runRelayfileCloudLogin } from "./cloud-login.js";
|
|
3
|
+
import { CloudAbortError, CloudApiError, CloudTimeoutError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, UnknownProviderError } from "./setup-errors.js";
|
|
4
|
+
import { WORKSPACE_INTEGRATION_PROVIDERS } from "./setup-types.js";
|
|
5
|
+
import { RELAYFILE_SDK_VERSION } from "./version.js";
|
|
6
|
+
export { RELAYFILE_SDK_VERSION } from "./version.js";
|
|
7
|
+
const DEFAULT_CLOUD_API_URL = "https://agentrelay.com/cloud";
|
|
8
|
+
const DEFAULT_RELAYCAST_BASE_URL = "https://api.relaycast.dev";
|
|
9
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
10
|
+
const DEFAULT_RETRY_BASE_DELAY_MS = 500;
|
|
11
|
+
const DEFAULT_RETRY_MAX_DELAY_MS = 5_000;
|
|
12
|
+
const DEFAULT_RETRY_MAX_RETRIES = 3;
|
|
13
|
+
const DEFAULT_AGENT_NAME = "sdk-agent";
|
|
14
|
+
const DEFAULT_SCOPES = ["fs:read", "fs:write"];
|
|
15
|
+
const DEFAULT_WAIT_INTERVAL_MS = 2_000;
|
|
16
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 300_000;
|
|
17
|
+
const TOKEN_REFRESH_AGE_MS = 55 * 60 * 1000;
|
|
18
|
+
export class RelayfileSetup {
|
|
19
|
+
cloudApiUrl;
|
|
20
|
+
accessToken;
|
|
21
|
+
requestTimeoutMs;
|
|
22
|
+
retryOptions;
|
|
23
|
+
static async login(options = {}) {
|
|
24
|
+
const cloudApiUrl = options.cloudApiUrl ?? DEFAULT_CLOUD_API_URL;
|
|
25
|
+
const tokens = await runRelayfileCloudLogin({
|
|
26
|
+
...options,
|
|
27
|
+
cloudApiUrl
|
|
28
|
+
});
|
|
29
|
+
await options.onTokens?.({ ...tokens });
|
|
30
|
+
return RelayfileSetup.fromCloudTokens(tokens, {
|
|
31
|
+
...options,
|
|
32
|
+
cloudApiUrl: tokens.apiUrl ?? cloudApiUrl
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
static fromCloudTokens(tokens, options = {}) {
|
|
36
|
+
const cloudApiUrl = options.cloudApiUrl ?? tokens.apiUrl ?? DEFAULT_CLOUD_API_URL;
|
|
37
|
+
return new RelayfileSetup({
|
|
38
|
+
...options,
|
|
39
|
+
cloudApiUrl,
|
|
40
|
+
accessToken: createRelayfileCloudAccessTokenProvider({
|
|
41
|
+
...tokens,
|
|
42
|
+
apiUrl: tokens.apiUrl ?? cloudApiUrl
|
|
43
|
+
}, {
|
|
44
|
+
...options,
|
|
45
|
+
cloudApiUrl
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.cloudApiUrl = options.cloudApiUrl ?? DEFAULT_CLOUD_API_URL;
|
|
51
|
+
this.accessToken = options.accessToken;
|
|
52
|
+
this.requestTimeoutMs = Math.max(1, Math.floor(options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS));
|
|
53
|
+
this.retryOptions = normalizeRetryOptions(options.retry);
|
|
54
|
+
}
|
|
55
|
+
async createWorkspace(options = {}) {
|
|
56
|
+
const createResponse = validateCreateWorkspaceResponse(await this.requestJson({
|
|
57
|
+
operation: "createWorkspace",
|
|
58
|
+
method: "POST",
|
|
59
|
+
path: "api/v1/workspaces",
|
|
60
|
+
body: compactObject({
|
|
61
|
+
name: options.name,
|
|
62
|
+
permissions: clonePermissions(options.permissions)
|
|
63
|
+
})
|
|
64
|
+
}));
|
|
65
|
+
const joinOptions = normalizeJoinWorkspaceOptions({
|
|
66
|
+
agentName: options.agentName,
|
|
67
|
+
scopes: options.scopes,
|
|
68
|
+
permissions: options.permissions
|
|
69
|
+
});
|
|
70
|
+
const joinResponse = await this.joinWorkspaceResponse(createResponse.workspaceId, joinOptions);
|
|
71
|
+
return new WorkspaceHandle({
|
|
72
|
+
setup: this,
|
|
73
|
+
info: {
|
|
74
|
+
workspaceId: createResponse.workspaceId,
|
|
75
|
+
relayfileUrl: joinResponse.relayfileUrl,
|
|
76
|
+
relaycastApiKey: joinResponse.relaycastApiKey,
|
|
77
|
+
relaycastBaseUrl: joinResponse.relaycastBaseUrl,
|
|
78
|
+
createdAt: createResponse.createdAt,
|
|
79
|
+
name: createResponse.name,
|
|
80
|
+
wsUrl: joinResponse.wsUrl
|
|
81
|
+
},
|
|
82
|
+
token: joinResponse.token,
|
|
83
|
+
joinOptions
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async joinWorkspace(workspaceId, options = {}) {
|
|
87
|
+
const joinOptions = normalizeJoinWorkspaceOptions(options);
|
|
88
|
+
const joinResponse = await this.joinWorkspaceResponse(workspaceId, joinOptions);
|
|
89
|
+
return new WorkspaceHandle({
|
|
90
|
+
setup: this,
|
|
91
|
+
info: {
|
|
92
|
+
workspaceId: joinResponse.workspaceId,
|
|
93
|
+
relayfileUrl: joinResponse.relayfileUrl,
|
|
94
|
+
relaycastApiKey: joinResponse.relaycastApiKey,
|
|
95
|
+
relaycastBaseUrl: joinResponse.relaycastBaseUrl,
|
|
96
|
+
wsUrl: joinResponse.wsUrl
|
|
97
|
+
},
|
|
98
|
+
token: joinResponse.token,
|
|
99
|
+
joinOptions
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async joinWorkspaceResponse(workspaceId, options) {
|
|
103
|
+
return validateJoinWorkspaceResponse(await this.requestJson({
|
|
104
|
+
operation: "joinWorkspace",
|
|
105
|
+
method: "POST",
|
|
106
|
+
path: `api/v1/workspaces/${encodeURIComponent(workspaceId)}/join`,
|
|
107
|
+
body: compactObject({
|
|
108
|
+
agentName: options.agentName,
|
|
109
|
+
scopes: [...options.scopes],
|
|
110
|
+
permissions: clonePermissions(options.permissions)
|
|
111
|
+
})
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
async requestJson(options) {
|
|
115
|
+
const url = buildCloudUrl(this.cloudApiUrl, options.path);
|
|
116
|
+
const body = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
117
|
+
let retries = 0;
|
|
118
|
+
for (;;) {
|
|
119
|
+
const token = await resolveToken(options.tokenProvider ?? this.accessToken);
|
|
120
|
+
const headers = {
|
|
121
|
+
"X-Relayfile-SDK-Version": RELAYFILE_SDK_VERSION
|
|
122
|
+
};
|
|
123
|
+
if (token) {
|
|
124
|
+
headers.Authorization = `Bearer ${token}`;
|
|
125
|
+
}
|
|
126
|
+
if (body !== undefined) {
|
|
127
|
+
headers["Content-Type"] = "application/json";
|
|
128
|
+
}
|
|
129
|
+
let response;
|
|
130
|
+
try {
|
|
131
|
+
response = await fetchWithTimeout(url, {
|
|
132
|
+
method: options.method,
|
|
133
|
+
headers,
|
|
134
|
+
body,
|
|
135
|
+
signal: options.signal
|
|
136
|
+
}, options.timeoutMs ?? this.requestTimeoutMs, options.operation);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof CloudAbortError ||
|
|
140
|
+
error instanceof CloudTimeoutError) {
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (!shouldRetryError(error, retries, this.retryOptions.maxRetries, options.signal)) {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
retries += 1;
|
|
147
|
+
await sleep(computeRetryDelayMs(this.retryOptions, retries, null), options.signal, options.operation);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const payload = await readResponseBody(response);
|
|
151
|
+
if (response.ok) {
|
|
152
|
+
return payload;
|
|
153
|
+
}
|
|
154
|
+
if (shouldRetryStatus(response.status, retries, this.retryOptions.maxRetries, options.signal)) {
|
|
155
|
+
retries += 1;
|
|
156
|
+
await sleep(computeRetryDelayMs(this.retryOptions, retries, response.headers.get("retry-after")), options.signal, options.operation);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
throw new CloudApiError(response.status, payload);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
getCloudApiUrl() {
|
|
163
|
+
return this.cloudApiUrl;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export class WorkspaceHandle {
|
|
167
|
+
info;
|
|
168
|
+
workspaceId;
|
|
169
|
+
_setup;
|
|
170
|
+
_joinOptions;
|
|
171
|
+
_pendingConnections = new Map();
|
|
172
|
+
_token;
|
|
173
|
+
_tokenIssuedAt;
|
|
174
|
+
_client;
|
|
175
|
+
_refreshPromise;
|
|
176
|
+
constructor(options) {
|
|
177
|
+
this.info = options.info;
|
|
178
|
+
this.workspaceId = options.info.workspaceId;
|
|
179
|
+
this._setup = options.setup;
|
|
180
|
+
this._joinOptions = {
|
|
181
|
+
agentName: options.joinOptions.agentName,
|
|
182
|
+
scopes: [...options.joinOptions.scopes],
|
|
183
|
+
permissions: clonePermissions(options.joinOptions.permissions)
|
|
184
|
+
};
|
|
185
|
+
this._token = options.token;
|
|
186
|
+
this._tokenIssuedAt = Date.now();
|
|
187
|
+
}
|
|
188
|
+
client() {
|
|
189
|
+
if (!this._client) {
|
|
190
|
+
this._client = new RelayFileClient({
|
|
191
|
+
baseUrl: this.info.relayfileUrl,
|
|
192
|
+
token: async () => this.getOrRefreshToken()
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return this._client;
|
|
196
|
+
}
|
|
197
|
+
async connectIntegration(provider, options = {}) {
|
|
198
|
+
assertProvider(provider);
|
|
199
|
+
const requestedConnectionId = normalizeConnectionId(options.connectionId);
|
|
200
|
+
if (requestedConnectionId) {
|
|
201
|
+
const alreadyConnected = await this.isConnected(provider, requestedConnectionId);
|
|
202
|
+
if (alreadyConnected) {
|
|
203
|
+
this._pendingConnections.set(provider, requestedConnectionId);
|
|
204
|
+
return {
|
|
205
|
+
alreadyConnected: true,
|
|
206
|
+
connectLink: null,
|
|
207
|
+
sessionToken: null,
|
|
208
|
+
expiresAt: null,
|
|
209
|
+
connectionId: requestedConnectionId
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const response = validateConnectSessionResponse(await this._setup.requestJson({
|
|
214
|
+
operation: "connectIntegration",
|
|
215
|
+
method: "POST",
|
|
216
|
+
path: `api/v1/workspaces/${encodeURIComponent(this.workspaceId)}/integrations/connect-session`,
|
|
217
|
+
body: {
|
|
218
|
+
allowedIntegrations: options.allowedIntegrations && options.allowedIntegrations.length > 0
|
|
219
|
+
? [...options.allowedIntegrations]
|
|
220
|
+
: [provider]
|
|
221
|
+
},
|
|
222
|
+
tokenProvider: async () => this.getOrRefreshToken()
|
|
223
|
+
}));
|
|
224
|
+
const connectionId = normalizeConnectionId(response.connectionId) ?? this.workspaceId;
|
|
225
|
+
this._pendingConnections.set(provider, connectionId);
|
|
226
|
+
return {
|
|
227
|
+
alreadyConnected: false,
|
|
228
|
+
connectLink: response.connectLink,
|
|
229
|
+
sessionToken: response.token,
|
|
230
|
+
expiresAt: response.expiresAt,
|
|
231
|
+
connectionId
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async connectNotion(options = {}) {
|
|
235
|
+
return this.connectIntegration("notion", {
|
|
236
|
+
...options,
|
|
237
|
+
allowedIntegrations: ["notion"]
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async waitForConnection(provider, options = {}) {
|
|
241
|
+
assertProvider(provider);
|
|
242
|
+
const connectionId = this.resolveConnectionId(provider, options.connectionId);
|
|
243
|
+
const pollIntervalMs = Math.max(0, Math.floor(options.pollIntervalMs ?? options.intervalMs ?? DEFAULT_WAIT_INTERVAL_MS));
|
|
244
|
+
const timeoutMs = Math.max(1, Math.floor(options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
|
|
245
|
+
const startedAt = Date.now();
|
|
246
|
+
for (;;) {
|
|
247
|
+
throwIfAborted(options.signal, "waitForConnection");
|
|
248
|
+
const elapsedMs = Date.now() - startedAt;
|
|
249
|
+
options.onPoll?.(elapsedMs);
|
|
250
|
+
if (elapsedMs >= timeoutMs) {
|
|
251
|
+
throw new IntegrationConnectionTimeoutError({
|
|
252
|
+
provider,
|
|
253
|
+
connectionId,
|
|
254
|
+
elapsedMs,
|
|
255
|
+
timeoutMs
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const remainingMs = timeoutMs - elapsedMs;
|
|
259
|
+
let ready;
|
|
260
|
+
try {
|
|
261
|
+
ready = await this.getConnectionStatus(provider, connectionId, {
|
|
262
|
+
signal: options.signal,
|
|
263
|
+
timeoutMs: remainingMs
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (error instanceof CloudTimeoutError) {
|
|
268
|
+
throw new IntegrationConnectionTimeoutError({
|
|
269
|
+
provider,
|
|
270
|
+
connectionId,
|
|
271
|
+
elapsedMs: Date.now() - startedAt,
|
|
272
|
+
timeoutMs
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
if (ready) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const sleepMs = Math.min(pollIntervalMs, Math.max(0, timeoutMs - (Date.now() - startedAt)));
|
|
281
|
+
await sleep(sleepMs, options.signal, "waitForConnection");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async waitForNotion(options = {}) {
|
|
285
|
+
return this.waitForConnection("notion", options);
|
|
286
|
+
}
|
|
287
|
+
async isConnected(provider, connectionId) {
|
|
288
|
+
assertProvider(provider);
|
|
289
|
+
return this.getConnectionStatus(provider, this.resolveConnectionId(provider, connectionId));
|
|
290
|
+
}
|
|
291
|
+
async disconnectIntegration(provider, _connectionId) {
|
|
292
|
+
assertProvider(provider);
|
|
293
|
+
await this._setup.requestJson({
|
|
294
|
+
operation: "disconnectIntegration",
|
|
295
|
+
method: "DELETE",
|
|
296
|
+
path: `api/v1/workspaces/${encodeURIComponent(this.workspaceId)}/integrations/${encodeURIComponent(provider)}/status`,
|
|
297
|
+
tokenProvider: async () => this.getOrRefreshToken()
|
|
298
|
+
});
|
|
299
|
+
this._pendingConnections.delete(provider);
|
|
300
|
+
}
|
|
301
|
+
getToken() {
|
|
302
|
+
return this._token;
|
|
303
|
+
}
|
|
304
|
+
mountEnv(options = {}) {
|
|
305
|
+
const relaycastBaseUrl = this.resolveRelaycastBaseUrl(options.relaycastBaseUrl);
|
|
306
|
+
return compactStringRecord({
|
|
307
|
+
RELAYFILE_BASE_URL: this.info.relayfileUrl,
|
|
308
|
+
RELAYFILE_TOKEN: this.getToken(),
|
|
309
|
+
RELAYFILE_WORKSPACE: this.workspaceId,
|
|
310
|
+
RELAYFILE_REMOTE_PATH: options.remotePath ?? "/",
|
|
311
|
+
RELAYFILE_LOCAL_DIR: options.localDir,
|
|
312
|
+
RELAYFILE_MOUNT_MODE: options.mode,
|
|
313
|
+
RELAYCAST_API_KEY: this.info.relaycastApiKey,
|
|
314
|
+
RELAY_API_KEY: this.info.relaycastApiKey,
|
|
315
|
+
RELAYCAST_BASE_URL: relaycastBaseUrl,
|
|
316
|
+
RELAY_BASE_URL: relaycastBaseUrl
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
agentInvite(options = {}) {
|
|
320
|
+
const relaycastBaseUrl = this.resolveRelaycastBaseUrl(options.relaycastBaseUrl);
|
|
321
|
+
return compactObject({
|
|
322
|
+
workspaceId: this.workspaceId,
|
|
323
|
+
cloudApiUrl: this._setup.getCloudApiUrl(),
|
|
324
|
+
relayfileUrl: this.info.relayfileUrl,
|
|
325
|
+
relaycastApiKey: this.info.relaycastApiKey,
|
|
326
|
+
relaycastBaseUrl,
|
|
327
|
+
agentName: options.agentName ?? this._joinOptions.agentName,
|
|
328
|
+
scopes: options.scopes && options.scopes.length > 0
|
|
329
|
+
? [...options.scopes]
|
|
330
|
+
: [...this._joinOptions.scopes],
|
|
331
|
+
relayfileToken: options.includeRelayfileToken === false ? undefined : this.getToken(),
|
|
332
|
+
createdAt: this.info.createdAt,
|
|
333
|
+
name: this.info.name
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async refreshToken() {
|
|
337
|
+
if (!this._refreshPromise) {
|
|
338
|
+
this._refreshPromise = this.performRefreshToken();
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
await this._refreshPromise;
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
this._refreshPromise = undefined;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async performRefreshToken() {
|
|
348
|
+
const response = await this._setup.joinWorkspaceResponse(this.workspaceId, this._joinOptions);
|
|
349
|
+
this._token = response.token;
|
|
350
|
+
this._tokenIssuedAt = Date.now();
|
|
351
|
+
}
|
|
352
|
+
async getOrRefreshToken() {
|
|
353
|
+
if (Date.now() - this._tokenIssuedAt >= TOKEN_REFRESH_AGE_MS) {
|
|
354
|
+
await this.refreshToken();
|
|
355
|
+
}
|
|
356
|
+
return this._token;
|
|
357
|
+
}
|
|
358
|
+
resolveConnectionId(provider, connectionId) {
|
|
359
|
+
const resolved = normalizeConnectionId(connectionId) ?? this._pendingConnections.get(provider);
|
|
360
|
+
if (!resolved) {
|
|
361
|
+
throw new MissingConnectionIdError(provider);
|
|
362
|
+
}
|
|
363
|
+
return resolved;
|
|
364
|
+
}
|
|
365
|
+
async getConnectionStatus(provider, connectionId, options = {}) {
|
|
366
|
+
const query = new URLSearchParams({ connectionId });
|
|
367
|
+
const response = validateIntegrationStatusResponse(await this._setup.requestJson({
|
|
368
|
+
operation: "getIntegrationStatus",
|
|
369
|
+
method: "GET",
|
|
370
|
+
path: `api/v1/workspaces/${encodeURIComponent(this.workspaceId)}/integrations/${encodeURIComponent(provider)}/status?${query.toString()}`,
|
|
371
|
+
signal: options.signal,
|
|
372
|
+
timeoutMs: options.timeoutMs,
|
|
373
|
+
tokenProvider: async () => this.getOrRefreshToken()
|
|
374
|
+
}));
|
|
375
|
+
return response.ready;
|
|
376
|
+
}
|
|
377
|
+
resolveRelaycastBaseUrl(override) {
|
|
378
|
+
return (normalizeNonEmptyString(override) ??
|
|
379
|
+
normalizeNonEmptyString(this.info.relaycastBaseUrl) ??
|
|
380
|
+
DEFAULT_RELAYCAST_BASE_URL);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function assertProvider(provider) {
|
|
384
|
+
if (!WORKSPACE_INTEGRATION_PROVIDERS.includes(provider)) {
|
|
385
|
+
throw new UnknownProviderError(provider);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function normalizeJoinWorkspaceOptions(options = {}) {
|
|
389
|
+
return {
|
|
390
|
+
agentName: normalizeNonEmptyString(options.agentName) ?? DEFAULT_AGENT_NAME,
|
|
391
|
+
scopes: options.scopes && options.scopes.length > 0
|
|
392
|
+
? [...options.scopes]
|
|
393
|
+
: [...DEFAULT_SCOPES],
|
|
394
|
+
permissions: clonePermissions(options.permissions)
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function normalizeRetryOptions(options) {
|
|
398
|
+
const maxRetries = Math.max(0, Math.floor(options?.maxRetries ?? DEFAULT_RETRY_MAX_RETRIES));
|
|
399
|
+
const baseDelayMs = Math.max(1, Math.floor(options?.baseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS));
|
|
400
|
+
return {
|
|
401
|
+
maxRetries,
|
|
402
|
+
baseDelayMs,
|
|
403
|
+
maxDelayMs: DEFAULT_RETRY_MAX_DELAY_MS
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function validateCreateWorkspaceResponse(payload) {
|
|
407
|
+
return {
|
|
408
|
+
workspaceId: requireStringField(payload, "workspaceId"),
|
|
409
|
+
relayfileUrl: requireStringField(payload, "relayfileUrl"),
|
|
410
|
+
relaycastApiKey: requireStringField(payload, "relaycastApiKey"),
|
|
411
|
+
createdAt: requireStringField(payload, "createdAt"),
|
|
412
|
+
name: readOptionalStringField(payload, "name")
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function validateJoinWorkspaceResponse(payload) {
|
|
416
|
+
return {
|
|
417
|
+
workspaceId: requireStringField(payload, "workspaceId"),
|
|
418
|
+
token: requireStringField(payload, "token"),
|
|
419
|
+
relayfileUrl: requireStringField(payload, "relayfileUrl"),
|
|
420
|
+
wsUrl: requireStringField(payload, "wsUrl"),
|
|
421
|
+
relaycastApiKey: requireStringField(payload, "relaycastApiKey"),
|
|
422
|
+
relaycastBaseUrl: readOptionalStringField(payload, "relaycastBaseUrl")
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function validateConnectSessionResponse(payload) {
|
|
426
|
+
return {
|
|
427
|
+
token: requireStringField(payload, "token"),
|
|
428
|
+
expiresAt: requireStringField(payload, "expiresAt"),
|
|
429
|
+
connectLink: requireStringField(payload, "connectLink"),
|
|
430
|
+
connectionId: readOptionalStringField(payload, "connectionId")
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function validateIntegrationStatusResponse(payload) {
|
|
434
|
+
return {
|
|
435
|
+
ready: requireBooleanField(payload, "ready")
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function requireStringField(payload, field) {
|
|
439
|
+
const value = readField(payload, field);
|
|
440
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
441
|
+
throw new MalformedCloudResponseError(field, payload);
|
|
442
|
+
}
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
function readOptionalStringField(payload, field) {
|
|
446
|
+
const value = readField(payload, field);
|
|
447
|
+
if (value === undefined) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
451
|
+
throw new MalformedCloudResponseError(field, payload);
|
|
452
|
+
}
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
function requireBooleanField(payload, field) {
|
|
456
|
+
const value = readField(payload, field);
|
|
457
|
+
if (typeof value !== "boolean") {
|
|
458
|
+
throw new MalformedCloudResponseError(field, payload);
|
|
459
|
+
}
|
|
460
|
+
return value;
|
|
461
|
+
}
|
|
462
|
+
function readField(payload, field) {
|
|
463
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
return payload[field];
|
|
467
|
+
}
|
|
468
|
+
function normalizeConnectionId(connectionId) {
|
|
469
|
+
return normalizeNonEmptyString(connectionId);
|
|
470
|
+
}
|
|
471
|
+
function normalizeNonEmptyString(value) {
|
|
472
|
+
const normalized = value?.trim();
|
|
473
|
+
return normalized ? normalized : undefined;
|
|
474
|
+
}
|
|
475
|
+
function clonePermissions(permissions) {
|
|
476
|
+
if (!permissions) {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
return compactObject({
|
|
480
|
+
readonly: permissions.readonly ? [...permissions.readonly] : undefined,
|
|
481
|
+
ignored: permissions.ignored ? [...permissions.ignored] : undefined
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function compactObject(value) {
|
|
485
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
486
|
+
}
|
|
487
|
+
function compactStringRecord(value) {
|
|
488
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => {
|
|
489
|
+
const [, entryValue] = entry;
|
|
490
|
+
return entryValue !== undefined;
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
function buildCloudUrl(baseUrl, path) {
|
|
494
|
+
const base = new URL(baseUrl);
|
|
495
|
+
if (!base.pathname.endsWith("/")) {
|
|
496
|
+
base.pathname = `${base.pathname}/`;
|
|
497
|
+
}
|
|
498
|
+
return new URL(path.replace(/^\/+/, ""), base).toString();
|
|
499
|
+
}
|
|
500
|
+
async function resolveToken(provider) {
|
|
501
|
+
if (!provider) {
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
return typeof provider === "function" ? provider() : provider;
|
|
505
|
+
}
|
|
506
|
+
async function fetchWithTimeout(url, init, timeoutMs, operation) {
|
|
507
|
+
if (init.signal?.aborted) {
|
|
508
|
+
throw new CloudAbortError(operation);
|
|
509
|
+
}
|
|
510
|
+
let didTimeout = false;
|
|
511
|
+
let didAbort = false;
|
|
512
|
+
const controller = new AbortController();
|
|
513
|
+
const onAbort = () => {
|
|
514
|
+
didAbort = true;
|
|
515
|
+
controller.abort();
|
|
516
|
+
};
|
|
517
|
+
const timer = setTimeout(() => {
|
|
518
|
+
didTimeout = true;
|
|
519
|
+
controller.abort();
|
|
520
|
+
}, timeoutMs);
|
|
521
|
+
init.signal?.addEventListener("abort", onAbort, { once: true });
|
|
522
|
+
try {
|
|
523
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
if (didTimeout) {
|
|
527
|
+
throw new CloudTimeoutError(operation, timeoutMs);
|
|
528
|
+
}
|
|
529
|
+
if (didAbort || init.signal?.aborted) {
|
|
530
|
+
throw new CloudAbortError(operation);
|
|
531
|
+
}
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
finally {
|
|
535
|
+
clearTimeout(timer);
|
|
536
|
+
init.signal?.removeEventListener("abort", onAbort);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async function readResponseBody(response) {
|
|
540
|
+
const text = await response.text();
|
|
541
|
+
if (text === "") {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
545
|
+
if (contentType.includes("application/json")) {
|
|
546
|
+
try {
|
|
547
|
+
return JSON.parse(text);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
return text;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return text;
|
|
554
|
+
}
|
|
555
|
+
function shouldRetryStatus(status, retries, maxRetries, signal) {
|
|
556
|
+
if (signal?.aborted || retries >= maxRetries) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
return status === 429 || (status >= 500 && status <= 599);
|
|
560
|
+
}
|
|
561
|
+
function shouldRetryError(error, retries, maxRetries, signal) {
|
|
562
|
+
if (signal?.aborted || retries >= maxRetries) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
return !(error instanceof Error && error.name === "AbortError");
|
|
566
|
+
}
|
|
567
|
+
function computeRetryDelayMs(options, retryAttempt, retryAfterHeader) {
|
|
568
|
+
const retryAfterMs = parseRetryAfterMs(retryAfterHeader);
|
|
569
|
+
if (retryAfterMs !== null) {
|
|
570
|
+
return Math.min(options.maxDelayMs, retryAfterMs);
|
|
571
|
+
}
|
|
572
|
+
const backoff = options.baseDelayMs * Math.pow(2, Math.max(0, retryAttempt - 1));
|
|
573
|
+
return Math.min(options.maxDelayMs, backoff);
|
|
574
|
+
}
|
|
575
|
+
function parseRetryAfterMs(retryAfterHeader) {
|
|
576
|
+
if (!retryAfterHeader) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const seconds = Number.parseInt(retryAfterHeader, 10);
|
|
580
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
581
|
+
return seconds * 1000;
|
|
582
|
+
}
|
|
583
|
+
const timestamp = Date.parse(retryAfterHeader);
|
|
584
|
+
if (!Number.isNaN(timestamp)) {
|
|
585
|
+
return Math.max(0, timestamp - Date.now());
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
async function sleep(delayMs, signal, operation) {
|
|
590
|
+
if (delayMs <= 0) {
|
|
591
|
+
throwIfAborted(signal, operation);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
throwIfAborted(signal, operation);
|
|
595
|
+
await new Promise((resolve, reject) => {
|
|
596
|
+
const timer = setTimeout(() => {
|
|
597
|
+
signal?.removeEventListener("abort", onAbort);
|
|
598
|
+
resolve();
|
|
599
|
+
}, delayMs);
|
|
600
|
+
const onAbort = () => {
|
|
601
|
+
clearTimeout(timer);
|
|
602
|
+
signal?.removeEventListener("abort", onAbort);
|
|
603
|
+
reject(new CloudAbortError(operation));
|
|
604
|
+
};
|
|
605
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function throwIfAborted(signal, operation) {
|
|
609
|
+
if (signal?.aborted) {
|
|
610
|
+
throw new CloudAbortError(operation);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const RELAYFILE_SDK_VERSION = "0.6.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const RELAYFILE_SDK_VERSION = "0.6.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "TypeScript SDK for relayfile — real-time filesystem for humans and agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,10 +12,15 @@
|
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"typecheck": "tsc --noEmit",
|
|
14
14
|
"test": "vitest run",
|
|
15
|
+
"test:e2e": "node scripts/setup-e2e.mjs",
|
|
16
|
+
"agent-workspace:e2e": "node scripts/agent-workspace-golden-path-e2e.mjs",
|
|
17
|
+
"test:e2e:golden-path": "node scripts/agent-workspace-golden-path-e2e.mjs",
|
|
18
|
+
"demo:agent-workspace": "npm run build && node scripts/agent-workspace-demo.mjs",
|
|
19
|
+
"setup:e2e": "node scripts/setup-e2e.mjs",
|
|
15
20
|
"prepublishOnly": "npm run build"
|
|
16
21
|
},
|
|
17
22
|
"dependencies": {
|
|
18
|
-
"@relayfile/core": "0.
|
|
23
|
+
"@relayfile/core": "0.6.1"
|
|
19
24
|
},
|
|
20
25
|
"devDependencies": {
|
|
21
26
|
"typescript": "^5.7.3",
|