@rayrun/sdk 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rayrun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @rayrun/sdk
2
+
3
+ ```sh
4
+ npm install @rayrun/sdk
5
+ ```
6
+
7
+ ```js
8
+ import { Rayrun } from '@rayrun/sdk';
9
+
10
+ const rayrun = new Rayrun({ apiKey: process.env.RAYRUN_API_KEY });
11
+ const { items: connections } = await rayrun.connections.list();
12
+ ```
13
+
14
+ Create keys in **Dashboard → Settings → API keys**. The plaintext is shown once.
15
+
16
+ The client covers catalog search, connection and credential setup, OAuth links, indexing, tool and
17
+ client policies, activity, review queues, and webhooks. Safe reads retry rate limits, server errors,
18
+ and transient network failures; writes do not retry automatically.
19
+
20
+ ## Stream code run-ahead
21
+
22
+ A model harness that receives `execute_code` arguments incrementally can let Rayrun start eligible
23
+ reads before generation finishes. Authenticate this companion channel with the same MCP OAuth access
24
+ token used for the final tool call. Feed cumulative JSON argument snapshots, not individual token
25
+ deltas.
26
+
27
+ ```js
28
+ import { RayrunGateway } from '@rayrun/sdk';
29
+
30
+ const gateway = new RayrunGateway({ accessToken: mcpAccessToken });
31
+ const runAhead = await gateway.codeRunAhead.open();
32
+
33
+ try {
34
+ for await (const argumentsSnapshot of streamedExecuteCodeArguments) {
35
+ void runAhead?.feedArguments(argumentsSnapshot);
36
+ }
37
+
38
+ await runAhead?.flush();
39
+
40
+ const result = await mcp.callTool({
41
+ name: 'execute_code',
42
+ arguments: JSON.parse(finalArguments),
43
+ ...(runAhead ? { _meta: runAhead.meta } : {}),
44
+ });
45
+ } finally {
46
+ await runAhead?.close();
47
+ }
48
+ ```
49
+
50
+ `open()` returns `null` when this optional optimization is rate-limited or temporarily unavailable.
51
+ `feedArguments()` coalesces snapshots while one request is in flight; call `flush()` once before the
52
+ final MCP request. It throws synchronously when a snapshot is not a string, so TypeScript users get
53
+ the mistake at the call site. If an active session becomes unavailable, `meta` becomes an empty object. In
54
+ either case, send the final MCP call normally. Rayrun only runs ahead tools that are explicitly
55
+ allowed, approved as Read by a workspace owner, and advertised by their server as read-only and
56
+ idempotent. The final MCP call rechecks the token, complete program, policy, definition, arguments,
57
+ and upstream identity before it can use an early result.
58
+
59
+ ## Verify webhooks
60
+
61
+ Pass the exact request body and the `Rayrun-Signature` header before parsing the event. Verification
62
+ also rejects timestamps outside a five-minute replay window by default.
63
+
64
+ ```js
65
+ import { verifyWebhookSignature } from '@rayrun/sdk';
66
+
67
+ const valid = verifyWebhookSignature({
68
+ body: rawBody,
69
+ secret: process.env.RAYRUN_WEBHOOK_SECRET,
70
+ signature: request.headers['rayrun-signature'],
71
+ });
72
+ ```
73
+
74
+ After verification, parse the body as `WebhookPayload`. Every event has a stable `id`, `type`, and
75
+ `occurredAt`; its `data` shape is narrowed by `type`. Use `id` to deduplicate retries.
76
+
77
+ ```ts
78
+ import type { WebhookPayload } from '@rayrun/sdk';
79
+
80
+ const event: WebhookPayload = JSON.parse(rawBody);
81
+ if (event.type === 'review.changed') {
82
+ console.log(event.data.reviewUid, event.data.status);
83
+ }
84
+ ```
package/index.d.ts ADDED
@@ -0,0 +1,265 @@
1
+ /* oxlint-disable perfectionist/sort-classes, perfectionist/sort-modules, perfectionist/sort-object-types, perfectionist/sort-union-types -- Public types follow the documented API resource order. */
2
+ export type CursorPage<T> = { items: T[]; page: { nextCursor: string | null } };
3
+ export type PageQuery = { cursor?: string; limit?: number };
4
+ export type ToolDecision = 'allow' | 'ask' | 'block';
5
+ export type ToolAccessMode = 'read-only' | 'ask-before-changes' | 'allow-all' | 'custom';
6
+ export type Risk = 'read' | 'change' | 'destructive' | 'unknown';
7
+ export type Connection = {
8
+ authorizedAt: string | null;
9
+ authType: 'api-key' | 'basic' | 'none' | 'oauth2';
10
+ createdAt: string;
11
+ uid: string;
12
+ displayName: string;
13
+ enabled: boolean;
14
+ healthy: boolean | null;
15
+ kind: 'mcp' | 'openapi';
16
+ lastIndexedAt: string | null;
17
+ slug: string;
18
+ status: 'connected' | 'disabled' | 'error' | 'indexing' | 'needs-authorization' | 'new';
19
+ toolAccessMode: ToolAccessMode;
20
+ };
21
+ export type CatalogEntry = {
22
+ authType: Connection['authType'];
23
+ category: string | null;
24
+ description: string | null;
25
+ name: string;
26
+ namespace: string;
27
+ slug: string;
28
+ transport: string;
29
+ uid: string;
30
+ };
31
+ export type Tool = {
32
+ uid: string;
33
+ connectionUid: string;
34
+ name: string;
35
+ description: string | null;
36
+ risk: Risk;
37
+ riskReason: string;
38
+ approved: boolean;
39
+ rule: ToolDecision | null;
40
+ available: boolean;
41
+ callCount: number;
42
+ inputSchema: Record<string, unknown>;
43
+ title: string | null;
44
+ };
45
+ export type Client = {
46
+ clientName: string;
47
+ grantedAt: string;
48
+ lastUsedAt: string | null;
49
+ redirectHost: string;
50
+ revokedAt: string | null;
51
+ toolAccessMode: ToolAccessMode;
52
+ uid: string;
53
+ userName: string;
54
+ };
55
+ export type Activity = {
56
+ calledAt: string;
57
+ clientName: string | null;
58
+ connectionName: string | null;
59
+ durationMs: number | null;
60
+ errorCode: string | null;
61
+ id: string;
62
+ policyDecision: ToolDecision | null;
63
+ succeeded: boolean | null;
64
+ toolName: string;
65
+ userName: string | null;
66
+ };
67
+ export type Review = {
68
+ connectionName: string;
69
+ createdAt: string;
70
+ expiresAt: string;
71
+ status: 'approved' | 'cancelled' | 'consumed' | 'denied' | 'expired' | 'pending';
72
+ toolName: string;
73
+ uid: string;
74
+ };
75
+ export type Webhook = {
76
+ createdAt: string;
77
+ description: string | null;
78
+ enabled: boolean;
79
+ eventTypes: WebhookEvent[];
80
+ uid: string;
81
+ url: string;
82
+ };
83
+ export type Credential =
84
+ | {
85
+ authType: 'api-key';
86
+ apiKey: string;
87
+ apiKeyLocation: 'bearer-authentication' | 'header' | 'search-parameter';
88
+ apiKeyName?: string | null;
89
+ headers?: Record<string, string> | null;
90
+ }
91
+ | {
92
+ authType: 'basic';
93
+ basicPassword: string;
94
+ basicUsername: string;
95
+ headers?: Record<string, string> | null;
96
+ }
97
+ | { authType: 'none'; headers?: Record<string, string> | null };
98
+ export type WebhookEvent =
99
+ | '*'
100
+ | 'activity.call'
101
+ | 'connection.changed'
102
+ | 'connection.indexed'
103
+ | 'review.changed'
104
+ | 'tool.definition-changed'
105
+ | 'tool.policy-changed';
106
+ export type WebhookEnvelope<T extends Exclude<WebhookEvent, '*'>, D> = {
107
+ data: D;
108
+ id: string;
109
+ occurredAt: string;
110
+ type: T;
111
+ };
112
+ export type WebhookPayload =
113
+ | WebhookEnvelope<
114
+ 'activity.call',
115
+ {
116
+ activityId: string;
117
+ calledAt: string;
118
+ connectionUid: string | null;
119
+ errorCode: string | null;
120
+ policyDecision: ToolDecision | null;
121
+ succeeded: boolean | null;
122
+ toolName: string;
123
+ }
124
+ >
125
+ | WebhookEnvelope<
126
+ 'connection.changed',
127
+ {
128
+ authType: Connection['authType'];
129
+ connectionUid: string;
130
+ enabled: boolean;
131
+ kind: Connection['kind'];
132
+ name: string;
133
+ }
134
+ >
135
+ | WebhookEnvelope<
136
+ 'connection.indexed',
137
+ {
138
+ connectionUid: string;
139
+ indexReferenceId: string;
140
+ status: string;
141
+ toolCount: number;
142
+ }
143
+ >
144
+ | WebhookEnvelope<
145
+ 'review.changed',
146
+ {
147
+ connectionUid: string;
148
+ reviewUid: string;
149
+ status: Review['status'];
150
+ toolUid: string;
151
+ }
152
+ >
153
+ | WebhookEnvelope<
154
+ 'tool.definition-changed',
155
+ { approved: boolean; connectionUid: string; kind: 'added' | 'changed'; toolUid: string }
156
+ >
157
+ | WebhookEnvelope<
158
+ 'tool.policy-changed',
159
+ {
160
+ connectionUid: string | null;
161
+ decision: ToolDecision | null;
162
+ eventType: string;
163
+ toolUid: string | null;
164
+ }
165
+ >;
166
+
167
+ export const CODE_RUN_AHEAD_META_KEY: 'io.rayrun/run-ahead-session';
168
+
169
+ export class RayrunApiError extends Error {
170
+ code: string;
171
+ requestId: string | null;
172
+ status: number;
173
+ }
174
+
175
+ export function verifyWebhookSignature(options: {
176
+ body: string;
177
+ now?: number;
178
+ secret: string;
179
+ signature: string;
180
+ toleranceSeconds?: number;
181
+ }): boolean;
182
+
183
+ export class Rayrun {
184
+ constructor(options: { apiKey: string; baseUrl?: string; fetch?: typeof fetch });
185
+ catalog: {
186
+ list(query?: PageQuery & { query?: string }): Promise<CursorPage<CatalogEntry>>;
187
+ };
188
+ connections: {
189
+ create(
190
+ body:
191
+ | {
192
+ kind: 'mcp';
193
+ name: string;
194
+ url: string;
195
+ description?: string | null;
196
+ transport?: 'sse' | 'streamable-http';
197
+ }
198
+ | { kind: 'openapi'; specificationUrl: string; baseUrl?: string; name?: string },
199
+ ): Promise<{ connection: { uid: string } }>;
200
+ createOAuthLink(uid: string): Promise<{ authorizationUrl: string }>;
201
+ delete(uid: string): Promise<void>;
202
+ index(uid: string): Promise<{ enqueued: boolean }>;
203
+ list(query?: PageQuery): Promise<CursorPage<Connection>>;
204
+ setCredential(uid: string, body: Credential): Promise<{ enqueued: boolean }>;
205
+ setEnabled(
206
+ uid: string,
207
+ enabled: boolean,
208
+ ): Promise<{ connection: Pick<Connection, 'displayName' | 'enabled' | 'uid'> }>;
209
+ setPolicy(uid: string, mode: ToolAccessMode): Promise<void>;
210
+ };
211
+ tools: {
212
+ list(query?: PageQuery & { connectionUid?: string }): Promise<CursorPage<Tool>>;
213
+ setPolicy(
214
+ connectionUid: string,
215
+ toolUid: string,
216
+ body: { decision: ToolDecision | null; riskConfirmed?: boolean; riskOverride?: Risk | null },
217
+ ): Promise<void>;
218
+ };
219
+ clients: {
220
+ list(query?: PageQuery): Promise<CursorPage<Client>>;
221
+ setPolicy(uid: string, mode: Exclude<ToolAccessMode, 'custom'>): Promise<void>;
222
+ setToolPolicy(
223
+ uid: string,
224
+ toolUid: string,
225
+ body: { decision: ToolDecision | null; riskConfirmed?: boolean },
226
+ ): Promise<void>;
227
+ };
228
+ activity: { list(query?: PageQuery): Promise<CursorPage<Activity>> };
229
+ reviews: { list(query?: PageQuery): Promise<CursorPage<Review>> };
230
+ webhooks: {
231
+ create(body: {
232
+ url: string;
233
+ description?: string | null;
234
+ eventTypes: WebhookEvent[];
235
+ }): Promise<{ uid: string; secret: string }>;
236
+ delete(uid: string): Promise<void>;
237
+ list(): Promise<{ items: Webhook[] }>;
238
+ verify: typeof verifyWebhookSignature;
239
+ };
240
+ request(
241
+ method: string,
242
+ path: string,
243
+ options?: { body?: unknown; query?: Record<string, unknown>; signal?: AbortSignal },
244
+ ): Promise<unknown>;
245
+ }
246
+
247
+ export class RayrunCodeRunAheadSession {
248
+ private constructor();
249
+ readonly expiresAt: string;
250
+ readonly sessionId: string;
251
+ readonly meta: { readonly 'io.rayrun/run-ahead-session': string } | Record<string, never>;
252
+ feedArguments(rawArguments: string, options?: { signal?: AbortSignal }): Promise<void>;
253
+ flush(): Promise<void>;
254
+ close(options?: { signal?: AbortSignal }): Promise<void>;
255
+ }
256
+
257
+ export class RayrunGateway {
258
+ constructor(options: { accessToken: string; baseUrl?: string; fetch?: typeof fetch });
259
+ codeRunAhead: {
260
+ open(options?: {
261
+ services?: readonly string[];
262
+ signal?: AbortSignal;
263
+ }): Promise<RayrunCodeRunAheadSession | null>;
264
+ };
265
+ }
package/index.js ADDED
@@ -0,0 +1,325 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+
3
+ export class RayrunApiError extends Error {
4
+ constructor(message, { code, requestId, status }) {
5
+ super(message);
6
+ this.name = 'RayrunApiError';
7
+ this.code = code;
8
+ this.requestId = requestId;
9
+ this.status = status;
10
+ }
11
+ }
12
+
13
+ export const CODE_RUN_AHEAD_META_KEY = 'io.rayrun/run-ahead-session';
14
+
15
+ const isRunAheadAvailabilityError = (error) => {
16
+ return (
17
+ error instanceof TypeError ||
18
+ (error instanceof RayrunApiError &&
19
+ (error.status === 404 || error.status === 409 || error.status === 429 || error.status >= 500))
20
+ );
21
+ };
22
+
23
+ const withoutUndefined = (value) => {
24
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
25
+ };
26
+
27
+ const retryDelayMilliseconds = (retryAfter, attempt) => {
28
+ if (retryAfter) {
29
+ const seconds = Number(retryAfter);
30
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1_000;
31
+
32
+ const date = Date.parse(retryAfter);
33
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
34
+ }
35
+
36
+ const base = 250 * 2 ** attempt;
37
+ return base + Math.floor(Math.random() * Math.max(1, base / 4));
38
+ };
39
+
40
+ const wait = async (milliseconds, signal) => {
41
+ await new Promise((resolve, reject) => {
42
+ const finish = () => {
43
+ signal?.removeEventListener('abort', onAbort);
44
+ resolve();
45
+ };
46
+ const onAbort = () => {
47
+ clearTimeout(timeout);
48
+ signal?.removeEventListener('abort', onAbort);
49
+ reject(signal.reason);
50
+ };
51
+ const timeout = setTimeout(finish, milliseconds);
52
+
53
+ if (signal?.aborted) return onAbort();
54
+ signal?.addEventListener('abort', onAbort, { once: true });
55
+ });
56
+ };
57
+
58
+ export const verifyWebhookSignature = ({
59
+ body,
60
+ now = Date.now(),
61
+ secret,
62
+ signature,
63
+ toleranceSeconds = 300,
64
+ }) => {
65
+ const fields = new Map(signature.split(',').map((field) => field.split('=', 2)));
66
+ const timestamp = fields.get('t');
67
+ const suppliedDigest = fields.get('v1');
68
+ if (
69
+ !timestamp ||
70
+ !suppliedDigest ||
71
+ !/^\d+$/u.test(timestamp) ||
72
+ !/^[0-9a-f]{64}$/u.test(suppliedDigest)
73
+ ) {
74
+ return false;
75
+ }
76
+ if (Math.abs(Math.floor(now / 1_000) - Number(timestamp)) > toleranceSeconds) return false;
77
+
78
+ const expectedDigest = createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
79
+ return timingSafeEqual(Buffer.from(suppliedDigest, 'hex'), Buffer.from(expectedDigest, 'hex'));
80
+ };
81
+
82
+ export class Rayrun {
83
+ constructor({
84
+ apiKey,
85
+ baseUrl = 'https://ray.run',
86
+ fetch: fetchImplementation = globalThis.fetch,
87
+ }) {
88
+ if (!apiKey) throw new TypeError('Rayrun requires an apiKey.');
89
+ if (!fetchImplementation) throw new TypeError('Rayrun requires a fetch implementation.');
90
+
91
+ this.apiKey = apiKey;
92
+ this.baseUrl = baseUrl.replace(/\/$/u, '');
93
+ this.fetch = fetchImplementation;
94
+
95
+ this.catalog = { list: (query) => this.request('GET', '/catalog', { query }) };
96
+ this.connections = {
97
+ create: (body) => this.request('POST', '/connections', { body }),
98
+ createOAuthLink: (uid) => this.request('POST', `/connections/${uid}/oauth-link`),
99
+ delete: (uid) => this.request('DELETE', `/connections/${uid}`),
100
+ index: (uid) => this.request('POST', `/connections/${uid}/index`),
101
+ list: (query) => this.request('GET', '/connections', { query }),
102
+ setCredential: (uid, body) => this.request('PUT', `/connections/${uid}/credential`, { body }),
103
+ setEnabled: (uid, enabled) =>
104
+ this.request('PATCH', `/connections/${uid}`, { body: { enabled } }),
105
+ setPolicy: (uid, mode) =>
106
+ this.request('PATCH', `/connections/${uid}/policy`, { body: { mode } }),
107
+ };
108
+ this.tools = {
109
+ list: (query) => this.request('GET', '/tools', { query }),
110
+ setPolicy: (connectionUid, toolUid, body) =>
111
+ this.request('PATCH', `/connections/${connectionUid}/tools/${toolUid}/policy`, { body }),
112
+ };
113
+ this.clients = {
114
+ list: (query) => this.request('GET', '/clients', { query }),
115
+ setPolicy: (uid, mode) => this.request('PATCH', `/clients/${uid}/policy`, { body: { mode } }),
116
+ setToolPolicy: (uid, toolUid, body) =>
117
+ this.request('PATCH', `/clients/${uid}/tools/${toolUid}/policy`, { body }),
118
+ };
119
+ this.activity = { list: (query) => this.request('GET', '/activity', { query }) };
120
+ this.reviews = { list: (query) => this.request('GET', '/reviews', { query }) };
121
+ this.webhooks = {
122
+ create: (body) => this.request('POST', '/webhooks', { body }),
123
+ delete: (uid) => this.request('DELETE', `/webhooks/${uid}`),
124
+ list: () => this.request('GET', '/webhooks'),
125
+ verify: verifyWebhookSignature,
126
+ };
127
+ }
128
+
129
+ async request(method, path, { body, query, signal } = {}) {
130
+ const url = new URL(`${this.baseUrl}/v1${path}`);
131
+ for (const [name, value] of Object.entries(withoutUndefined(query ?? {}))) {
132
+ url.searchParams.set(name, String(value));
133
+ }
134
+
135
+ const retries = method === 'GET' ? 2 : 0;
136
+ for (let attempt = 0; ; attempt += 1) {
137
+ let response;
138
+
139
+ try {
140
+ response = await this.fetch(url, {
141
+ body: body === undefined ? undefined : JSON.stringify(body),
142
+ headers: withoutUndefined({
143
+ authorization: `Bearer ${this.apiKey}`,
144
+ 'content-type': body === undefined ? undefined : 'application/json',
145
+ 'user-agent': '@rayrun/sdk',
146
+ }),
147
+ method,
148
+ signal,
149
+ });
150
+ } catch (error) {
151
+ if (attempt >= retries || signal?.aborted || error?.name === 'AbortError') throw error;
152
+ await wait(retryDelayMilliseconds(null, attempt), signal);
153
+ continue;
154
+ }
155
+
156
+ if ((response.status === 429 || response.status >= 500) && attempt < retries) {
157
+ const retryAfter = response.headers.get('retry-after');
158
+ await Promise.resolve(response.body?.cancel()).catch(() => {});
159
+ await wait(retryDelayMilliseconds(retryAfter, attempt), signal);
160
+ continue;
161
+ }
162
+
163
+ if (!response.ok) {
164
+ const problem = await response.json().catch(() => null);
165
+ throw new RayrunApiError(
166
+ problem?.error?.message ?? `Rayrun API request failed with HTTP ${response.status}.`,
167
+ {
168
+ code: problem?.error?.code ?? 'request_failed',
169
+ requestId: problem?.error?.requestId ?? response.headers.get('rayrun-request-id'),
170
+ status: response.status,
171
+ },
172
+ );
173
+ }
174
+
175
+ return response.status === 204 ? undefined : response.json();
176
+ }
177
+ }
178
+ }
179
+
180
+ export class RayrunCodeRunAheadSession {
181
+ constructor({ gateway, sessionId, expiresAt }) {
182
+ this.gateway = gateway;
183
+ this.sessionId = sessionId;
184
+ this.expiresAt = expiresAt;
185
+ this.active = true;
186
+ this.latestArguments = '';
187
+ this.sentArguments = '';
188
+ this.pending = Promise.resolve();
189
+ this.draining = false;
190
+ }
191
+
192
+ get meta() {
193
+ return this.active ? { [CODE_RUN_AHEAD_META_KEY]: this.sessionId } : {};
194
+ }
195
+
196
+ feedArguments(rawArguments, { signal } = {}) {
197
+ if (typeof rawArguments !== 'string') {
198
+ throw new TypeError('Run-ahead arguments must be a JSON string snapshot.');
199
+ }
200
+
201
+ if (rawArguments.length <= this.latestArguments.length) return this.pending;
202
+
203
+ this.latestArguments = rawArguments;
204
+ if (this.draining) return this.pending;
205
+
206
+ this.draining = true;
207
+ this.pending = this.pending
208
+ .then(async () => {
209
+ while (this.active && this.sentArguments.length < this.latestArguments.length) {
210
+ const snapshot = this.latestArguments;
211
+
212
+ this.sentArguments = snapshot;
213
+
214
+ try {
215
+ await this.gateway.request({
216
+ body: { arguments: snapshot, operation: 'feed', sessionId: this.sessionId },
217
+ signal,
218
+ });
219
+ } catch (error) {
220
+ if (!isRunAheadAvailabilityError(error)) throw error;
221
+
222
+ this.active = false;
223
+ }
224
+ }
225
+ })
226
+ .finally(() => {
227
+ this.draining = false;
228
+ });
229
+ // Harnesses commonly enqueue snapshots without awaiting each one, then call flush() once.
230
+ // Attach a handler now so a contract/authentication error cannot become an unhandled rejection
231
+ // before flush() observes and rethrows the original rejected promise.
232
+ void this.pending.catch(() => {});
233
+
234
+ return this.pending;
235
+ }
236
+
237
+ async flush() {
238
+ try {
239
+ await this.pending;
240
+ } catch (error) {
241
+ if (isRunAheadAvailabilityError(error)) {
242
+ this.active = false;
243
+ return;
244
+ }
245
+
246
+ throw error;
247
+ }
248
+ }
249
+
250
+ async close({ signal } = {}) {
251
+ await this.pending.catch(() => {});
252
+ if (!this.active) return;
253
+
254
+ try {
255
+ await this.gateway.request({
256
+ body: { operation: 'close', sessionId: this.sessionId },
257
+ signal,
258
+ });
259
+ } catch {
260
+ // Closing is best-effort. The server expires abandoned sessions after two minutes, and an
261
+ // optional latency optimization must never replace or mask the authoritative MCP result.
262
+ } finally {
263
+ this.active = false;
264
+ }
265
+ }
266
+ }
267
+
268
+ export class RayrunGateway {
269
+ constructor({
270
+ accessToken,
271
+ baseUrl = 'https://ray.run',
272
+ fetch: fetchImplementation = globalThis.fetch,
273
+ }) {
274
+ if (!accessToken) throw new TypeError('RayrunGateway requires an accessToken.');
275
+ if (!fetchImplementation) throw new TypeError('RayrunGateway requires a fetch implementation.');
276
+
277
+ this.accessToken = accessToken;
278
+ this.baseUrl = baseUrl.replace(/\/$/u, '');
279
+ this.fetch = fetchImplementation;
280
+ this.codeRunAhead = {
281
+ open: async ({ services, signal } = {}) => {
282
+ try {
283
+ const created = await this.request({
284
+ body: withoutUndefined({ operation: 'start', services }),
285
+ signal,
286
+ });
287
+
288
+ return new RayrunCodeRunAheadSession({ gateway: this, ...created });
289
+ } catch (error) {
290
+ if (isRunAheadAvailabilityError(error)) return null;
291
+ throw error;
292
+ }
293
+ },
294
+ };
295
+ }
296
+
297
+ async request({ body, signal }) {
298
+ const response = await this.fetch(`${this.baseUrl}/mcp/run-ahead`, {
299
+ body: JSON.stringify(body),
300
+ headers: {
301
+ authorization: `Bearer ${this.accessToken}`,
302
+ 'content-type': 'application/json',
303
+ 'user-agent': '@rayrun/sdk',
304
+ },
305
+ method: 'POST',
306
+ signal,
307
+ });
308
+
309
+ if (!response.ok) {
310
+ const problem = await response.json().catch(() => null);
311
+ throw new RayrunApiError(
312
+ problem?.error_description ??
313
+ problem?.error?.message ??
314
+ `Rayrun gateway request failed with HTTP ${response.status}.`,
315
+ {
316
+ code: problem?.error?.code ?? problem?.error ?? 'request_failed',
317
+ requestId: problem?.error?.requestId ?? response.headers.get('rayrun-request-id'),
318
+ status: response.status,
319
+ },
320
+ );
321
+ }
322
+
323
+ return response.status === 204 ? undefined : response.json();
324
+ }
325
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@rayrun/sdk",
3
+ "version": "0.2.0",
4
+ "description": "Type-safe client for the Rayrun public API",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/rayrundev/rayrun.git",
9
+ "directory": "packages/sdk"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "index.d.ts",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "type": "module",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./index.d.ts",
21
+ "import": "./index.js"
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "test": "vitest run"
29
+ },
30
+ "devDependencies": {
31
+ "vitest": "^4.1.10"
32
+ },
33
+ "engines": {
34
+ "node": ">=20"
35
+ }
36
+ }