Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

424 lines
13 KiB
TypeScript

/**
* Persists OAuth credential material for a connector account and reads the
* resulting refs back out. `persistConnectorCredentialRefs` writes each secret
* to the first available durable vault (connector credential store, vault, or
* SECRETS) and records a `vaultRef` pointer on the account via storage; it
* refuses to proceed unless both a vault writer and a ref writer exist, so an
* account is never marked connected without durable credentials.
* `credentialRefRecordsFromMetadata` is the read side, extracting ref records
* from account metadata for the credential resolver. Consumed by the connector
* account provider on OAuth completion and by `DefaultGoogleCredentialResolver`.
*/
import {
CONNECTOR_ACCOUNT_STORAGE_SERVICE_TYPE,
type ConnectorAccountManager,
type IAgentRuntime,
} from "@elizaos/core";
type JsonValue =
| string
| number
| boolean
| null
| undefined
| JsonValue[]
| { readonly [key: string]: JsonValue };
type JsonRecord = Record<string, JsonValue>;
export interface ConnectorCredentialRefMetadata extends JsonRecord {
credentialType: string;
vaultRef: string;
expiresAt?: number;
metadata?: JsonRecord;
}
export interface ConnectorCredentialRefRecordLike {
credentialType: string;
vaultRef?: string | null;
metadata?: JsonRecord | null;
expiresAt?: number | string | Date | null;
updatedAt?: number | string | Date | null;
version?: string | number | null;
}
export interface ConnectorCredentialPersistResult {
refs: ConnectorCredentialRefMetadata[];
vaultAvailable: boolean;
storageAvailable: boolean;
}
interface ConnectorCredentialInput {
credentialType: string;
value: string;
expiresAt?: number;
metadata?: JsonRecord;
}
interface PersistConnectorCredentialRefsParams {
runtime: IAgentRuntime;
manager?: ConnectorAccountManager;
provider: string;
accountIdForRef: string;
storageAccountId?: string;
credentials: ConnectorCredentialInput[];
caller: string;
}
type VaultWriter = {
name: string;
write: (vaultRef: string, credential: ConnectorCredentialInput) => Promise<string>;
};
type CredentialRefWriter = {
name: string;
write: (ref: ConnectorCredentialRefMetadata) => Promise<void>;
};
export async function persistConnectorCredentialRefs(
params: PersistConnectorCredentialRefsParams
): Promise<ConnectorCredentialPersistResult> {
const refs: ConnectorCredentialRefMetadata[] = [];
const vaultWriters = resolveVaultWriters(params.runtime, {
provider: params.provider,
accountId: params.accountIdForRef,
caller: params.caller,
});
if (vaultWriters.length === 0) {
throw new Error(
`No durable connector credential store or vault writer is available for ${params.provider} account ${params.accountIdForRef}. Refusing to mark OAuth account connected without persisted credentials.`
);
}
if (!params.storageAccountId) {
throw new Error(
`No durable connector account id is available for ${params.provider} account ${params.accountIdForRef}. Refusing to mark OAuth account connected without persisted credential refs.`
);
}
const storageWriters = resolveCredentialRefWriters(
params.runtime,
params.manager,
params.storageAccountId
);
if (storageWriters.length === 0) {
throw new Error(
`No durable connector credential ref writer is available for ${params.provider} account ${params.storageAccountId}. Refusing to mark OAuth account connected without persisted credential refs.`
);
}
for (const credential of params.credentials) {
const plannedRef = buildConnectorCredentialVaultRef({
agentId: nonEmptyString(params.runtime.agentId) ?? "agent",
provider: params.provider,
accountId: params.accountIdForRef,
credentialType: credential.credentialType,
});
const vaultRef = await writeWithFirstAvailableVault(vaultWriters, plannedRef, credential);
refs.push({
credentialType: credential.credentialType,
vaultRef,
...(credential.expiresAt !== undefined ? { expiresAt: credential.expiresAt } : {}),
...(credential.metadata ? { metadata: credential.metadata } : {}),
});
}
if (refs.length > 0) {
await writeRefsToStorage(storageWriters, refs);
}
return {
refs,
vaultAvailable: vaultWriters.length > 0,
storageAvailable: storageWriters.length > 0,
};
}
export function credentialRefRecordsFromMetadata(
metadata: unknown
): ConnectorCredentialRefRecordLike[] {
const record = asRecord(metadata);
if (!record) return [];
const oauth = asRecord(record.oauth);
return [
...credentialRefsFromUnknown(record.credentialRefs),
...credentialRefsFromUnknown(record.oauthCredentialRefs),
...credentialRefsFromUnknown(oauth?.credentialRefs),
];
}
function credentialRefsFromUnknown(value: unknown): ConnectorCredentialRefRecordLike[] {
if (Array.isArray(value)) {
return value.flatMap((entry) => {
const ref = credentialRefFromRecord(asRecord(entry));
return ref ? [ref] : [];
});
}
const record = asRecord(value);
if (!record) return [];
return Object.entries(record).flatMap(([credentialType, entry]) => {
const entryRecord = asRecord(entry);
if (entryRecord) {
const ref = credentialRefFromRecord({
credentialType,
...entryRecord,
});
return ref ? [ref] : [];
}
const vaultRef = nonEmptyString(entry);
return vaultRef ? [{ credentialType, vaultRef }] : [];
});
}
function credentialRefFromRecord(
record: JsonRecord | undefined
): ConnectorCredentialRefRecordLike | null {
if (!record) return null;
const credentialType = nonEmptyString(record.credentialType ?? record.type ?? record.name);
const vaultRef = nonEmptyString(record.vaultRef ?? record.ref);
if (!credentialType || !vaultRef) return null;
return {
credentialType,
vaultRef,
metadata: asRecord(record.metadata) ?? null,
expiresAt: record.expiresAt as ConnectorCredentialRefRecordLike["expiresAt"],
updatedAt: record.updatedAt as ConnectorCredentialRefRecordLike["updatedAt"],
version: (record.version ??
record.credentialVersion) as ConnectorCredentialRefRecordLike["version"],
};
}
function resolveVaultWriters(
runtime: IAgentRuntime,
context: { provider: string; accountId: string; caller: string }
): VaultWriter[] {
const writers: VaultWriter[] = [];
const credentialStore = getFirstService(runtime, [
"connector_credential_store",
"CONNECTOR_CREDENTIAL_STORE",
"connectorCredentialStore",
"credential_store",
]) as {
putSecret?: (params: {
vaultRef?: string;
agentId: string;
provider: string;
accountId: string;
credentialType: string;
value: string;
caller?: string;
}) => Promise<string> | string;
} | null;
if (typeof credentialStore?.putSecret === "function") {
writers.push({
name: "connector_credential_store",
write: async (vaultRef, credential) =>
credentialStore.putSecret?.({
vaultRef,
agentId: nonEmptyString(runtime.agentId) ?? "agent",
provider: context.provider,
accountId: context.accountId,
credentialType: credential.credentialType,
value: credential.value,
caller: context.caller,
}) ?? vaultRef,
});
}
const vault = getFirstService(runtime, ["vault", "VAULT"]) as {
set?: (
key: string,
value: string,
options?: { sensitive?: boolean; caller?: string }
) => Promise<void> | void;
} | null;
if (typeof vault?.set === "function") {
writers.push({
name: "vault",
write: async (vaultRef, credential) => {
await vault.set?.(vaultRef, credential.value, {
sensitive: true,
caller: context.caller,
});
return vaultRef;
},
});
}
const secrets = getService(runtime, "SECRETS") as {
setGlobal?: (
key: string,
value: string,
config?: { sensitive?: boolean }
) => Promise<boolean> | boolean;
set?: (
key: string,
value: string,
context: JsonRecord,
config?: { sensitive?: boolean }
) => Promise<boolean> | boolean;
} | null;
if (typeof secrets?.setGlobal === "function" || typeof secrets?.set === "function") {
writers.push({
name: "SECRETS",
write: async (vaultRef, credential) => {
if (typeof secrets.setGlobal === "function") {
await secrets.setGlobal(vaultRef, credential.value, { sensitive: true });
return vaultRef;
}
await secrets.set?.(
vaultRef,
credential.value,
{ level: "global", agentId: runtime.agentId, requesterId: runtime.agentId },
{ sensitive: true }
);
return vaultRef;
},
});
}
return writers;
}
function resolveCredentialRefWriters(
runtime: IAgentRuntime,
manager: ConnectorAccountManager | undefined,
accountId: string
): CredentialRefWriter[] {
const candidates = [
manager?.getStorage?.(),
getService(runtime, CONNECTOR_ACCOUNT_STORAGE_SERVICE_TYPE),
(runtime as { adapter?: unknown }).adapter,
].filter(Boolean);
const writers: CredentialRefWriter[] = [];
for (const candidate of candidates) {
const writer = candidate as {
setConnectorAccountCredentialRef?: (params: {
accountId: string;
credentialType: string;
vaultRef: string;
metadata?: JsonRecord;
expiresAt?: number;
}) => Promise<unknown> | unknown;
setCredentialRef?: (params: {
accountId: string;
credentialType: string;
vaultRef: string;
metadata?: JsonRecord;
expiresAt?: number;
}) => Promise<unknown> | unknown;
};
if (typeof writer.setConnectorAccountCredentialRef === "function") {
writers.push({
name: "setConnectorAccountCredentialRef",
write: async (ref) => {
await writer.setConnectorAccountCredentialRef?.({
accountId,
credentialType: ref.credentialType,
vaultRef: ref.vaultRef,
...(ref.metadata ? { metadata: ref.metadata } : {}),
...(ref.expiresAt !== undefined ? { expiresAt: ref.expiresAt } : {}),
});
},
});
} else if (typeof writer.setCredentialRef === "function") {
writers.push({
name: "setCredentialRef",
write: async (ref) => {
await writer.setCredentialRef?.({
accountId,
credentialType: ref.credentialType,
vaultRef: ref.vaultRef,
...(ref.metadata ? { metadata: ref.metadata } : {}),
...(ref.expiresAt !== undefined ? { expiresAt: ref.expiresAt } : {}),
});
},
});
}
}
return writers;
}
async function writeWithFirstAvailableVault(
writers: VaultWriter[],
plannedRef: string,
credential: ConnectorCredentialInput
): Promise<string> {
const errors: string[] = [];
for (const writer of writers) {
try {
return await writer.write(plannedRef, credential);
} catch (error) {
errors.push(`${writer.name}: ${error instanceof Error ? error.message : String(error)}`);
}
}
throw new Error(`Failed to persist connector credential ref ${plannedRef}: ${errors.join("; ")}`);
}
async function writeRefsToStorage(
writers: CredentialRefWriter[],
refs: ConnectorCredentialRefMetadata[]
): Promise<void> {
const errors: string[] = [];
for (const writer of writers) {
try {
for (const ref of refs) {
await writer.write(ref);
}
return;
} catch (error) {
errors.push(`${writer.name}: ${error instanceof Error ? error.message : String(error)}`);
}
}
throw new Error(`Failed to persist connector credential refs: ${errors.join("; ")}`);
}
function buildConnectorCredentialVaultRef(params: {
agentId: string;
provider: string;
accountId: string;
credentialType: string;
}): string {
return [
"connector",
normalizeVaultSegment(params.agentId),
normalizeVaultSegment(params.provider),
normalizeVaultSegment(params.accountId),
normalizeVaultSegment(params.credentialType),
].join(".");
}
function normalizeVaultSegment(value: string): string {
const normalized = value
.trim()
.replace(/[^a-zA-Z0-9_-]+/g, "_")
.replace(/^_+|_+$/g, "");
return (normalized || "unknown").slice(0, 64);
}
function getFirstService(runtime: IAgentRuntime, serviceTypes: readonly string[]): unknown {
for (const serviceType of serviceTypes) {
const service = getService(runtime, serviceType);
if (service) return service;
}
return null;
}
function getService(runtime: IAgentRuntime, serviceType: string): unknown {
try {
return runtime.getService?.(serviceType) ?? null;
} catch {
return null;
}
}
function asRecord(value: unknown): JsonRecord | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as JsonRecord)
: undefined;
}
function nonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}