mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 05:20:46 +08:00
84 lines
3 KiB
JavaScript
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;
|
|
},
|
|
});
|
|
}
|