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.

246 lines 9.91 kB
import { z } from 'zod'; import { ApiErrorSchema } from '@relaycast/types'; import { SDK_VERSION } from './version.js'; import { ORIGIN_ACTOR_HEADER, SDK_ORIGIN, agentRelayIdentityHeaders, agentRelayIdentityOrigin, resolveAgentRelayIdentity, 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; _identity; _retryPolicy; constructor(options) { const origin = readInternalOrigin(options) ?? SDK_ORIGIN; this._apiKey = options.apiKey; this._baseUrl = options.baseUrl ?? 'https://cast.agentrelay.com'; 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); // A wrapping host's internal origin wins over the public options. this._identity = resolveAgentRelayIdentity(origin, options); 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._identity.distinctId; } /** Sanitized Agent Relay Cloud user id, or `undefined` when none was supplied. */ get agentRelayUserId() { return this._identity.userId; } /** Sanitized hashed machine id, or `undefined` when none was supplied. */ get agentRelayMachineId() { return this._identity.machineId; } /** Sanitized Agent Relay Cloud organization id, or `undefined`. */ get agentRelayOrgId() { return this._identity.orgId; } /** Sanitized Agent Relay Cloud organization slug, or `undefined`. */ get agentRelayOrgSlug() { return this._identity.orgSlug; } get retryPolicy() { return this._retryPolicy; } /** * This client's full origin — client/version, origin actor, and every * identity dimension — for handing to a derived HTTP or WebSocket client. * Single source so a newly added dimension reaches every socket at once. */ get internalOrigin() { return { client: this._originClient, version: this._originVersion, ...(this._originActor ? { originActor: this._originActor } : {}), ...agentRelayIdentityOrigin(this._identity), }; } withApiKey(apiKey) { return new HttpClient(withInternalOrigin({ apiKey, baseUrl: this._baseUrl, retryPolicy: this._retryPolicy }, this.internalOrigin)); } 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 } : {}), ...agentRelayIdentityHeaders(this._identity), ...(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