UNPKG

@relaycast/sdk

Version:

TypeScript SDK for [Relaycast](https://relaycast.dev) — headless Slack for AI agents: channels, threads, DMs, reactions, files, search, and realtime events.

221 lines 9.07 kB
import { z } from 'zod'; import { ApiErrorSchema } from '@relaycast/types'; import { SDK_VERSION } from './version.js'; import { AGENT_RELAY_DISTINCT_ID_HEADER, ORIGIN_ACTOR_HEADER, SDK_ORIGIN, sanitizeAgentRelayDistinctId, sanitizeOriginActor, } from './origin.js'; import { camelizeKeys, decamelizeKey, decamelizeKeys } from './casing.js'; import { RelayError, relayErrorFromApi } from './errors.js'; const INTERNAL_ORIGIN = Symbol('relaycast.internal.origin'); function readInternalOrigin(options) { return options[INTERNAL_ORIGIN]; } export function withInternalOrigin(options, origin) { const copy = { ...options }; Object.defineProperty(copy, INTERNAL_ORIGIN, { value: origin, enumerable: false, writable: false, configurable: false, }); return copy; } export { RelayError } from './errors.js'; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } const DEFAULT_RETRY_POLICY = { maxRetries: 3, backoffMs: 1000, backoffMultiplier: 2, jitter: true, retryOn: [429, 500, 502, 503, 504], }; function normalizeRetryPolicy(input) { return { maxRetries: Number.isFinite(input?.maxRetries) ? Math.max(0, Math.floor(input.maxRetries)) : DEFAULT_RETRY_POLICY.maxRetries, backoffMs: Number.isFinite(input?.backoffMs) ? Math.max(0, Math.floor(input.backoffMs)) : DEFAULT_RETRY_POLICY.backoffMs, backoffMultiplier: Number.isFinite(input?.backoffMultiplier) ? Math.max(1, input.backoffMultiplier) : DEFAULT_RETRY_POLICY.backoffMultiplier, jitter: typeof input?.jitter === 'boolean' ? input.jitter : DEFAULT_RETRY_POLICY.jitter, retryOn: Array.isArray(input?.retryOn) ? input.retryOn.filter((status) => Number.isInteger(status) && status >= 100) : [...DEFAULT_RETRY_POLICY.retryOn], }; } function computeBackoffMs(policy, retryAttempt) { const exponential = policy.backoffMs * (policy.backoffMultiplier ** retryAttempt); if (!policy.jitter) return Math.max(0, Math.round(exponential)); const jitterFactor = 0.5 + Math.random(); return Math.max(0, Math.round(exponential * jitterFactor)); } function parseRetryAfterMs(res) { const header = res.headers.get('Retry-After'); if (!header) return null; const asSeconds = Number(header); if (Number.isFinite(asSeconds)) { return Math.max(0, Math.round(asSeconds * 1000)); } const asDate = Date.parse(header); if (Number.isNaN(asDate)) return null; return Math.max(0, asDate - Date.now()); } const apiEnvelopeSchema = z.object({ ok: z.boolean(), data: z.unknown().optional(), cursor: z.object({ next: z.string().nullable(), has_more: z.boolean(), }).optional(), error: z.object({ code: z.string(), message: z.string(), }).optional(), }); export class HttpClient { _apiKey; _baseUrl; _originClient; _originVersion; _originActor; _agentRelayDistinctId; _retryPolicy; constructor(options) { const origin = readInternalOrigin(options) ?? SDK_ORIGIN; this._apiKey = options.apiKey; this._baseUrl = options.baseUrl ?? 'https://gateway.relaycast.dev'; this._originClient = origin.client; this._originVersion = origin.version; // A wrapping host's internal origin is authoritative about the originActor; // fall back to the public `originActor` option for plain consumers. this._originActor = sanitizeOriginActor(origin.originActor ?? options.originActor); this._agentRelayDistinctId = sanitizeAgentRelayDistinctId(origin.agentRelayDistinctId ?? options.agentRelayDistinctId); this._retryPolicy = normalizeRetryPolicy(options.retryPolicy); } get apiKey() { return this._apiKey; } get baseUrl() { return this._baseUrl; } get originClient() { return this._originClient; } get originVersion() { return this._originVersion; } /** Sanitized originActor identifier, or `undefined` when none was supplied. */ get originActor() { return this._originActor; } /** Sanitized Agent Relay distinct id, or `undefined` when none was supplied. */ get agentRelayDistinctId() { return this._agentRelayDistinctId; } get retryPolicy() { return this._retryPolicy; } withApiKey(apiKey) { return new HttpClient(withInternalOrigin({ apiKey, baseUrl: this._baseUrl, retryPolicy: this._retryPolicy }, { client: this._originClient, version: this._originVersion, ...(this._originActor ? { originActor: this._originActor } : {}), ...(this._agentRelayDistinctId ? { agentRelayDistinctId: this._agentRelayDistinctId } : {}), })); } async request(method, path, body, query, options) { const url = new URL(path, this._baseUrl); if (query) { for (const [k, v] of Object.entries(query)) { if (v !== undefined) url.searchParams.set(decamelizeKey(k), v); } } const headers = { Authorization: `Bearer ${this._apiKey}`, 'X-SDK-Version': SDK_VERSION, 'X-Relaycast-Origin-Client': this._originClient, 'X-Relaycast-Origin-Version': this._originVersion, ...(this._originActor ? { [ORIGIN_ACTOR_HEADER]: this._originActor } : {}), ...(this._agentRelayDistinctId ? { [AGENT_RELAY_DISTINCT_ID_HEADER]: this._agentRelayDistinctId } : {}), ...(options?.headers || {}), }; const hasBody = body !== undefined && method.toUpperCase() !== 'GET'; if (hasBody) headers['Content-Type'] = 'application/json'; const wireBody = hasBody ? decamelizeKeys(body) : undefined; let attempt = 0; while (true) { let res; try { res = await fetch(url.toString(), { method, headers, body: hasBody ? JSON.stringify(wireBody) : undefined, }); } catch (err) { if (attempt < this._retryPolicy.maxRetries) { const waitMs = computeBackoffMs(this._retryPolicy, attempt); attempt += 1; await sleep(waitMs); continue; } throw new RelayError('transport_error', `Network request failed: ${err instanceof Error ? err.message : 'unknown error'}`, { retryable: true, cause: err }); } if (this._retryPolicy.retryOn.includes(res.status) && attempt < this._retryPolicy.maxRetries) { const waitMs = res.status === 429 ? parseRetryAfterMs(res) ?? computeBackoffMs(this._retryPolicy, attempt) : computeBackoffMs(this._retryPolicy, attempt); attempt += 1; await sleep(waitMs); continue; } // 204 No Content — return undefined (used by DELETE endpoints) if (res.status === 204) { return undefined; } let json; try { json = await res.json(); } catch (err) { throw new RelayError('transport_error', `Failed to parse response as JSON: ${err instanceof Error ? err.message : 'unknown error'}`, { statusCode: res.status, retryable: false, cause: err }); } const envelope = apiEnvelopeSchema.safeParse(json); if (!envelope.success) { throw new RelayError('transport_error', 'Invalid API response', { statusCode: res.status, retryable: false, }); } if (!envelope.data.ok) { const errParsed = ApiErrorSchema.safeParse(json); const code = errParsed.success ? errParsed.data.error.code : 'unknown_error'; const message = errParsed.success ? errParsed.data.error.message : 'Unknown error'; throw relayErrorFromApi(code, message, res.status); } const data = envelope.data.data; const parsedData = options?.schema ? options.schema.parse(data) : data; return camelizeKeys(parsedData); } } get(path, query, options) { return this.request('GET', path, undefined, query, options); } post(path, body, options) { return this.request('POST', path, body, undefined, options); } patch(path, body, options) { return this.request('PATCH', path, body, undefined, options); } put(path, body, options) { return this.request('PUT', path, body, undefined, options); } async delete(path, options) { await this.request('DELETE', path, undefined, undefined, options); } } //# sourceMappingURL=client.js.map