⚙️ Enhance caching mechanism and add refresh option for secret retrieval

This commit is contained in:
2026-04-06 13:24:01 +08:00
parent 7689b6be11
commit aef9c77aee
2 changed files with 137 additions and 18 deletions

View File

@@ -1,15 +1,23 @@
// Reference:
// - https://docs.deno.com/runtime/fundamentals/testing/
import {decodeBase64, encodeBase64} from "jsr:@std/encoding/base64";
import {encodeBase32} from "jsr:@std/encoding/base32";
import {dirname, fromFileUrl} from "jsr:@std/path";
import {toArrayBuffer} from "jsr:@std/streams";
import {spawn, SpawnOptionsWithoutStdio} from "node:child_process";
import {createWriteStream, mkdir, readFile, readFileSync, rm, stat, writeFile,} from "node:fs";
import {pipeline} from "node:stream";
import {promisify} from "node:util";
import {getRandomValues} from "node:crypto";
import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64";
import { encodeBase32 } from "jsr:@std/encoding/base32";
import { dirname, fromFileUrl } from "jsr:@std/path";
import { toArrayBuffer } from "jsr:@std/streams";
import { spawn, SpawnOptionsWithoutStdio } from "node:child_process";
import {
createWriteStream,
mkdir,
readFile,
readFileSync,
rm,
stat,
writeFile,
} from "node:fs";
import { pipeline } from "node:stream";
import { promisify } from "node:util";
import { getRandomValues } from "node:crypto";
// reference: https://docs.deno.com/examples/hex_base64_encoding/
// import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64";
@@ -709,7 +717,8 @@ class Logger {
__debug: boolean = false;
constructor() {
this.__debug = osEnv("LOGGER") === "*";
this.__debug = osEnv("LOGGER") === "*" ||
osEnv("DENO_COMMONS_LOGGER") === "*";
}
// deno-lint-ignore no-explicit-any
@@ -1080,12 +1089,11 @@ export async function decryptDefault(
const key = await __getMasterEncryptionKey();
const nonce = input.slice(0, 12);
const ciphertextWithTag = input.slice(12);
const plaintext = await crypto.subtle.decrypt(
return await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce },
key,
ciphertextWithTag,
);
return plaintext;
}
export async function decryptDefaultAsString(
@@ -1095,6 +1103,94 @@ export async function decryptDefaultAsString(
return new TextDecoder().decode(plaintext);
}
interface LocalCacheOptions {
basePath?: string;
tryRefresh?: boolean;
defaultCacheTimeSecs?: number;
maxCacheTimeSecs?: number;
}
interface LocalCacheValue<T> {
value: T;
cacheTimestampSecs: number;
}
export async function readWithLocalCache<T>(
cacheKey: string,
read: () => Promise<T>,
options?: LocalCacheOptions,
): Promise<T> {
if (!cacheKey || !read) {
throw new Error("Key and read callback function are both required.");
}
const cacheKeyHash = await sha256AndBase32OfString(cacheKey);
const basePath = options?.basePath ?? "~/.cache/local-cache";
const cacheDir = joinPath(
basePath,
cacheKeyHash.substring(0, 2),
cacheKeyHash.substring(2, 4),
);
if (!await existsPath(cacheDir)) {
await makeDirectory(cacheDir, true);
}
const cacheFile = joinPath(cacheDir, cacheKeyHash);
log.debug(`key: ${cacheKey} --> ${cacheFile}`);
const cachedContent = await readFileToString(cacheFile);
let cachedValue: LocalCacheValue<T> | null = null;
if (cachedContent) {
try {
cachedValue = JSON.parse(
await decryptDefaultAsString(cachedContent),
) as LocalCacheValue<T>;
log.debug(`key: ${cacheKey}, cached value:`, cachedValue);
} catch (e) {
log.debug(`key :${cacheKey}, decrypt error`, e);
}
}
const nowSecs = Math.floor(Date.now() / 1000);
if (cachedValue && options.defaultCacheTimeSecs && !options?.tryRefresh) {
if (
(nowSecs - cachedValue.cacheTimestampSecs) <
options.defaultCacheTimeSecs
) {
log.debug(
`key: ${cacheKey} cache valid, now: ${nowSecs}, cacheTimestampSecs: ${cachedValue.cacheTimestampSecs}, default cache time secs: ${options.defaultCacheTimeSecs}`,
);
return cachedValue.value;
}
}
let lastError = null;
try {
const value = await read();
await writeStringToFile(
cacheFile,
await encryptDefaultAsBase64(
JSON.stringify({
value: value,
cacheTimestampSecs: nowSecs,
} as LocalCacheValue<T>),
),
);
return value;
} catch (e) {
lastError = e;
log.debug("error in read", e);
}
if (cachedValue && options.maxCacheTimeSecs) {
if (
(nowSecs - cachedValue.cacheTimestampSecs) <
options.maxCacheTimeSecs
) {
log.debug(
`key: ${cacheKey} fallback cached value, now: ${nowSecs}, cacheTimestampSecs: ${cachedValue.cacheTimestampSecs}, max cache time secs: ${options.maxCacheTimeSecs}`,
);
return cachedValue.value;
}
}
throw lastError;
}
export async function getKeyRingPassword(
service: string,
user: string,
@@ -1535,6 +1631,8 @@ export type SecretValueRunEnv = "ALIBABA_CLOUD" | "HATTER_CLI";
export interface GetSecretValueOptions {
runEnv?: SecretValueRunEnv;
localCache?: boolean;
tryRefresh?: boolean;
}
export function getRunEnv(): stirng | null {
@@ -1555,11 +1653,10 @@ function toGetSecretValueOptions(
return runEnvOrOptions as GetSecretValueOptions;
}
export async function getSecretValue(
async function __getSecretValue(
key: string,
runEnvOrOptions?: SecretValueRunEnv | GetSecretValueOptions,
options?: GetSecretValueOptions,
): Promise<string> {
const options = toGetSecretValueOptions(runEnvOrOptions);
const runEnv = options?.runEnv ?? getRunEnv();
if (runEnv == "ALIBABA_CLOUD") {
return await getSecretValueViaAlibabaCloudInstanceIdentity(key);
@@ -1567,6 +1664,24 @@ export async function getSecretValue(
return await getSecretValueViaHatterCli(key);
}
export async function getSecretValue(
key: string,
runEnvOrOptions?: SecretValueRunEnv | GetSecretValueOptions,
): Promise<string> {
const options = toGetSecretValueOptions(runEnvOrOptions);
return await readWithLocalCache(
`secret_value:${key}`,
async (): Promise<string> => {
return await __getSecretValue(key, options);
},
{
tryRefresh: options.tryRefresh,
defaultCacheTimeSecs: 3600,
maxCacheTimeSecs: 3600 * 24,
},
);
}
export interface StsToken {
mode: string;
expiration: string;