UNPKG

splitwise

Version:

A TypeScript SDK for the Splitwise API.

162 lines 6.55 kB
"use strict"; /** * Shared internals for the OAuth token endpoint: * - POSTing form-encoded credentials * - Parsing the response into an OAuthToken * - Translating HTTP/network failures into SDK error types * * Both client-credentials and authorization-code flows hit the same endpoint * with the same response shape, so they share this code path. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_AUTHORIZE_URL = exports.DEFAULT_TOKEN_URL = void 0; exports.postTokenRequest = postTokenRequest; const errors_js_1 = require("../errors.js"); const retry_js_1 = require("../retry.js"); const DEFAULT_TIMEOUT_MS = 30_000; const DEFAULT_MAX_RETRIES = 2; exports.DEFAULT_TOKEN_URL = 'https://secure.splitwise.com/oauth/token'; exports.DEFAULT_AUTHORIZE_URL = 'https://secure.splitwise.com/oauth/authorize'; function isPlainObject(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } function extractOAuthErrorMessage(body) { if (!isPlainObject(body)) return null; const errorDescription = body['error_description']; if (typeof errorDescription === 'string' && errorDescription.length > 0) { return errorDescription; } const error = body['error']; if (typeof error === 'string' && error.length > 0) { return error; } return null; } function extractOAuthErrorCode(body) { if (isPlainObject(body) && typeof body['error'] === 'string') { return body['error']; } return 'oauth_error'; } /** * POSTs `params` form-encoded to the token endpoint and returns the parsed token. * * 401/400 responses are mapped to SplitwiseAuthenticationError because the OAuth * spec uses 400 for things like `invalid_grant` even though the SDK normally * reserves 400 for validation errors. * * Honors timeout, AbortSignal, and exponential-backoff retry on transient * failures (matching the main HttpClient's behavior). 4xx responses are not * retried since they indicate the credentials themselves are bad. */ async function postTokenRequest(params, options = {}) { const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES; const callerSignal = options.signal; return (0, retry_js_1.withRetry)(() => postTokenRequestOnce(params, options), { maxRetries }, ({ error }) => { // Don't burn through retries if the caller has given up. if (callerSignal?.aborted === true) return false; // Network failures, 5xx, and rate-limit responses are transient. // 4xx (bad credentials) are not -- they indicate the request itself // is wrong and retrying won't help. return (error instanceof errors_js_1.SplitwiseConnectionError || error instanceof errors_js_1.SplitwiseRateLimitError || error instanceof errors_js_1.SplitwiseServerError); }); } async function postTokenRequestOnce(params, options) { const tokenUrl = options.tokenUrl ?? exports.DEFAULT_TOKEN_URL; const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS; const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis); const body = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { body.append(key, value); } // Compose the timeout-driven controller with any caller-supplied signal, // matching HttpClient.requestOnce. const controller = new AbortController(); const timeoutHandle = setTimeout(() => { controller.abort(); }, timeout); const callerSignal = options.signal; let abortListenerCleanup; if (callerSignal !== undefined) { if (callerSignal.aborted) { controller.abort(); } else { const onAbort = () => { controller.abort(); }; callerSignal.addEventListener('abort', onAbort); abortListenerCleanup = () => { callerSignal.removeEventListener('abort', onAbort); }; } } let response; try { response = await fetchImpl(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', }, body: body.toString(), signal: controller.signal, }); } catch (error) { const err = error; if (err.name === 'AbortError') { if (callerSignal?.aborted === true) { throw new errors_js_1.SplitwiseConnectionError('OAuth token request aborted by caller', err); } throw new errors_js_1.SplitwiseConnectionError(`OAuth token request timed out after ${timeout}ms`, err); } throw new errors_js_1.SplitwiseConnectionError(err.message || 'Network request failed', err); } finally { clearTimeout(timeoutHandle); abortListenerCleanup?.(); } const rawText = await response.text(); let parsed = undefined; if (rawText.length > 0) { try { parsed = JSON.parse(rawText); } catch { // Non-JSON body; leave parsed undefined and fall back to raw text below. } } if (!response.ok) { const message = extractOAuthErrorMessage(parsed) ?? `HTTP ${response.status} ${response.statusText || ''}`.trim(); const code = extractOAuthErrorCode(parsed); if (response.status === 400 || response.status === 401) { throw new errors_js_1.SplitwiseAuthenticationError(message, code, parsed ?? rawText); } throw (0, errors_js_1.createApiError)(response.status, message, code, parsed ?? rawText, response.headers); } const body_ = parsed; const accessToken = body_?.access_token; const tokenType = body_?.token_type; if (typeof accessToken !== 'string' || accessToken.length === 0) { throw new errors_js_1.SplitwiseAuthenticationError('OAuth token response missing access_token', 'invalid_response', parsed ?? rawText); } const token = { accessToken, tokenType: typeof tokenType === 'string' ? tokenType : 'bearer', }; if (typeof body_?.expires_in === 'number' && Number.isFinite(body_.expires_in)) { token.expiresAt = Date.now() + body_.expires_in * 1000; } if (typeof body_?.refresh_token === 'string' && body_.refresh_token.length > 0) { token.refreshToken = body_.refresh_token; } return token; } //# sourceMappingURL=internal.js.map