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.

155 lines 6.94 kB
import { SDK_VERSION } from './version.js'; export const SDK_ORIGIN = Object.freeze({ client: '@relaycast/sdk', version: SDK_VERSION, }); /** * HTTP header (and WS query param) used to tell the relaycast server which * originActor is driving the request. Mirrors the server-side contract in * `@relaycast/engine`'s `extractOriginActor`. */ export const ORIGIN_ACTOR_HEADER = 'X-Relaycast-Origin-Actor'; export const AGENT_RELAY_DISTINCT_ID_HEADER = 'X-Agent-Relay-Distinct-Id'; export const AGENT_RELAY_DISTINCT_ID_QUERY = 'agent_relay_distinct_id'; export const AGENT_RELAY_MACHINE_ID_HEADER = 'X-Agent-Relay-Machine-Id'; export const AGENT_RELAY_MACHINE_ID_QUERY = 'agent_relay_machine_id'; export const AGENT_RELAY_USER_ID_HEADER = 'X-Agent-Relay-User-Id'; export const AGENT_RELAY_USER_ID_QUERY = 'agent_relay_user_id'; export const AGENT_RELAY_ORG_ID_HEADER = 'X-Agent-Relay-Org-Id'; export const AGENT_RELAY_ORG_ID_QUERY = 'agent_relay_org_id'; export const AGENT_RELAY_ORG_SLUG_HEADER = 'X-Agent-Relay-Org-Slug'; export const AGENT_RELAY_ORG_SLUG_QUERY = 'agent_relay_org_slug'; /** Upper bound on the originActor identifier — generous enough for a UA-style token. */ const ORIGIN_ACTOR_MAX_LENGTH = 128; const AGENT_RELAY_DISTINCT_ID_MAX_LENGTH = 128; /** * Characters permitted in a originActor identifier. Deliberately broad enough for a * User-Agent-style token (`name/version (model=...; setting)`) while excluding * CR/LF and other control characters — dropping those is what keeps a malformed * upstream value from smuggling a header injection past the relaycast WAF. */ const ORIGIN_ACTOR_ALLOWED = /^[a-z0-9 ._\-/():=;,+@]+$/i; const AGENT_RELAY_DISTINCT_ID_ALLOWED = /^[a-z0-9._:-]+$/i; /** * Normalize a caller-supplied originActor identifier to the wire contract. * * Returns a lowercased, length-capped token, or `undefined` when the input is * empty or contains disallowed characters — in which case callers omit the * header entirely rather than sending garbage the server would reject anyway. */ export function sanitizeOriginActor(raw) { if (!raw) return undefined; const trimmed = raw.trim(); if (!trimmed) return undefined; if (!ORIGIN_ACTOR_ALLOWED.test(trimmed)) return undefined; return trimmed.slice(0, ORIGIN_ACTOR_MAX_LENGTH).toLowerCase(); } export function sanitizeAgentRelayDistinctId(raw) { if (!raw) return undefined; const trimmed = raw.trim(); if (!trimmed) return undefined; if (!AGENT_RELAY_DISTINCT_ID_ALLOWED.test(trimmed)) return undefined; return trimmed.slice(0, AGENT_RELAY_DISTINCT_ID_MAX_LENGTH); } /** Identity ids share the distinct-id contract: same charset, same length cap. */ export const sanitizeAgentRelayUserId = sanitizeAgentRelayDistinctId; export const sanitizeAgentRelayMachineId = sanitizeAgentRelayDistinctId; export const sanitizeAgentRelayOrgId = sanitizeAgentRelayDistinctId; export function sanitizeAgentRelayOrgSlug(raw) { if (!raw) return undefined; const trimmed = raw.trim(); if (!trimmed) return undefined; if (!AGENT_RELAY_DISTINCT_ID_ALLOWED.test(trimmed)) return undefined; return trimmed.slice(0, 120); } export function resolveAgentRelayIdentity(...sources) { /** * First candidate that survives sanitization wins. * * Sanitizing inside the loop rather than after it matters: a malformed * higher-priority value would otherwise shadow a valid lower-priority one and * drop the dimension entirely, so a wrapping host with a bad internal value * silently lost identity the caller had supplied correctly. */ const pick = (key, sanitize) => { for (const source of sources) { const value = source?.[key]; if (typeof value !== 'string') continue; const sanitized = sanitize(value); if (sanitized) return sanitized; } return undefined; }; const userId = pick('agentRelayUserId', sanitizeAgentRelayUserId); const machineId = pick('agentRelayMachineId', sanitizeAgentRelayMachineId); const orgId = pick('agentRelayOrgId', sanitizeAgentRelayOrgId); const orgSlug = pick('agentRelayOrgSlug', sanitizeAgentRelayOrgSlug); const distinctId = pick('agentRelayDistinctId', sanitizeAgentRelayDistinctId) ?? userId ?? machineId; return { ...(distinctId ? { distinctId } : {}), ...(machineId ? { machineId } : {}), ...(userId ? { userId } : {}), ...(orgId ? { orgId } : {}), ...(orgSlug ? { orgSlug } : {}), }; } /** * Project a resolved identity back onto the {@link InternalOrigin} fields. * * Every place that hands identity to another client (`withApiKey`, and the * WebSocket clients built by `RelayCast` and `AgentClient`) goes through this * rather than spreading the fields by hand — that duplication is how the agent * socket silently missed the user/machine/org dimensions when they were added. */ export function agentRelayIdentityOrigin(identity) { return { ...(identity.distinctId ? { agentRelayDistinctId: identity.distinctId } : {}), ...(identity.machineId ? { agentRelayMachineId: identity.machineId } : {}), ...(identity.userId ? { agentRelayUserId: identity.userId } : {}), ...(identity.orgId ? { agentRelayOrgId: identity.orgId } : {}), ...(identity.orgSlug ? { agentRelayOrgSlug: identity.orgSlug } : {}), }; } /** Identity headers for an HTTP request. Omits anything unset. */ export function agentRelayIdentityHeaders(identity) { return { ...(identity.distinctId ? { [AGENT_RELAY_DISTINCT_ID_HEADER]: identity.distinctId } : {}), ...(identity.machineId ? { [AGENT_RELAY_MACHINE_ID_HEADER]: identity.machineId } : {}), ...(identity.userId ? { [AGENT_RELAY_USER_ID_HEADER]: identity.userId } : {}), ...(identity.orgId ? { [AGENT_RELAY_ORG_ID_HEADER]: identity.orgId } : {}), ...(identity.orgSlug ? { [AGENT_RELAY_ORG_SLUG_HEADER]: identity.orgSlug } : {}), }; } /** * Identity query params for a WebSocket upgrade — browsers can't set custom * headers on a WS handshake, so the same values ride the query string. */ export function applyAgentRelayIdentityQuery(url, identity) { if (identity.distinctId) { url.searchParams.set(AGENT_RELAY_DISTINCT_ID_QUERY, identity.distinctId); } if (identity.machineId) { url.searchParams.set(AGENT_RELAY_MACHINE_ID_QUERY, identity.machineId); } if (identity.userId) { url.searchParams.set(AGENT_RELAY_USER_ID_QUERY, identity.userId); } if (identity.orgId) { url.searchParams.set(AGENT_RELAY_ORG_ID_QUERY, identity.orgId); } if (identity.orgSlug) { url.searchParams.set(AGENT_RELAY_ORG_SLUG_QUERY, identity.orgSlug); } } //# sourceMappingURL=origin.js.map