mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
classifyPolarError now lifts a 400 carrying invalid_grant on the token endpoint from persistent to reauth_required, mirroring Oura via the shared isOAuthInvalidGrant helper. A stale or replayed authorization code at connect time now surfaces the reconnect prompt instead of silently parking after 24 h. The BYO-credentials DELETE now audit-logs and parks the integration ledger at disconnected for parity with the dedicated disconnect route, and the PUT audit-logs the credential write. Refresh the connect-route comment that still described Polar as env-only OAuth; it has resolved BYO-first since v1.17.1. Add the invalid_grant classification test plus the previously-missing callback (happy path, CSRF mismatch, cross-user, off-spec token), connect, and credentials route tests.
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
/**
|
|
* v1.17.0 (F4) — off-response classifier for Polar AccessLink replies.
|
|
*
|
|
* Polar signals failure through the HTTP status code, so the classification is
|
|
* status-driven. The shared core lives in
|
|
* `src/lib/integrations/http-status-classifier.ts`; this module re-exports it
|
|
* under the Polar-specific names callers already use.
|
|
*
|
|
* - `success` → HTTP 2xx (incl. 204 No Content — Polar returns 204
|
|
* when a collection window holds no records).
|
|
* - `transient` → retryable: 429 (rate-limited), 5xx upstream, 3xx
|
|
* CDN glitch, and an unknown status.
|
|
* - `reauth_required` → 401 / 403 — the access token is rejected or the user
|
|
* revoked the grant; the user must reconnect.
|
|
* - `persistent` → any other 4xx — a malformed request / contract
|
|
* mismatch that a retry won't fix.
|
|
*/
|
|
|
|
import {
|
|
classifyHttpStatus,
|
|
classifyIntegrationError,
|
|
IntegrationApiError,
|
|
isOAuthInvalidGrant,
|
|
type ClassifiedHttpResponse,
|
|
type IntegrationClassification,
|
|
} from "@/lib/integrations/http-status-classifier";
|
|
|
|
export type PolarClassification = IntegrationClassification;
|
|
|
|
export type ClassifiedPolarResponse = ClassifiedHttpResponse;
|
|
|
|
export function classifyPolarResponse(
|
|
httpStatus: number,
|
|
): ClassifiedPolarResponse {
|
|
return classifyHttpStatus(httpStatus);
|
|
}
|
|
|
|
/** Typed error carrying the classification verdict. Extends the shared base. */
|
|
export class PolarApiError extends IntegrationApiError {
|
|
constructor(opts: {
|
|
verb: string;
|
|
classification: PolarClassification;
|
|
httpStatus: number | undefined;
|
|
reason: string;
|
|
upstreamError?: string;
|
|
}) {
|
|
super({ vendor: "Polar", ...opts });
|
|
this.name = "PolarApiError";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* True when a caught error is an OAuth `invalid_grant` on the token endpoint —
|
|
* a stale/replayed authorization code on Polar's only token call
|
|
* (`exchangeCode`). Lifts a 400 carrying `invalid_grant` from `persistent` to
|
|
* `reauth_required` so the connection surfaces the reconnect prompt instead of
|
|
* silently parking after 24 h.
|
|
*/
|
|
export function isInvalidGrant(err: unknown): boolean {
|
|
return isOAuthInvalidGrant(err, "Polar");
|
|
}
|
|
|
|
/**
|
|
* Read the classification from any caught error. A non-`PolarApiError` defaults
|
|
* to `transient` so a plain `Error` retries rather than disabling the
|
|
* integration. Falls back to parsing the status out of the message shape for a
|
|
* caller that lost the prototype across a pg-boss retry.
|
|
*/
|
|
export function classifyPolarError(err: unknown): PolarClassification {
|
|
if (isInvalidGrant(err)) return "reauth_required";
|
|
return classifyIntegrationError(err, "Polar");
|
|
}
|