mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
92 lines
3.2 KiB
TypeScript
92 lines
3.2 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { asDateTimestampMs } from "openclaw/plugin-sdk/number-runtime";
|
|
import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime";
|
|
import { COPILOT_INTEGRATION_ID } from "openclaw/plugin-sdk/provider-auth";
|
|
import { PUBLIC_GITHUB_COPILOT_DOMAIN } from "./domain.js";
|
|
|
|
export const COPILOT_TOKEN_CACHE_NAMESPACE = "token";
|
|
export const COPILOT_TOKEN_CACHE_MAX_ENTRIES = 8;
|
|
|
|
export type CachedCopilotToken = {
|
|
token: string;
|
|
expiresAt: number;
|
|
updatedAt: number;
|
|
integrationId?: string;
|
|
sourceCredentialFingerprint?: string;
|
|
domain?: string;
|
|
};
|
|
|
|
export function fingerprintCopilotSourceCredential(githubToken: string): string {
|
|
return createHash("sha256").update(githubToken).digest("hex");
|
|
}
|
|
|
|
export function isCopilotTokenUsable(params: {
|
|
cache: CachedCopilotToken;
|
|
domain: string;
|
|
sourceCredentialFingerprint: string;
|
|
now?: number;
|
|
}): boolean {
|
|
const expiresAt = asDateTimestampMs(params.cache.expiresAt);
|
|
// Pre-domain cache entries were public-only. Keep public upgrades warm while
|
|
// forcing tenant requests to exchange a tenant-scoped token.
|
|
const cacheDomain = params.cache.domain ?? PUBLIC_GITHUB_COPILOT_DOMAIN;
|
|
return (
|
|
params.cache.integrationId === COPILOT_INTEGRATION_ID &&
|
|
cacheDomain === params.domain &&
|
|
params.cache.sourceCredentialFingerprint === params.sourceCredentialFingerprint &&
|
|
expiresAt !== undefined &&
|
|
expiresAt - (params.now ?? Date.now()) > 5 * 60 * 1000
|
|
);
|
|
}
|
|
|
|
type CopilotTokenCache = {
|
|
path: string;
|
|
load(): CachedCopilotToken | undefined;
|
|
save(value: CachedCopilotToken): void;
|
|
};
|
|
|
|
export function resolveCopilotTokenCache(params: {
|
|
domain: string;
|
|
sourceCredentialFingerprint: string;
|
|
openCacheStore?: () => PluginStateSyncKeyedStore<CachedCopilotToken>;
|
|
cachePath?: string;
|
|
loadJsonFileImpl?: (path: string) => unknown;
|
|
saveJsonFileImpl?: (path: string, value: CachedCopilotToken) => void;
|
|
}): CopilotTokenCache {
|
|
const usesExplicitCacheAdapter =
|
|
params.cachePath !== undefined ||
|
|
params.loadJsonFileImpl !== undefined ||
|
|
params.saveJsonFileImpl !== undefined;
|
|
if (usesExplicitCacheAdapter) {
|
|
// Explicit file adapters are test/compat seams only. Runtime state is SQLite.
|
|
const cachePath = params.cachePath?.trim() || "explicit-cache";
|
|
const loadJsonFileFn = params.loadJsonFileImpl ?? (() => undefined);
|
|
const saveJsonFileFn = params.saveJsonFileImpl ?? (() => undefined);
|
|
return {
|
|
path: cachePath,
|
|
load: () => loadJsonFileFn(cachePath) as CachedCopilotToken | undefined,
|
|
save: (value) => saveJsonFileFn(cachePath, value),
|
|
};
|
|
}
|
|
|
|
const store = params.openCacheStore?.();
|
|
if (!store) {
|
|
// Direct live/tests may call the provider helper without plugin registration.
|
|
// They exchange normally but do not persist runtime state outside SQLite.
|
|
return {
|
|
path: "uncached",
|
|
load: () => undefined,
|
|
save: () => undefined,
|
|
};
|
|
}
|
|
const key = `${params.domain}:${params.sourceCredentialFingerprint}`;
|
|
return {
|
|
path: "plugin-state",
|
|
load: () => store.lookup(key),
|
|
save: (value) =>
|
|
store.register(key, value, {
|
|
ttlMs: Math.max(1, value.expiresAt - Date.now()),
|
|
}),
|
|
};
|
|
}
|