@relayfile/sdk 0.1.4 → 0.1.6
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 +2 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.js +29 -12
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/writeback-consumer.d.ts +31 -0
- package/dist/writeback-consumer.js +139 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ await client.writeFile({
|
|
|
40
40
|
});
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
Use a relayfile JWT whose claims include `workspace_id`, `agent_name`, and `aud: ["relayfile"]`. The SDK adds `X-Correlation-Id` automatically for API calls.
|
|
44
|
+
|
|
43
45
|
## Full Docs
|
|
44
46
|
|
|
45
47
|
Full documentation is available in the [relayfile docs](https://github.com/AgentWorkforce/relayfile/tree/main/docs).
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type AdminIngressStatusResponse, type AdminSyncStatusResponse, type BulkWriteInput, type BulkWriteResponse, type BackendStatusResponse, type AckResponse, type DeleteFileInput, type DeadLetterItem, type DeadLetterFeedResponse, type EventFeedResponse, type ExportJsonResponse, type ExportOptions, type FileQueryResponse, type FileReadResponse, type FilesystemEvent, type GetEventsOptions, type GetAdminIngressStatusOptions, type GetAdminSyncStatusOptions, type GetOperationsOptions, type GetSyncDeadLettersOptions, type GetSyncIngressStatusOptions, type GetSyncStatusOptions, type ListTreeOptions, type OperationFeedResponse, type OperationStatusResponse, type QueuedResponse, type QueryFilesOptions, type SyncIngressStatusResponse, type SyncStatusResponse, type TreeResponse, type WriteFileInput, type WriteQueuedResponse, type IngestWebhookInput, type WritebackItem, type AckWritebackInput, type AckWritebackResponse } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Bearer token or token factory used for Relayfile API requests.
|
|
4
|
+
*
|
|
5
|
+
* When you mint JWTs for Relayfile, the server expects claims like:
|
|
6
|
+
* `{ workspace_id: "ws_123", agent_name: "review-bot", aud: ["relayfile"] }`
|
|
7
|
+
*/
|
|
2
8
|
export type AccessTokenProvider = string | (() => string | Promise<string>);
|
|
3
9
|
export interface RelayFileRetryOptions {
|
|
4
10
|
maxRetries?: number;
|
|
@@ -11,6 +17,12 @@ export declare const DEFAULT_RELAYFILE_BASE_URL = "https://api.relayfile.dev";
|
|
|
11
17
|
export interface RelayFileClientOptions {
|
|
12
18
|
/** API base URL. Defaults to https://api.relayfile.dev */
|
|
13
19
|
baseUrl?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Bearer token or token factory for SDK requests.
|
|
22
|
+
*
|
|
23
|
+
* Relayfile-authenticated JWTs should include `workspace_id`, `agent_name`,
|
|
24
|
+
* and `aud` containing `relayfile`.
|
|
25
|
+
*/
|
|
14
26
|
token: AccessTokenProvider;
|
|
15
27
|
fetchImpl?: typeof fetch;
|
|
16
28
|
userAgent?: string;
|
package/dist/client.js
CHANGED
|
@@ -30,7 +30,19 @@ function buildQuery(params) {
|
|
|
30
30
|
return encoded ? `?${encoded}` : "";
|
|
31
31
|
}
|
|
32
32
|
function generateCorrelationId() {
|
|
33
|
-
return `rf_${
|
|
33
|
+
return `rf_${crypto.randomUUID()}`;
|
|
34
|
+
}
|
|
35
|
+
function getHeaderValue(headers, name) {
|
|
36
|
+
if (!headers) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const target = name.toLowerCase();
|
|
40
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
41
|
+
if (key.toLowerCase() === target) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
34
46
|
}
|
|
35
47
|
async function resolveToken(tokenProvider) {
|
|
36
48
|
if (typeof tokenProvider === "function") {
|
|
@@ -202,21 +214,23 @@ export class RelayFileClient {
|
|
|
202
214
|
});
|
|
203
215
|
}
|
|
204
216
|
async writeFile(input) {
|
|
205
|
-
const
|
|
217
|
+
const { workspaceId, path, correlationId, baseRevision, content, contentType, encoding, semantics, signal } = input;
|
|
218
|
+
const query = buildQuery({ path });
|
|
206
219
|
return this.request({
|
|
207
220
|
method: "PUT",
|
|
208
|
-
path: `/v1/workspaces/${encodeURIComponent(
|
|
209
|
-
correlationId
|
|
221
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/fs/file${query}`,
|
|
222
|
+
correlationId,
|
|
210
223
|
headers: {
|
|
211
|
-
"If-Match":
|
|
224
|
+
"If-Match": baseRevision
|
|
212
225
|
},
|
|
226
|
+
// Single-file PUT expects the target path in the query string, not the JSON body.
|
|
213
227
|
body: {
|
|
214
|
-
contentType:
|
|
215
|
-
content
|
|
216
|
-
encoding
|
|
228
|
+
contentType: contentType ?? "text/markdown",
|
|
229
|
+
content,
|
|
230
|
+
encoding,
|
|
217
231
|
semantics: input.semantics
|
|
218
232
|
},
|
|
219
|
-
signal
|
|
233
|
+
signal
|
|
220
234
|
});
|
|
221
235
|
}
|
|
222
236
|
async bulkWrite(input) {
|
|
@@ -498,11 +512,14 @@ export class RelayFileClient {
|
|
|
498
512
|
return payload;
|
|
499
513
|
}
|
|
500
514
|
async performRequest(params) {
|
|
501
|
-
const
|
|
515
|
+
const existingCorrelationId = getHeaderValue(params.headers, "X-Correlation-Id");
|
|
516
|
+
const correlationId = existingCorrelationId ?? params.correlationId ?? generateCorrelationId();
|
|
502
517
|
const baseHeaders = {
|
|
503
|
-
|
|
504
|
-
...params.headers
|
|
518
|
+
...(params.headers ?? {})
|
|
505
519
|
};
|
|
520
|
+
if (!existingCorrelationId) {
|
|
521
|
+
baseHeaders["X-Correlation-Id"] = correlationId;
|
|
522
|
+
}
|
|
506
523
|
if (params.body !== undefined) {
|
|
507
524
|
baseHeaders["Content-Type"] = "application/json";
|
|
508
525
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL, type ConnectWebSocketOptions, type RelayFileRetryOptions, type WebSocketConnection } from "./client.js";
|
|
1
|
+
export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL, type AccessTokenProvider, type ConnectWebSocketOptions, type RelayFileClientOptions, type RelayFileRetryOptions, type WebSocketConnection } from "./client.js";
|
|
2
2
|
export { RelayFileSync, type RelayFileSyncOptions, type RelayFileSyncPong, type RelayFileSyncReconnectOptions, type RelayFileSyncSocket, type RelayFileSyncState } from "./sync.js";
|
|
3
3
|
export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
|
|
4
4
|
export { IntegrationProvider, computeCanonicalPath } from "./provider.js";
|
|
5
5
|
export type { WebhookInput, ListProviderFilesOptions, WatchProviderEventsOptions } from "./provider.js";
|
|
6
6
|
export type { ConnectionProvider, NormalizedWebhook, ProxyHeaders, ProxyMethod, ProxyQuery, ProxyRequest, ProxyResponse, } from "./connection.js";
|
|
7
|
-
export type { AckResponse, AckWritebackInput, AckWritebackResponse, AdminIngressAlert, AdminIngressAlertProfile, AdminIngressEffectiveAlertProfile, AdminIngressAlertSeverity, AdminIngressAlertThresholds, AdminIngressAlertTotals, AdminIngressAlertType, AdminIngressStatusResponse, AdminSyncAlert, AdminSyncAlertSeverity, AdminSyncAlertThresholds, AdminSyncAlertTotals, AdminSyncAlertType, AdminSyncStatusResponse, BackendStatusResponse, BulkWriteFile, BulkWriteInput, BulkWriteResponse, ConflictErrorResponse, DeleteFileInput, DeadLetterFeedResponse, DeadLetterItem, ErrorResponse, EventFeedResponse, ExportFormat, ExportJsonResponse, ExportOptions, FileQueryItem, FileQueryResponse, FileReadResponse, FileSemantics, FileWriteRequest, FilesystemEvent, FilesystemEventType, EventOrigin, GetEventsOptions, GetAdminSyncStatusOptions, GetAdminIngressStatusOptions, GetOperationsOptions, GetSyncDeadLettersOptions, GetSyncIngressStatusOptions, GetSyncStatusOptions, IngestWebhookInput, ListTreeOptions, OperationFeedResponse, OperationStatus, OperationStatusResponse, QueuedResponse, QueryFilesOptions, SyncIngressStatusResponse, SyncProviderStatus, SyncProviderStatusState, SyncRefreshRequest, SyncStatusResponse, TreeEntry, TreeResponse, WritebackActionType, WritebackState, WritebackItem, WriteFileInput, WriteQueuedResponse } from "./types.js";
|
|
7
|
+
export type { AckResponse, AckWritebackInput, AckWritebackResponse, AdminIngressAlert, AdminIngressAlertProfile, AdminIngressEffectiveAlertProfile, AdminIngressAlertSeverity, AdminIngressAlertThresholds, AdminIngressAlertTotals, AdminIngressAlertType, AdminIngressStatusResponse, AdminSyncAlert, AdminSyncAlertSeverity, AdminSyncAlertThresholds, AdminSyncAlertTotals, AdminSyncAlertType, AdminSyncStatusResponse, BackendStatusResponse, BulkWriteFile, BulkWriteInput, BulkWriteResponse, ConflictErrorResponse, DeleteFileInput, DeadLetterFeedResponse, DeadLetterItem, ErrorResponse, EventFeedResponse, ExportFormat, ExportJsonResponse, ExportOptions, FileQueryItem, FileQueryResponse, FileReadResponse, FileSemantics, FileWriteRequest, FilesystemEvent, FilesystemEventType, EventOrigin, GetEventsOptions, GetAdminSyncStatusOptions, GetAdminIngressStatusOptions, GetOperationsOptions, GetSyncDeadLettersOptions, GetSyncIngressStatusOptions, GetSyncStatusOptions, IngestWebhookInput, ListTreeOptions, OperationFeedResponse, OperationStatus, OperationStatusResponse, QueuedResponse, QueryFilesOptions, RelayFileJwtClaims, SyncIngressStatusResponse, SyncProviderStatus, SyncProviderStatusState, SyncRefreshRequest, SyncStatusResponse, TreeEntry, TreeResponse, WritebackActionType, WritebackState, WritebackItem, WriteFileInput, WriteQueuedResponse } from "./types.js";
|
|
8
|
+
export { WritebackConsumer } from "./writeback-consumer.js";
|
|
9
|
+
export type { WritebackHandler, WritebackConsumerOptions } from "./writeback-consumer.js";
|
package/dist/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { RelayFileSync } from "./sync.js";
|
|
|
3
3
|
export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
|
|
4
4
|
// Integration providers
|
|
5
5
|
export { IntegrationProvider, computeCanonicalPath } from "./provider.js";
|
|
6
|
+
export { WritebackConsumer } from "./writeback-consumer.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
export type FileNodeType = "file" | "dir";
|
|
2
|
+
/**
|
|
3
|
+
* JWT claims expected by the Relayfile Go API when your token provider mints
|
|
4
|
+
* bearer tokens for SDK requests.
|
|
5
|
+
*
|
|
6
|
+
* Example minting payload:
|
|
7
|
+
* `const claims: RelayFileJwtClaims = { workspace_id: "ws_123", agent_name: "review-bot", aud: ["relayfile"] };`
|
|
8
|
+
*/
|
|
9
|
+
export interface RelayFileJwtClaims {
|
|
10
|
+
/** Workspace id the token is scoped to, for example `ws_123`. */
|
|
11
|
+
workspace_id: string;
|
|
12
|
+
/** Stable agent name presented to the Relayfile server, for example `review-bot`. */
|
|
13
|
+
agent_name: string;
|
|
14
|
+
/** Audience must contain `relayfile`. */
|
|
15
|
+
aud: "relayfile" | string[];
|
|
16
|
+
exp?: number;
|
|
17
|
+
iat?: number;
|
|
18
|
+
nbf?: number;
|
|
19
|
+
iss?: string;
|
|
20
|
+
sub?: string;
|
|
21
|
+
[claim: string]: unknown;
|
|
22
|
+
}
|
|
2
23
|
export interface TreeEntry {
|
|
3
24
|
path: string;
|
|
4
25
|
type: FileNodeType;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RelayFileClient } from "./client.js";
|
|
2
|
+
import type { ConnectionProvider } from "./connection.js";
|
|
3
|
+
import type { WritebackItem } from "./types.js";
|
|
4
|
+
export interface WritebackHandler {
|
|
5
|
+
canHandle(path: string): boolean;
|
|
6
|
+
execute(item: WritebackItem, provider: ConnectionProvider): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export interface WritebackConsumerOptions {
|
|
9
|
+
client: RelayFileClient;
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
handlers: WritebackHandler[];
|
|
12
|
+
provider: ConnectionProvider;
|
|
13
|
+
pollIntervalMs?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
export declare class WritebackConsumer {
|
|
17
|
+
private readonly client;
|
|
18
|
+
private readonly workspaceId;
|
|
19
|
+
private readonly handlers;
|
|
20
|
+
private readonly provider;
|
|
21
|
+
private readonly pollIntervalMs;
|
|
22
|
+
private readonly signal?;
|
|
23
|
+
private loopPromise?;
|
|
24
|
+
private stopped;
|
|
25
|
+
constructor(opts: WritebackConsumerOptions);
|
|
26
|
+
start(): Promise<void>;
|
|
27
|
+
stop(): void;
|
|
28
|
+
pollOnce(signal?: AbortSignal): Promise<void>;
|
|
29
|
+
private runLoop;
|
|
30
|
+
private ackFailure;
|
|
31
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
2
|
+
export class WritebackConsumer {
|
|
3
|
+
client;
|
|
4
|
+
workspaceId;
|
|
5
|
+
handlers;
|
|
6
|
+
provider;
|
|
7
|
+
pollIntervalMs;
|
|
8
|
+
signal;
|
|
9
|
+
loopPromise;
|
|
10
|
+
stopped = false;
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
this.client = opts.client;
|
|
13
|
+
this.workspaceId = opts.workspaceId;
|
|
14
|
+
this.handlers = opts.handlers;
|
|
15
|
+
this.provider = opts.provider;
|
|
16
|
+
this.pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
17
|
+
this.signal = opts.signal;
|
|
18
|
+
if (this.pollIntervalMs < 0) {
|
|
19
|
+
throw new RangeError("pollIntervalMs must be greater than or equal to 0");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async start() {
|
|
23
|
+
if (this.loopPromise) {
|
|
24
|
+
if (this.stopped) {
|
|
25
|
+
// Previous run was stopped — reset and start fresh
|
|
26
|
+
this.stopped = false;
|
|
27
|
+
this.loopPromise = undefined;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return this.loopPromise;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.loopPromise = this.runLoop();
|
|
34
|
+
return this.loopPromise;
|
|
35
|
+
}
|
|
36
|
+
stop() {
|
|
37
|
+
this.stopped = true;
|
|
38
|
+
}
|
|
39
|
+
async pollOnce(signal = this.signal) {
|
|
40
|
+
const items = await this.client.listPendingWritebacks(this.workspaceId, undefined, signal);
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
if (signal?.aborted || this.stopped) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const handler = this.handlers.find((candidate) => candidate.canHandle(item.path));
|
|
46
|
+
if (!handler) {
|
|
47
|
+
await this.ackFailure(item, new Error(`No writeback handler found for path: ${item.path}`), signal);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let executed = false;
|
|
51
|
+
try {
|
|
52
|
+
await handler.execute(item, this.provider);
|
|
53
|
+
executed = true;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (isAbortError(error) || signal?.aborted) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
await this.ackFailure(item, error, signal);
|
|
60
|
+
}
|
|
61
|
+
if (executed) {
|
|
62
|
+
try {
|
|
63
|
+
await this.client.ackWriteback({
|
|
64
|
+
workspaceId: this.workspaceId,
|
|
65
|
+
itemId: item.id,
|
|
66
|
+
success: true,
|
|
67
|
+
correlationId: item.correlationId,
|
|
68
|
+
signal
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (ackError) {
|
|
72
|
+
if (isAbortError(ackError) || signal?.aborted) {
|
|
73
|
+
throw ackError;
|
|
74
|
+
}
|
|
75
|
+
// Handler succeeded — do NOT send failure ack (would cause duplicate writes on retry).
|
|
76
|
+
// Log and move on; server will eventually time out and retry the ack.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async runLoop() {
|
|
82
|
+
while (!this.stopped && !this.signal?.aborted) {
|
|
83
|
+
try {
|
|
84
|
+
await this.pollOnce(this.signal);
|
|
85
|
+
if (this.stopped || this.signal?.aborted) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await sleep(this.pollIntervalMs, this.signal);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (isAbortError(error) || this.signal?.aborted || this.stopped) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async ackFailure(item, error, signal) {
|
|
99
|
+
await this.client.ackWriteback({
|
|
100
|
+
workspaceId: this.workspaceId,
|
|
101
|
+
itemId: item.id,
|
|
102
|
+
success: false,
|
|
103
|
+
error: toErrorMessage(error),
|
|
104
|
+
correlationId: item.correlationId,
|
|
105
|
+
signal
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function sleep(ms, signal) {
|
|
110
|
+
if (ms === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (signal?.aborted) {
|
|
114
|
+
throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
115
|
+
}
|
|
116
|
+
await new Promise((resolve, reject) => {
|
|
117
|
+
const onAbort = () => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
reject(signal.reason ?? new DOMException("The operation was aborted", "AbortError"));
|
|
120
|
+
};
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
signal?.removeEventListener("abort", onAbort);
|
|
123
|
+
resolve();
|
|
124
|
+
}, ms);
|
|
125
|
+
if (!signal) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function isAbortError(error) {
|
|
132
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
133
|
+
}
|
|
134
|
+
function toErrorMessage(error) {
|
|
135
|
+
if (error instanceof Error) {
|
|
136
|
+
return error.message;
|
|
137
|
+
}
|
|
138
|
+
return String(error);
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
13
14
|
"test": "vitest run",
|
|
14
15
|
"prepublishOnly": "npm run build"
|
|
15
16
|
},
|