mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-08 22:00:50 +08:00
Localize WhatsApp access-grant approvals via t() and push Host language from the UI on load and locale change. Co-authored-by: Cursor <cursoragent@cursor.com>
73 lines
3.4 KiB
JavaScript
73 lines
3.4 KiB
JavaScript
import { getImHostLanguage, setImHostLanguage } from '../../src/channels/shared/i18n.mjs';
|
|
import { createUpdateRuntime } from './update-runtime.mjs';
|
|
import { createUpdateService } from './update-service.mjs';
|
|
import { installConnectionRpcChannel } from './connection-rpc-mount.mjs';
|
|
import { resolveRpcAuthority } from './rpc-authority.mjs';
|
|
|
|
export const UPDATE_RPC_CHANNEL = '/dsh-im';
|
|
export const UPDATE_ENDPOINTS = Object.freeze(['update.status', 'update.check', 'update.install']);
|
|
export const HOST_LANGUAGE_SET_ENDPOINT = 'host.language.set';
|
|
|
|
function validLanguagePayload(payload) {
|
|
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return false;
|
|
const keys = Object.keys(payload);
|
|
return keys.length === 1
|
|
&& keys[0] === 'language'
|
|
&& typeof payload.language === 'string'
|
|
&& payload.language.trim().length > 0
|
|
&& payload.language.length <= 32;
|
|
}
|
|
|
|
function validPayload(endpoint, payload) {
|
|
if (endpoint === HOST_LANGUAGE_SET_ENDPOINT) return validLanguagePayload(payload);
|
|
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return false;
|
|
const keys = Object.keys(payload);
|
|
if (endpoint !== 'update.install') return keys.length === 0;
|
|
return keys.length === 2 && keys.every((key) => key === 'checkId' || key === 'requestId')
|
|
&& ['checkId', 'requestId'].every((key) => typeof payload[key] === 'string'
|
|
&& /^[A-Za-z0-9_-]{1,128}$/.test(payload[key]));
|
|
}
|
|
|
|
const PUBLIC_ERRORS = new Set([
|
|
'check-failed', 'invalid-release', 'check-expired', 'installation-changed', 'update-busy',
|
|
'install-failed', 'verify-failed', 'state-unavailable', 'interrupted', 'disposed',
|
|
'source-install', 'unknown-profile', 'unsupported-runtime', 'registry-conflict',
|
|
'incompatible-node', 'pending-restart', 'recovery-required', 'executor-unavailable',
|
|
'invalid-installation', 'registry-check-failed', 'install-timeout', 'install-interrupted', 'invalid-version',
|
|
]);
|
|
|
|
export function createUpdateRpcHandler(service) {
|
|
return async (endpoint, payload, signal) => {
|
|
if (endpoint !== HOST_LANGUAGE_SET_ENDPOINT && !UPDATE_ENDPOINTS.includes(endpoint)
|
|
|| !validPayload(endpoint, payload)) {
|
|
return { ok: false, error: { code: 'bad-request', message: 'Invalid update request.' } };
|
|
}
|
|
if (endpoint === HOST_LANGUAGE_SET_ENDPOINT) {
|
|
setImHostLanguage(payload.language);
|
|
return { ok: true, value: { language: getImHostLanguage() } };
|
|
}
|
|
if (signal?.aborted) return { ok: false, error: { code: 'cancelled', message: 'Request cancelled.' } };
|
|
try {
|
|
// The submitted install belongs to the Host, not the lifetime of this browser request.
|
|
const value = endpoint === 'update.install' ? await service.install(payload)
|
|
: endpoint === 'update.check' ? await service.check() : await service.status();
|
|
return { ok: true, value };
|
|
} catch (error) {
|
|
const code = PUBLIC_ERRORS.has(error?.code) ? error.code : 'update-failed';
|
|
return { ok: false, error: { code, message: code } };
|
|
}
|
|
};
|
|
}
|
|
|
|
export function installUpdateRpc(ctx, options = {}) {
|
|
const runtime = options.runtime ?? createUpdateRuntime({ ctx, moduleUrl: import.meta.url });
|
|
const service = options.service ?? createUpdateService({ runtime });
|
|
resolveRpcAuthority('loopback');
|
|
const dispose = installConnectionRpcChannel(
|
|
ctx,
|
|
UPDATE_RPC_CHANNEL,
|
|
createUpdateRpcHandler(service),
|
|
);
|
|
ctx.effect(() => () => service.close(), 'dsh-im: close update installer');
|
|
return dispose;
|
|
}
|