fix: harden widget request caching
This commit is contained in:
1 parent
c81088d876
commit
47724f44eb
26 files changed
+838
-209
No files matched your search
@@ -1,15 +1,66 @@
|
||||
import superjson from "superjson";
|
||||
|
||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||
import { createKeyedFingerprint, decryptSecret, encryptSecret, verifyKeyedFingerprint } from "@homarr/common/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
import { createGetSetChannel, getIntegrationSessionStoreKey } from "@homarr/redis";
|
||||
|
||||
const logger = createLogger({ module: "sessionStore" });
|
||||
import type { IntegrationSecret } from "./types";
|
||||
|
||||
export const createSessionStore = <TValue>(integration: { id: string }) => {
|
||||
const logger = createLogger({ module: "sessionStore" });
|
||||
const sessionEnvelopeType = "homarr-integration-session";
|
||||
const sessionEnvelopeVersion = 1;
|
||||
|
||||
interface SessionStoreIntegration {
|
||||
id: string;
|
||||
url: string;
|
||||
decryptedSecrets: readonly IntegrationSecret[];
|
||||
}
|
||||
|
||||
interface SessionEnvelope<TValue> {
|
||||
type: typeof sessionEnvelopeType;
|
||||
version: typeof sessionEnvelopeVersion;
|
||||
configurationFingerprint: string;
|
||||
value: TValue;
|
||||
}
|
||||
|
||||
const compareStrings = (left: string, right: string) => {
|
||||
if (left < right) return -1;
|
||||
if (left > right) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
const serializeSessionConfiguration = (integration: SessionStoreIntegration) => {
|
||||
const secrets = integration.decryptedSecrets
|
||||
.map(({ kind, value }) => [kind, value] as const)
|
||||
.toSorted(([leftKind, leftValue], [rightKind, rightValue]) => {
|
||||
const kindComparison = compareStrings(leftKind, rightKind);
|
||||
if (kindComparison !== 0) return kindComparison;
|
||||
return compareStrings(leftValue, rightValue);
|
||||
});
|
||||
|
||||
return JSON.stringify({ integrationId: integration.id, url: integration.url, secrets });
|
||||
};
|
||||
|
||||
const isSessionEnvelope = <TValue>(value: unknown): value is SessionEnvelope<TValue> => {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
if (!("value" in value)) return false;
|
||||
|
||||
return (
|
||||
"type" in value &&
|
||||
value.type === sessionEnvelopeType &&
|
||||
"version" in value &&
|
||||
value.version === sessionEnvelopeVersion &&
|
||||
"configurationFingerprint" in value &&
|
||||
typeof value.configurationFingerprint === "string"
|
||||
);
|
||||
};
|
||||
|
||||
export const createSessionStore = <TValue>(integration: SessionStoreIntegration) => {
|
||||
const channelName = getIntegrationSessionStoreKey(integration.id);
|
||||
const channel = createGetSetChannel<`${string}.${string}`>(channelName);
|
||||
const serializedConfiguration = serializeSessionConfiguration(integration);
|
||||
const configurationFingerprint = createKeyedFingerprint(serializedConfiguration);
|
||||
|
||||
return {
|
||||
async getAsync() {
|
||||
@@ -17,7 +68,16 @@ export const createSessionStore = <TValue>(integration: { id: string }) => {
|
||||
const value = await channel.getAsync();
|
||||
if (!value) return null;
|
||||
try {
|
||||
return superjson.parse<TValue>(decryptSecret(value));
|
||||
const envelope = superjson.parse<unknown>(decryptSecret(value));
|
||||
if (!isSessionEnvelope<TValue>(envelope)) {
|
||||
logger.debug("Ignoring legacy or unsupported session", { store: channelName });
|
||||
return null;
|
||||
}
|
||||
if (!verifyKeyedFingerprint(serializedConfiguration, envelope.configurationFingerprint)) {
|
||||
logger.debug("Ignoring session for a different integration configuration", { store: channelName });
|
||||
return null;
|
||||
}
|
||||
return envelope.value;
|
||||
} catch (error) {
|
||||
logger.warn("Failed to load session", { store: channelName, error });
|
||||
return null;
|
||||
@@ -31,7 +91,13 @@ export const createSessionStore = <TValue>(integration: { id: string }) => {
|
||||
async setAsync(value: TValue, options?: { ttlSeconds?: number }) {
|
||||
logger.debug("Updating session in store", { store: channelName, ttlSeconds: options?.ttlSeconds });
|
||||
try {
|
||||
await channel.setAsync(encryptSecret(superjson.stringify(value)), options);
|
||||
const envelope: SessionEnvelope<TValue> = {
|
||||
type: sessionEnvelopeType,
|
||||
version: sessionEnvelopeVersion,
|
||||
configurationFingerprint,
|
||||
value,
|
||||
};
|
||||
await channel.setAsync(encryptSecret(superjson.stringify(envelope)), options);
|
||||
} catch (error) {
|
||||
logger.error(new ErrorWithMetadata("Failed to save session", { store: channelName }, { cause: error }));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user