UNPKG

@sgnl-ai/set-transmitter

Version:

HTTP transmission library for Security Event Tokens (SET) with CAEP/SSF support

300 lines (293 loc) 9.58 kB
'use strict'; // src/types.ts var DEFAULT_RETRY_CONFIG = { maxAttempts: 3, retryableStatuses: [429, 502, 503, 504], backoffMs: 1e3, maxBackoffMs: 1e4, backoffMultiplier: 2 }; var DEFAULT_OPTIONS = { timeout: 3e4, parseResponse: true, validateStatus: (status) => status < 400}; // src/constants.ts var EventTypes = { SESSION_REVOKED: "https://schemas.openid.net/secevent/caep/event-type/session-revoked", TOKEN_CLAIMS_CHANGE: "https://schemas.openid.net/secevent/caep/event-type/token-claims-change", CREDENTIAL_CHANGE: "https://schemas.openid.net/secevent/caep/event-type/credential-change", ASSURANCE_LEVEL_CHANGE: "https://schemas.openid.net/secevent/caep/event-type/assurance-level-change", DEVICE_COMPLIANCE_CHANGE: "https://schemas.openid.net/secevent/caep/event-type/device-compliance-change" }; var CONTENT_TYPE_SET = "application/secevent+jwt"; var CONTENT_TYPE_JSON = "application/json"; var DEFAULT_USER_AGENT = "SGNL-Action-Framework/1.0"; // src/errors.ts var TransmissionError = class _TransmissionError extends Error { constructor(message, statusCode, retryable = false, responseBody, responseHeaders) { super(message); this.statusCode = statusCode; this.retryable = retryable; this.responseBody = responseBody; this.responseHeaders = responseHeaders; this.name = "TransmissionError"; Object.setPrototypeOf(this, _TransmissionError.prototype); } }; var TimeoutError = class _TimeoutError extends TransmissionError { constructor(message, timeout) { super(`${message} (timeout: ${timeout}ms)`, void 0, true); this.name = "TimeoutError"; Object.setPrototypeOf(this, _TimeoutError.prototype); } }; var NetworkError = class _NetworkError extends TransmissionError { constructor(message, cause) { super(message, void 0, true); this.name = "NetworkError"; if (cause) { this.cause = cause; } Object.setPrototypeOf(this, _NetworkError.prototype); } }; var ValidationError = class _ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; Object.setPrototypeOf(this, _ValidationError.prototype); } }; // src/retry.ts function calculateBackoff(attempt, config, retryAfterMs) { if (retryAfterMs !== void 0 && retryAfterMs > 0) { return Math.min(retryAfterMs, config.maxBackoffMs); } const exponentialDelay = config.backoffMs * Math.pow(config.backoffMultiplier, attempt - 1); const clampedDelay = Math.min(exponentialDelay, config.maxBackoffMs); const jitter = clampedDelay * 0.25; const minDelay = clampedDelay - jitter; const maxDelay = clampedDelay + jitter; return Math.floor(Math.random() * (maxDelay - minDelay) + minDelay); } function parseRetryAfter(retryAfterHeader) { if (!retryAfterHeader) { return void 0; } const delaySeconds = parseInt(retryAfterHeader, 10); if (!isNaN(delaySeconds)) { return delaySeconds * 1e3; } const retryDate = new Date(retryAfterHeader); if (!isNaN(retryDate.getTime())) { const delayMs = retryDate.getTime() - Date.now(); return delayMs > 0 ? delayMs : void 0; } return void 0; } function isRetryableStatus(statusCode, retryableStatuses) { return retryableStatuses.includes(statusCode); } function shouldRetry(statusCode, attempt, config) { if (attempt >= config.maxAttempts) { return false; } if (statusCode === void 0) { return true; } return isRetryableStatus(statusCode, config.retryableStatuses); } async function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } // src/utils.ts function isValidSET(jwt) { if (typeof jwt !== "string") { return false; } const parts = jwt.split("."); if (parts.length !== 3) { return false; } const base64urlRegex = /^[A-Za-z0-9_-]+$/; return parts.every((part) => base64urlRegex.test(part)); } function normalizeAuthToken(token) { if (!token) { return void 0; } if (token.startsWith("Bearer ")) { return token; } return `Bearer ${token}`; } function mergeHeaders(defaultHeaders, customHeaders) { return { ...defaultHeaders, ...customHeaders }; } function parseResponseHeaders(headers) { const result = {}; headers.forEach((value, key) => { result[key] = value; }); return result; } async function parseResponseBody(response, parseJson) { const text = await response.text(); if (!parseJson || !text) { return text; } const contentType = response.headers.get("content-type"); if (contentType?.includes("application/json")) { try { return JSON.parse(text); } catch { return text; } } return text; } // src/transmitter.ts async function transmitSET(jwt, url, options = {}) { if (!isValidSET(jwt)) { throw new ValidationError("Invalid SET format: JWT must be in format header.payload.signature"); } let parsedUrl; try { parsedUrl = new URL(url); } catch { throw new ValidationError(`Invalid URL: ${url}`); } const mergedOptions = { authToken: options.authToken, headers: options.headers || {}, timeout: options.timeout ?? DEFAULT_OPTIONS.timeout, parseResponse: options.parseResponse ?? DEFAULT_OPTIONS.parseResponse, validateStatus: options.validateStatus ?? DEFAULT_OPTIONS.validateStatus, retry: { ...DEFAULT_RETRY_CONFIG, ...options.retry || {} } }; const baseHeaders = { "Content-Type": CONTENT_TYPE_SET, Accept: CONTENT_TYPE_JSON, "User-Agent": DEFAULT_USER_AGENT }; const authToken = normalizeAuthToken(mergedOptions.authToken); if (authToken) { baseHeaders["Authorization"] = authToken; } const headers = mergeHeaders(baseHeaders, mergedOptions.headers); let lastError; let lastResponse; for (let attempt = 1; attempt <= mergedOptions.retry.maxAttempts; attempt++) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), mergedOptions.timeout); try { const response = await fetch(parsedUrl.toString(), { method: "POST", headers, body: jwt, signal: controller.signal }); clearTimeout(timeoutId); lastResponse = response; const responseHeaders = parseResponseHeaders(response.headers); const responseBody = await parseResponseBody(response, mergedOptions.parseResponse); const isSuccess = mergedOptions.validateStatus(response.status); if (isSuccess) { return { status: "success", statusCode: response.status, body: responseBody, headers: responseHeaders }; } const canRetry = shouldRetry(response.status, attempt, mergedOptions.retry); if (!canRetry) { return { status: "failed", statusCode: response.status, body: responseBody, headers: responseHeaders, error: `HTTP ${response.status}: ${response.statusText}`, retryable: mergedOptions.retry.retryableStatuses.includes(response.status) }; } const retryAfterMs = parseRetryAfter(responseHeaders["retry-after"]); const backoffMs = calculateBackoff(attempt, mergedOptions.retry, retryAfterMs); await delay(backoffMs); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error) { if (error.name === "AbortError") { lastError = new TimeoutError("Request timed out", mergedOptions.timeout); } else { lastError = new NetworkError(`Network error: ${error.message}`, error); } } else { lastError = new NetworkError("Unknown network error"); } if (!shouldRetry(void 0, attempt, mergedOptions.retry)) { throw lastError; } const backoffMs = calculateBackoff(attempt, mergedOptions.retry); await delay(backoffMs); } } catch (error) { if (error instanceof ValidationError) { throw error; } lastError = error instanceof Error ? error : new Error(String(error)); } } if (lastResponse) { const responseHeaders = parseResponseHeaders(lastResponse.headers); let responseBody = ""; try { responseBody = await parseResponseBody(lastResponse, mergedOptions.parseResponse); } catch { responseBody = ""; } return { status: "failed", statusCode: lastResponse.status, body: responseBody, headers: responseHeaders, error: lastError?.message || `HTTP ${lastResponse.status}: ${lastResponse.statusText}`, retryable: true }; } throw lastError || new TransmissionError("Failed to transmit SET after all retry attempts", void 0, true); } function createTransmitter(defaultOptions) { return (jwt, url, options) => { const mergedOptions = { ...defaultOptions, ...options, headers: { ...defaultOptions?.headers || {}, ...options?.headers || {} }, retry: { ...defaultOptions?.retry || {}, ...options?.retry || {} } }; return transmitSET(jwt, url, mergedOptions); }; } exports.EventTypes = EventTypes; exports.NetworkError = NetworkError; exports.TimeoutError = TimeoutError; exports.TransmissionError = TransmissionError; exports.ValidationError = ValidationError; exports.createTransmitter = createTransmitter; exports.isValidSET = isValidSET; exports.transmitSET = transmitSET; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map