fix(polar): clear each leg on its own success, read a lone 403 as refused

The workouts leg recorded its failures under its own leg but left the
success to the vitals leg. A leg-scoped success clears only its own
leg, so one workouts failure stayed on the row for good and kept the
connection in error however healthy the exercises read became. Each
leg now records its own success.

AccessLink documents its 403 as missing consent for that data. A 403
on one collection while a sibling succeeded with the same token was
classified reauth_required and put a working connection behind a
reconnect prompt no reconnect clears. Like Oura's scope refusals, such
a collection (or the exercises read, once the vitals leg proved the
token) is now noted on the wide event and not counted as a failure. A
403 on every collection, or any 401, stays one.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 12:23:57 +02:00
1 parent a66c54a1f1
commit 1fc0268b3d
8 files changed
+277 -59

No files matched your search

+14 -3
View File
@@ -67,13 +67,21 @@ beforeEach(() => {
vi.clearAllMocks();
vi.mocked(checkRateLimit).mockResolvedValue({ allowed: true } as never);
vi.mocked(requireAuth).mockResolvedValue({ user: { id: "u1" } } as never);
syncUserPolarMock.mockResolvedValue({ imported: 0, failed: false });
syncUserPolarMock.mockResolvedValue({
imported: 0,
failed: false,
tokenProven: true,
});
syncUserPolarWorkoutsMock.mockResolvedValue(0);
});
describe("POST /api/polar/sync", () => {
it("runs both Polar legs and totals what they imported", async () => {
syncUserPolarMock.mockResolvedValue({ imported: 5, failed: false });
syncUserPolarMock.mockResolvedValue({
imported: 5,
failed: false,
tokenProven: true,
});
syncUserPolarWorkoutsMock.mockResolvedValue(2);
const response = await post(request());
@@ -84,7 +92,10 @@ describe("POST /api/polar/sync", () => {
error: null,
});
expect(syncUserPolarMock).toHaveBeenCalledWith("u1");
expect(syncUserPolarWorkoutsMock).toHaveBeenCalledWith("u1");
// The vitals leg tells the workouts leg the token just worked.
expect(syncUserPolarWorkoutsMock).toHaveBeenCalledWith("u1", {
tokenProven: true,
});
});
it("does not call a run whose vitals collections all failed a success", async () => {
@@ -17,20 +17,30 @@ import { syncUserPolarLegs } from "../polar-sync";
beforeEach(() => {
vi.clearAllMocks();
syncUserPolar.mockResolvedValue({ imported: 0, failed: false });
syncUserPolar.mockResolvedValue({
imported: 0,
failed: false,
tokenProven: true,
});
syncUserPolarWorkouts.mockResolvedValue(0);
});
describe("syncUserPolarLegs — composite independence on the shared polar ledger", () => {
it("runs both legs and returns the summed count when both pass", async () => {
syncUserPolar.mockResolvedValue({ imported: 5, failed: false });
syncUserPolar.mockResolvedValue({
imported: 5,
failed: false,
tokenProven: true,
});
syncUserPolarWorkouts.mockResolvedValue(2);
await expect(syncUserPolarLegs("u1")).resolves.toEqual({
imported: 7,
failed: false,
});
expect(syncUserPolar).toHaveBeenCalledWith("u1");
expect(syncUserPolarWorkouts).toHaveBeenCalledWith("u1");
expect(syncUserPolarWorkouts).toHaveBeenCalledWith("u1", {
tokenProven: true,
});
});
it("sums both counts without throwing when the vitals leg returns a partial-failure count", async () => {
@@ -38,7 +48,11 @@ describe("syncUserPolarLegs — composite independence on the shared polar ledge
// its healthy-collections import count rather than throwing. The wrapper
// must sum it with the workout leg and complete cleanly — a partial vitals
// failure never starves the user's success accounting at the cohort level.
syncUserPolar.mockResolvedValue({ imported: 4, failed: true });
syncUserPolar.mockResolvedValue({
imported: 4,
failed: true,
tokenProven: true,
});
syncUserPolarWorkouts.mockResolvedValue(2);
// The partial is carried, not swallowed: the wrapper completes with the
// summed count AND says a leg did not settle, so the card cannot paint a
@@ -48,7 +62,9 @@ describe("syncUserPolarLegs — composite independence on the shared polar ledge
failed: true,
});
expect(syncUserPolar).toHaveBeenCalledWith("u1");
expect(syncUserPolarWorkouts).toHaveBeenCalledWith("u1");
expect(syncUserPolarWorkouts).toHaveBeenCalledWith("u1", {
tokenProven: true,
});
});
it("still attempts the workout leg when the vitals leg fails (vice-versa isolation)", async () => {
@@ -57,11 +73,17 @@ describe("syncUserPolarLegs — composite independence on the shared polar ledge
// The vitals error is rethrown (attributable), but only AFTER the workout
// leg was attempted — a vitals failure never starves the workout leg.
await expect(syncUserPolarLegs("u1")).rejects.toThrow("vitals down");
expect(syncUserPolarWorkouts).toHaveBeenCalledWith("u1");
expect(syncUserPolarWorkouts).toHaveBeenCalledWith("u1", {
tokenProven: false,
});
});
it("leaves the vitals leg's result intact when the workout leg fails", async () => {
syncUserPolar.mockResolvedValue({ imported: 4, failed: false });
syncUserPolar.mockResolvedValue({
imported: 4,
failed: false,
tokenProven: true,
});
syncUserPolarWorkouts.mockRejectedValue(new Error("workouts down"));
// The vitals leg ran to completion (its own success already recorded on
// the shared ledger); the workout error is surfaced without undoing it.
@@ -73,6 +95,8 @@ describe("syncUserPolarLegs — composite independence on the shared polar ledge
syncUserPolar.mockRejectedValue(new Error("vitals first"));
syncUserPolarWorkouts.mockRejectedValue(new Error("workouts second"));
await expect(syncUserPolarLegs("u1")).rejects.toThrow("vitals first");
expect(syncUserPolarWorkouts).toHaveBeenCalledWith("u1");
expect(syncUserPolarWorkouts).toHaveBeenCalledWith("u1", {
tokenProven: false,
});
});
});
+6 -3
View File
@@ -23,8 +23,9 @@ export type PolarSyncPayload = PollCohortPayload;
/**
* Run both Polar legs for one user with INDEPENDENT attribution on the shared
* `polar` ledger. Sequentially awaited (not `Promise.all`) so a workouts error
* stays attributable. Each leg records its own failure; the vitals leg owns the
* single success write. A failure in either leg is isolated — both legs are
* stays attributable, and so the vitals leg can tell the workouts leg whether
* the token just worked (a 403 on exercises is then a per-type refusal). Each
* leg records its own failure and its own success. A failure in either leg is isolated — both legs are
* always attempted, so a workouts-leg failure can never mask a vitals success
* and vice versa — then the FIRST error is rethrown so the cohort handler warns
* and skips this user's success accounting without losing the error.
@@ -45,18 +46,20 @@ export async function syncUserPolarLegs(
// it has to be carried explicitly — otherwise a run in which four of five
// collections failed reads exactly like a clean one.
let partial = false;
let tokenProven = false;
try {
const vitals = await syncUserPolar(userId);
total += vitals.imported;
partial = vitals.failed;
tokenProven = vitals.tokenProven;
} catch (err) {
firstError = err;
hadError = true;
}
try {
total += await syncUserPolarWorkouts(userId);
total += await syncUserPolarWorkouts(userId, { tokenProven });
} catch (err) {
if (!hadError) {
firstError = err;
+53 -7
View File
@@ -9,6 +9,7 @@ const {
getPolarConnection,
fetchExercises,
recordSyncFailure,
recordSyncSuccess,
} = vi.hoisted(() => ({
createManyAndReturn: vi.fn(),
update: vi.fn(),
@@ -16,6 +17,7 @@ const {
getPolarConnection: vi.fn(),
fetchExercises: vi.fn(),
recordSyncFailure: vi.fn(async () => {}),
recordSyncSuccess: vi.fn(async () => {}),
}));
vi.mock("@/lib/db", () => ({
@@ -32,6 +34,7 @@ vi.mock("../credentials", () => ({ getPolarConnection }));
vi.mock("@/lib/integrations/status", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/integrations/status")>()),
recordSyncFailure,
recordSyncSuccess,
}));
vi.mock("../client", async (importOriginal) => {
const actual = await importOriginal<typeof import("../client")>();
@@ -181,9 +184,9 @@ describe("syncUserPolarWorkouts", () => {
);
});
// Assumption #4: a 403 scope gap is a soft/recoverable ledger error, not a
// hard crash — recorded as reauth_required and rethrown for cohort isolation.
it("records a 403 scope error as a soft reauth_required failure", async () => {
// Without a read that proved the token, a 403 may be the account-wide
// consent gap: recorded as reauth_required and rethrown for cohort isolation.
it("records a 403 as a reauth_required failure when nothing proved the token", async () => {
fetchExercises.mockRejectedValue(
new PolarApiError({
verb: "fetchExercises",
@@ -204,11 +207,54 @@ describe("syncUserPolarWorkouts", () => {
);
});
it("never records success on the shared ledger (vitals leg owns the single success write)", async () => {
const status = await import("@/lib/integrations/status");
const spy = vi.spyOn(status, "recordSyncSuccess");
it("reads a 403 as a refused data type once the vitals leg proved the token", async () => {
fetchExercises.mockRejectedValue(
new PolarApiError({
verb: "fetchExercises",
classification: "reauth_required",
httpStatus: 403,
reason: "http_403",
}),
);
await expect(
syncUserPolarWorkouts("u1", { tokenProven: true }),
).resolves.toBe(0);
expect(recordSyncFailure).not.toHaveBeenCalled();
expect(recordSyncSuccess).toHaveBeenCalledWith("u1", "polar", {
leg: "workouts",
});
});
it("keeps a 401 a failure even after the vitals leg proved the token", async () => {
fetchExercises.mockRejectedValue(
new PolarApiError({
verb: "fetchExercises",
classification: "reauth_required",
httpStatus: 401,
reason: "http_401",
}),
);
await expect(
syncUserPolarWorkouts("u1", { tokenProven: true }),
).rejects.toBeInstanceOf(PolarApiError);
expect(recordSyncFailure).toHaveBeenCalledTimes(1);
expect(recordSyncSuccess).not.toHaveBeenCalled();
});
// A leg-scoped success clears only its own leg, so this leg has to write
// its own: left to the vitals leg, one workouts failure stayed on the row
// for good.
it("records its own leg's success after a clean read", async () => {
fetchExercises.mockResolvedValue([]);
await syncUserPolarWorkouts("u1");
expect(spy).not.toHaveBeenCalled();
expect(recordSyncSuccess).toHaveBeenCalledWith("u1", "polar", {
leg: "workouts",
});
});
it("records no success when the read failed", async () => {
fetchExercises.mockRejectedValue(new Error("boom"));
await expect(syncUserPolarWorkouts("u1")).rejects.toThrow("boom");
expect(recordSyncSuccess).not.toHaveBeenCalled();
});
});
+71 -11
View File
@@ -164,7 +164,11 @@ function createdRows() {
describe("syncUserPolar", () => {
it("no-ops cleanly for an unconnected user", async () => {
getConnMock.mockResolvedValue(null);
expect(await syncUserPolar("u1")).toEqual({ imported: 0, failed: false });
expect(await syncUserPolar("u1")).toEqual({
imported: 0,
failed: false,
tokenProven: false,
});
expect(createManyAndReturnMock).not.toHaveBeenCalled();
expect(recordSuccessMock).not.toHaveBeenCalled();
});
@@ -370,13 +374,13 @@ describe("syncUserPolar", () => {
it("isolates one dead collection — imports the healthy four and records one named failure", async () => {
getConnMock.mockResolvedValue(CONN);
// SpO2 403s (no sensor / consent), the other four return one row each.
// SpO2 fails upstream, the other four return one row each.
fetchSpo2Mock.mockRejectedValue(
new PolarApiError({
verb: "fetchSpo2",
classification: "reauth_required",
httpStatus: 403,
reason: "http_403",
classification: "transient",
httpStatus: 503,
reason: "http_503",
}),
);
fetchRechargesMock.mockResolvedValue([
@@ -402,6 +406,7 @@ describe("syncUserPolar", () => {
// The partial rides back with the count rather than being narrowed away.
expect(result.failed).toBe(true);
expect(result.tokenProven).toBe(true);
// The healthy four still wrote their rows — the source is not blanked.
const written = createdRows().map((row) => row.type);
expect(written).toContain("RECOVERY_SCORE");
@@ -410,19 +415,74 @@ describe("syncUserPolar", () => {
expect(written).toContain("CARDIO_LOAD");
expect(written).not.toContain("OXYGEN_SATURATION");
expect(imported).toBeGreaterThan(0);
// Exactly one failure, classified off the 403, naming spo2. No success.
// Exactly one failure, naming spo2. No success.
expect(recordFailureMock).toHaveBeenCalledTimes(1);
expect(recordFailureMock).toHaveBeenCalledWith(
expect.objectContaining({
integration: "polar",
kind: "reauth_required",
errorCode: "403",
kind: "transient",
errorCode: "503",
message: expect.stringContaining("spo2"),
}),
);
expect(recordSuccessMock).not.toHaveBeenCalled();
});
it("reads a 403 beside a healthy sibling as a refused data type, not a dead token", async () => {
getConnMock.mockResolvedValue(CONN);
// An account without the SpO2 sensor or the consent for it: the same token
// reads the other collections fine.
fetchSpo2Mock.mockRejectedValue(
new PolarApiError({
verb: "fetchSpo2",
classification: "reauth_required",
httpStatus: 403,
reason: "http_403",
}),
);
fetchCardioLoadsMock.mockResolvedValue([
{ date: "2026-06-10", cardio_load: 55 },
]);
const result = await syncUserPolar("u1");
expect(result).toMatchObject({ failed: false, tokenProven: true });
expect(createdRows().map((row) => row.type)).toContain("CARDIO_LOAD");
expect(recordFailureMock).not.toHaveBeenCalled();
expect(recordSuccessMock).toHaveBeenCalledWith("u1", "polar", {
leg: "vitals",
});
});
it("keeps a 403 on every collection a reauth_required failure", async () => {
getConnMock.mockResolvedValue(CONN);
for (const mock of [
fetchRechargesMock,
fetchSleepsMock,
fetchActivitiesMock,
fetchCardioLoadsMock,
fetchSpo2Mock,
]) {
mock.mockRejectedValue(
new PolarApiError({
verb: "fetch",
classification: "reauth_required",
httpStatus: 403,
reason: "http_403",
}),
);
}
const result = await syncUserPolar("u1");
expect(result).toMatchObject({ failed: true, tokenProven: false });
expect(recordFailureMock).toHaveBeenCalledTimes(1);
expect(recordFailureMock).toHaveBeenCalledWith(
expect.objectContaining({ kind: "reauth_required", errorCode: "403" }),
);
expect(recordSuccessMock).not.toHaveBeenCalled();
});
it("isolates a throwing mapper — one malformed record does not blank siblings", async () => {
getConnMock.mockResolvedValue(CONN);
// A malformed recharge record whose mapper throws on first field access
@@ -534,9 +594,9 @@ describe("syncUserPolar", () => {
mockByName[name]!.mockRejectedValue(
new PolarApiError({
verb: name,
classification: "reauth_required",
httpStatus: 403,
reason: "http_403",
classification: "persistent",
httpStatus: 404,
reason: "http_404",
}),
);
+11
View File
@@ -60,6 +60,17 @@ export function isInvalidGrant(err: unknown): boolean {
return isOAuthInvalidGrant(err, "Polar");
}
/**
* True for a collection read Polar refused with 403. AccessLink documents its
* 403 as "User has not accepted all mandatory consents": the token is good,
* the account does not share this one data type (or has nothing behind it).
* Only meaningful next to a read that succeeded with the same token — a 403 on
* every collection is the account-wide consent gap, and stays a failure.
*/
export function isPolarCollectionRefusal(err: unknown): boolean {
return err instanceof PolarApiError && err.httpStatus === 403;
}
/**
* Read the classification from any caught error. A non-`PolarApiError` defaults
* to `transient` so a plain `Error` retries rather than disabling the
+28 -13
View File
@@ -21,12 +21,13 @@
* only write and it is idempotent — no cursor, no backfill queue (the API caps
* history at 30 days, so first-connect backfill IS the first regular tick).
*
* Ledger: workouts and vitals share the single `polar` integration ledger. This
* leg records only its OWN FAILURE (and rethrows so the cohort handler can
* attribute it); the single success write stays the vitals leg's job so a fully
* successful pass records success exactly once. A workouts-leg failure records a
* transient/reauth failure on the shared ledger but never touches the vitals
* success timestamp (`recordSyncFailure` leaves `lastSuccessAt` intact).
* Ledger: workouts and vitals share the single `polar` integration ledger, each
* under its own leg. This leg records its own failure (and rethrows so the
* cohort handler can attribute it) and its own success. The success used to be
* left to the vitals leg, but a leg-scoped success clears only its own leg, so
* a workouts failure stayed on the row for good once recorded: every later
* vitals success saw the workouts leg still failing and kept the connection in
* error, however healthy the exercises read had become.
*/
import { prisma } from "@/lib/db";
import { emitInsertedWorkoutArrival } from "@/lib/arrivals/workout-emit";
@@ -35,11 +36,16 @@ import { annotate, getEvent } from "@/lib/logging/context";
import {
markSyncFailureRecorded,
recordSyncFailure,
recordSyncSuccess,
toFailureKind,
} from "@/lib/integrations/status";
import { fetchExercises, mapExercise, type PolarWorkoutRow } from "./client";
import { getPolarConnection } from "./credentials";
import { PolarApiError, classifyPolarError } from "./response-classifier";
import {
PolarApiError,
classifyPolarError,
isPolarCollectionRefusal,
} from "./response-classifier";
const POLAR_LEG_WORKOUTS = "workouts";
@@ -70,13 +76,16 @@ async function recordPolarWorkoutFailure(
* written. A user with no Polar connection is a clean no-op (returns 0, touches
* no status row).
*
* A 403 on the exercises read (a scope gap under `accesslink.read_all`) is
* classified through the shared response classifier as `reauth_required` — a
* SOFT/recoverable integration error surfaced on the status ledger, NOT a hard
* crash: it is recorded and rethrown so the composite cohort pass isolates this
* user and the vitals leg's own result is untouched.
* A 403 on the exercises read is a refusal when the vitals leg proved the token
* in the same run (`tokenProven`): the account does not share exercises, which
* no reconnect changes, so the leg reads nothing and stays healthy. Without
* that proof the 403 is classified `reauth_required` as before, recorded, and
* rethrown so the composite cohort pass isolates this user.
*/
export async function syncUserPolarWorkouts(userId: string): Promise<number> {
export async function syncUserPolarWorkouts(
userId: string,
opts: { tokenProven?: boolean } = {},
): Promise<number> {
const conn = await getPolarConnection(userId);
if (!conn) return 0;
@@ -91,6 +100,11 @@ export async function syncUserPolarWorkouts(userId: string): Promise<number> {
if (row) rows.push(row);
}
} catch (err) {
if (opts.tokenProven && isPolarCollectionRefusal(err)) {
annotate({ meta: { polar_collections_refused: [POLAR_LEG_WORKOUTS] } });
await recordSyncSuccess(userId, "polar", { leg: POLAR_LEG_WORKOUTS });
return 0;
}
await recordPolarWorkoutFailure(userId, err);
throw markSyncFailureRecorded(err);
}
@@ -110,6 +124,7 @@ export async function syncUserPolarWorkouts(userId: string): Promise<number> {
invalidateUserMeasurements(userId);
}
await recordSyncSuccess(userId, "polar", { leg: POLAR_LEG_WORKOUTS });
return imported;
}
+62 -14
View File
@@ -13,12 +13,19 @@
* exactly like Nightscout's token-rejected path.
*
* Isolation: the five vitals collections are settled independently
* (`Promise.allSettled`), so a single dead collection — e.g. a 403 on SpO2 or
* cardio-load on an account without the sensor or the higher tier — imports the
* healthy four and records ONE partial failure rather than blanking every
* collection. Mirrors Oura's shipped per-collection isolation, minus the
* token-refresh leg Polar structurally lacks (a 401 is just a per-collection
* failure the classifier maps to `reauth_required`).
* (`Promise.allSettled`), so a single dead collection imports the healthy four
* and records ONE partial failure rather than blanking every collection.
* Mirrors Oura's shipped per-collection isolation, minus the token-refresh leg
* Polar structurally lacks (a 401 is just a per-collection failure the
* classifier maps to `reauth_required`).
*
* A 403 on one collection while a sibling succeeded is a refusal, not a
* failure (`isPolarCollectionRefusal`): the same token just worked, so the
* account simply does not share that data type (SpO2 without the sensor,
* cardio load without the tier). Counting it as `reauth_required` put a
* working connection behind a "reconnect" prompt that no reconnect clears.
* The refused names ride the wide event; the leg records success. Mirrors
* Oura's handling of a scope the grant does not cover.
*
* Window contract: every hourly tick re-reads the full server-fixed window for
* each collection — 28 days for recharge / sleep / activity / cardio-load /
@@ -39,7 +46,7 @@
*/
import { prisma } from "@/lib/db";
import type { MeasurementType } from "@/generated/prisma/client";
import { getEvent } from "@/lib/logging/context";
import { annotate, getEvent } from "@/lib/logging/context";
import {
recordSyncFailure,
recordSyncSuccess,
@@ -80,7 +87,11 @@ import {
type SleepSegmentSweep,
} from "@/lib/sleep/sweep-stale-segments";
import { getPolarConnection } from "./credentials";
import { PolarApiError, classifyPolarError } from "./response-classifier";
import {
PolarApiError,
classifyPolarError,
isPolarCollectionRefusal,
} from "./response-classifier";
import { resolveUserTimezone } from "@/lib/tz/resolver";
/** Map a Polar error onto the shared integration-ledger failure kind. */
@@ -164,6 +175,15 @@ export interface PolarCollectionFailure {
err: unknown;
}
/** The vitals leg's outcome, plus whether any collection proved the token. */
export interface PolarVitalsResult extends SyncWriteResult {
/**
* True when at least one collection read succeeded with the token, so a 403
* elsewhere in this run (the workouts leg included) is a per-type refusal.
*/
tokenProven: boolean;
}
/**
* Sync one user's Polar data. Returns the rows written AND whether a
* collection did not settle — the bare count used to narrow the partial away,
@@ -177,9 +197,11 @@ export interface PolarCollectionFailure {
* escapes now is a write-path/unexpected error out of `upsertPolarMeasurements`
* — the poll-cohort boundary records that unmarked escape once.
*/
export async function syncUserPolar(userId: string): Promise<SyncWriteResult> {
export async function syncUserPolar(
userId: string,
): Promise<PolarVitalsResult> {
const conn = await getPolarConnection(userId);
if (!conn) return { imported: 0, failed: false };
if (!conn) return { imported: 0, failed: false, tokenProven: false };
// Date-keyed rows are anchored at local noon of their date in this zone.
const tz = await resolveUserTimezone(userId);
@@ -250,15 +272,23 @@ export async function syncUserPolar(userId: string): Promise<SyncWriteResult> {
collections.map((spec) => spec.collect(conn.accessToken)),
);
const readings: PolarMeasurementUpsert[] = [];
const failures: PolarCollectionFailure[] = [];
const settledFailures: PolarCollectionFailure[] = [];
settled.forEach((res, i) => {
const spec = collections[i]!;
if (res.status === "fulfilled") {
readings.push(...res.value);
} else {
failures.push({ name: spec.name, err: res.reason });
settledFailures.push({ name: spec.name, err: res.reason });
}
});
const tokenProven = settled.some((res) => res.status === "fulfilled");
const { failures, refused } = splitPolarRefusals(
settledFailures,
tokenProven,
);
if (refused.length > 0) {
annotate({ meta: { polar_collections_refused: refused } });
}
// Clear whatever an earlier scoring left under the re-fetched nights before
// the fresh set upserts (mirrors Google Health's replace-by-window order).
@@ -317,11 +347,29 @@ export async function syncUserPolar(userId: string): Promise<SyncWriteResult> {
? String(firstErr.httpStatus)
: undefined,
});
return { imported, failed: true };
return { imported, failed: true, tokenProven };
}
await recordSyncSuccess(userId, "polar", { leg: POLAR_LEG_VITALS });
return { imported, failed: false };
return { imported, failed: false, tokenProven };
}
/**
* Split a run's collection failures into the ones that count and the per-type
* refusals (a 403 while another read proved the token). Without a proof every
* failure counts: a 403 on every collection is the account-wide consent gap.
*/
export function splitPolarRefusals(
failures: PolarCollectionFailure[],
tokenProven: boolean,
): { failures: PolarCollectionFailure[]; refused: string[] } {
if (!tokenProven) return { failures, refused: [] };
return {
failures: failures.filter((f) => !isPolarCollectionRefusal(f.err)),
refused: failures
.filter((f) => isPolarCollectionRefusal(f.err))
.map((f) => f.name),
};
}
/**