VulpesOS/services/settings.mjs
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

97 lines
3.4 KiB
JavaScript

// No Gecko, XPCOM, DOM or filesystem imports in the service contract.
export const API_VERSION = 1;
export class ServiceError extends Error {
constructor(code) {
super(code);
this.code = code;
}
}
function key(value) {
if (
typeof value !== 'string' ||
!value ||
value.length > 128 ||
['__proto__', 'constructor', 'prototype'].includes(value)
) {
throw new ServiceError('INVALID_KEY');
}
return value;
}
// v1 transports JSON only. Binary images will need a separate attachment contract.
function jsonValue(value, depth = 0) {
if (depth > 20) throw new ServiceError('INVALID_VALUE');
if (value === null || typeof value === 'boolean' || typeof value === 'string') return;
if (typeof value === 'number' && Number.isFinite(value)) return;
if (Array.isArray(value)) {
value.forEach((v) => jsonValue(v, depth + 1));
return;
}
if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
for (const [k, v] of Object.entries(value)) {
key(k);
jsonValue(v, depth + 1);
}
return;
}
throw new ServiceError('INVALID_VALUE');
}
export class SettingsService {
constructor(storage, { maxBytes = 1024 * 1024 } = {}) {
this.storage = storage;
this.maxBytes = maxBytes;
}
// Only a trusted host adapter may bind a session. Never take identity or
// permissions from an application's RPC payload.
bindSession(identity, permissions) {
if (typeof identity !== 'string' || !identity) throw new ServiceError('INVALID_IDENTITY');
const grants = new Set(permissions);
return async (request) => {
const id = request && request.id;
try {
if (!request || request.version !== API_VERSION)
throw new ServiceError('API_VERSION_UNSUPPORTED');
if (!Number.isSafeInteger(id) || id < 0) throw new ServiceError('INVALID_REQUEST');
const { method, params } = request;
if (method === 'settings.get') {
if (!grants.has('settings.read')) throw new ServiceError('PERMISSION_DENIED');
const name = key(params && params.key);
const value = await this.storage.get(name);
return {
version: API_VERSION,
id,
result: { found: value !== undefined, value: value === undefined ? null : value },
};
}
if (method === 'settings.set') {
if (!grants.has('settings.write')) throw new ServiceError('PERMISSION_DENIED');
const values = params && params.values;
if (!values || Object.getPrototypeOf(values) !== Object.prototype)
throw new ServiceError('INVALID_VALUE');
const entries = Object.entries(values);
if (!entries.length || entries.length > 128) throw new ServiceError('INVALID_VALUE');
// Validate the entire request before opening a write transaction.
for (const [k, v] of entries) {
key(k);
jsonValue(v);
}
if (JSON.stringify(values).length > this.maxBytes)
throw new ServiceError('VALUE_TOO_LARGE');
await this.storage.setMany(entries);
return { version: API_VERSION, id, result: { committed: true } };
}
throw new ServiceError('METHOD_NOT_SUPPORTED');
} catch (error) {
return {
version: API_VERSION,
id,
error: { code: error instanceof ServiceError ? error.code : 'STORAGE_ERROR' },
};
}
};
}
}