mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Redact Nightscout token from recorded sync error
The entries URL carries the access token as ?token=<secret>; a SafeFetchError embeds the full target URL in its message, and the sync feeds err.message into recordSyncFailure, which the status endpoint returns as lastError. Strip token and api-secret query values to =REDACTED before the message reaches the ledger, so the secret can never surface in the status response.
This commit is contained in:
1 parent
1735205096
commit
d58f0dc699
2 files changed
+53
-1
No files matched your search
@@ -149,6 +149,34 @@ describe("syncUserNightscout", () => {
|
||||
expect(recordFailureMock.mock.calls[0]![0].kind).toBe("transient");
|
||||
});
|
||||
|
||||
it("redacts the token from the recorded lastError message", async () => {
|
||||
// A SafeFetchError embeds the full target URL — including `?token=<secret>`
|
||||
// — in its message; the recorded lastError must never leak it.
|
||||
fetchSgvEntriesMock.mockRejectedValue(
|
||||
new Error(
|
||||
"safeFetch network error: https://ns.example.com/api/v1/entries.json?count=576&type=sgv&token=ns-secret-abc123",
|
||||
),
|
||||
);
|
||||
await expect(syncUserNightscout("u1")).rejects.toThrow();
|
||||
const message = recordFailureMock.mock.calls[0]![0].message as string;
|
||||
expect(message).not.toContain("ns-secret-abc123");
|
||||
expect(message).toContain("token=REDACTED");
|
||||
// The rest of the diagnostic is preserved.
|
||||
expect(message).toContain("ns.example.com");
|
||||
});
|
||||
|
||||
it("also redacts a classic api-secret query param", async () => {
|
||||
fetchSgvEntriesMock.mockRejectedValue(
|
||||
new Error(
|
||||
"safeFetch refused private or non-public host: https://ns.lan/api/v1/entries.json?api-secret=deadbeef",
|
||||
),
|
||||
);
|
||||
await expect(syncUserNightscout("u1")).rejects.toThrow();
|
||||
const message = recordFailureMock.mock.calls[0]![0].message as string;
|
||||
expect(message).not.toContain("deadbeef");
|
||||
expect(message).toContain("api-secret=REDACTED");
|
||||
});
|
||||
|
||||
it("recomputes rollup buckets for the imported readings", async () => {
|
||||
fetchSgvEntriesMock.mockResolvedValue([ENTRY_A, ENTRY_B]);
|
||||
await syncUserNightscout("u1");
|
||||
|
||||
@@ -48,6 +48,28 @@ import { getUserNightscoutCredentials } from "./credentials";
|
||||
*/
|
||||
export const NIGHTSCOUT_SYNC_COUNT = 576;
|
||||
|
||||
/**
|
||||
* Redact the Nightscout auth secret from an error message before it reaches the
|
||||
* integration ledger / the `/api/nightscout/status` response.
|
||||
*
|
||||
* The entries URL carries the access token as a `?token=<secret>` query param
|
||||
* (and a classic instance may carry `api-secret`). A `SafeFetchError` embeds the
|
||||
* full target URL verbatim in its message — a private-host refusal, a timeout,
|
||||
* or a network failure would otherwise persist the token into `lastError` and
|
||||
* echo it back through the status endpoint. Rewrite any URL in the message so
|
||||
* the secret-bearing params read `=REDACTED`; everything else is preserved so
|
||||
* the operator still sees a useful diagnostic.
|
||||
*/
|
||||
export function redactNightscoutSecret(message: string): string {
|
||||
// Match `token=...` / `api-secret=...` up to the next param / whitespace /
|
||||
// quote boundary. Case-insensitive on the key; the value is whatever a URL
|
||||
// query value can hold short of a delimiter.
|
||||
return message.replace(
|
||||
/\b(token|api-secret)=[^\s&"']+/gi,
|
||||
"$1=REDACTED",
|
||||
);
|
||||
}
|
||||
|
||||
/** Map a Nightscout HTTP status / network error onto the shared ledger kind. */
|
||||
export function classifyNightscoutFailure(err: unknown): FailureKind {
|
||||
if (err instanceof NightscoutApiError && err.status != null) {
|
||||
@@ -85,7 +107,9 @@ export async function syncUserNightscout(
|
||||
userId,
|
||||
integration: "nightscout",
|
||||
kind: classifyNightscoutFailure(err),
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
message: redactNightscoutSecret(
|
||||
err instanceof Error ? err.message : String(err),
|
||||
),
|
||||
errorCode:
|
||||
err instanceof NightscoutApiError && err.status != null
|
||||
? String(err.status)
|
||||
|
||||
Reference in new issue
Block a user