@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
JavaScript
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