mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 22:50:47 +08:00
Internalize all IM channel implementations
This commit is contained in:
parent
8996476693
commit
bd469f58f8
95 changed files with 25012 additions and 100 deletions
84
plugin-src/host/channels/feishu/credential-store.mjs
Normal file
84
plugin-src/host/channels/feishu/credential-store.mjs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* 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;
|
||||
},
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue