dsh-im-ops/plugin-src/host/channels/feishu/credential-store.mjs
2026-08-15 15:40:53 +08:00

84 lines
3 KiB
JavaScript

/**
* Credential references owned by the Feishu Host plugin. They deliberately
* use DSH's credential provider instead of plugin settings, so the browser's
* configuration plane can only observe configured/source metadata.
*/
export const FEISHU_APP_ID_REF = 'DSH_FEISHU_APP_ID';
export const FEISHU_APP_SECRET_REF = 'DSH_FEISHU_APP_SECRET';
function assertNonEmptyString(value, label) {
if (typeof value !== 'string' || value.length === 0) {
throw new TypeError(`${label} must be a non-empty string`);
}
return value;
}
async function restore(provider, ref, previous) {
try {
if (previous?.value) await provider.set(ref, previous.value);
else await provider.unset(ref);
} catch {
// Preserve the original write failure. The provider remains the source
// of truth and will report the partial state through describe().
}
}
/**
* Adapt the real DSH `ctx.credentials` seam to the small interface consumed
* by the Feishu controller. Secret values only travel Host-to-Host here.
*
* @param {{resolve(Function), describe(Function), set(Function), unset(Function)}} provider
* @param {{appIdRef?: string, appSecretRef?: string}} options
*/
export function createDshCredentialStore(provider, options = {}) {
if (!provider
|| typeof provider.resolve !== 'function'
|| typeof provider.describe !== 'function'
|| typeof provider.set !== 'function'
|| typeof provider.unset !== 'function') {
throw new TypeError('A DSH credential provider is required');
}
const appIdRef = options.appIdRef ?? FEISHU_APP_ID_REF;
const appSecretRef = options.appSecretRef ?? FEISHU_APP_SECRET_REF;
return Object.freeze({
async save({ appId, appSecret }) {
const nextId = assertNonEmptyString(appId, 'Feishu App ID');
const nextSecret = assertNonEmptyString(appSecret, 'Feishu App Secret');
const [previousId, previousSecret] = await Promise.all([
provider.resolve(appIdRef),
provider.resolve(appSecretRef),
]);
try {
// Store the secret first. An App ID without its matching secret must
// never be treated as a usable integration.
await provider.set(appSecretRef, nextSecret);
await provider.set(appIdRef, nextId);
} catch {
await restore(provider, appSecretRef, previousSecret);
await restore(provider, appIdRef, previousId);
throw new Error('Unable to store the Feishu credentials.');
}
},
async clear() {
const outcomes = await Promise.allSettled([
provider.unset(appIdRef),
provider.unset(appSecretRef),
]);
if (outcomes.some((outcome) => outcome.status === 'rejected')) {
throw new Error('Unable to remove the Feishu credentials.');
}
},
async configured() {
const [appId, appSecret] = await Promise.all([
provider.describe(appIdRef),
provider.describe(appSecretRef),
]);
return appId.configured === true && appSecret.configured === true;
},
});
}