141 lines
4.7 KiB
JavaScript
141 lines
4.7 KiB
JavaScript
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { parseEnv } from 'node:util';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const DEFAULT_ENVIRONMENT_FILE = path.join(ROOT, 'pinokio', 'ENVIRONMENT');
|
|
|
|
export const PINOKIO_CONFIG_FIELDS = Object.freeze([
|
|
'GOOGLE_MAPS_API_KEY',
|
|
'CESIUM_ION_TOKEN',
|
|
'OPENAI_API_KEY',
|
|
'AISSTREAM_API_KEY',
|
|
'FIRMS_MAP_KEY',
|
|
'TOMTOM_API_KEY',
|
|
'OPENSKY_CLIENT_ID',
|
|
'OPENSKY_CLIENT_SECRET',
|
|
'LL2_API_TOKEN',
|
|
'GEV_RATELIMIT_OPENAI_PER_MIN',
|
|
'GEV_RATELIMIT_GOOGLE_PER_MIN',
|
|
'PINOKIO_SHARE_CLOUDFLARE',
|
|
'PINOKIO_SHARE_LOCAL',
|
|
'PINOKIO_SHARE_VAR',
|
|
]);
|
|
|
|
const PINOKIO_DEFAULTS = Object.freeze({
|
|
GEV_RATELIMIT_OPENAI_PER_MIN: '30',
|
|
GEV_RATELIMIT_GOOGLE_PER_MIN: '120',
|
|
PINOKIO_SHARE_CLOUDFLARE: 'false',
|
|
PINOKIO_SHARE_LOCAL: 'false',
|
|
PINOKIO_SHARE_VAR: '__gev_sharing_disabled__',
|
|
});
|
|
|
|
const PINOKIO_SHARE_SENTINEL = '__gev_sharing_disabled__';
|
|
const PINOKIO_SHARING_FIELDS = Object.freeze([
|
|
'PINOKIO_SHARE_CLOUDFLARE',
|
|
'PINOKIO_SHARE_LOCAL',
|
|
'PINOKIO_SHARE_VAR',
|
|
]);
|
|
|
|
function appendEnvironmentLine(source, line) {
|
|
const prefix = source.length > 0 && !source.endsWith('\n') ? '\n' : '';
|
|
return `${source}${prefix}${line}\n`;
|
|
}
|
|
|
|
function detectEnvironmentEncoding(buffer) {
|
|
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) return 'utf-16le';
|
|
if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) return 'utf-16be';
|
|
|
|
let evenNulls = 0;
|
|
let oddNulls = 0;
|
|
const sampleLength = Math.min(buffer.length, 512);
|
|
for (let index = 0; index < sampleLength; index += 1) {
|
|
if (buffer[index] !== 0) continue;
|
|
if (index % 2 === 0) evenNulls += 1;
|
|
else oddNulls += 1;
|
|
}
|
|
const minimumNulls = Math.max(2, Math.floor(sampleLength / 16));
|
|
if (oddNulls >= minimumNulls && oddNulls > evenNulls * 2) return 'utf-16le';
|
|
if (evenNulls >= minimumNulls && evenNulls > oddNulls * 2) return 'utf-16be';
|
|
return 'utf-8';
|
|
}
|
|
|
|
export function readEnvironmentSource(filepath) {
|
|
if (!existsSync(filepath)) return '';
|
|
const buffer = readFileSync(filepath);
|
|
try {
|
|
return new TextDecoder(detectEnvironmentEncoding(buffer), { fatal: true }).decode(buffer);
|
|
} catch {
|
|
throw new Error('Pinokio ENVIRONMENT could not be decoded as UTF-8 or UTF-16.');
|
|
}
|
|
}
|
|
|
|
/** Persist only the non-secret controls Pinokio itself re-reads at local.set. */
|
|
export function ensurePinokioSharingBoundary(filepath = DEFAULT_ENVIRONMENT_FILE) {
|
|
const original = existsSync(filepath) ? readFileSync(filepath) : null;
|
|
let source = readEnvironmentSource(filepath);
|
|
try {
|
|
if (source) parseEnv(source);
|
|
} catch {
|
|
throw new Error('Pinokio ENVIRONMENT could not be parsed.');
|
|
}
|
|
|
|
// Pinokio re-reads this file after the child preflight. Remove every legacy,
|
|
// blank, or duplicate control before appending one canonical safe block so
|
|
// that its later global/app merge cannot diverge from the checked state.
|
|
const sharingLine = new RegExp(
|
|
`^[\\t ]*(?:${PINOKIO_SHARING_FIELDS.join('|')})[\\t ]*=.*(?:\\r?\\n|$)`,
|
|
'gm',
|
|
);
|
|
source = source.replace(sharingLine, '');
|
|
source = appendEnvironmentLine(source, [
|
|
'PINOKIO_SHARE_CLOUDFLARE=false',
|
|
'PINOKIO_SHARE_LOCAL=false',
|
|
`PINOKIO_SHARE_VAR=${PINOKIO_SHARE_SENTINEL}`,
|
|
].join('\n'));
|
|
|
|
let configured;
|
|
try {
|
|
configured = parseEnv(source);
|
|
} catch {
|
|
throw new Error('Pinokio ENVIRONMENT could not be parsed.');
|
|
}
|
|
|
|
const encoded = Buffer.from(source, 'utf8');
|
|
if (!original || !original.equals(encoded)) {
|
|
writeFileSync(filepath, source, { mode: 0o600 });
|
|
}
|
|
return configured;
|
|
}
|
|
|
|
/** Read the app-scoped Pinokio configuration without exposing its values. */
|
|
export function readPinokioEnvironment(filepath = DEFAULT_ENVIRONMENT_FILE) {
|
|
if (!existsSync(filepath)) return {};
|
|
try {
|
|
return parseEnv(readEnvironmentSource(filepath));
|
|
} catch {
|
|
throw new Error('Pinokio ENVIRONMENT could not be parsed.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make the app-scoped Pinokio file authoritative over Pinokio-global values.
|
|
* Pinokio removes blank entries before merging environments, so each child
|
|
* must restore the raw app value before diagnosis or Vite configuration.
|
|
*/
|
|
export function applyPinokioEnvironment({
|
|
environment = process.env,
|
|
filepath = DEFAULT_ENVIRONMENT_FILE,
|
|
} = {}) {
|
|
const configured = ensurePinokioSharingBoundary(filepath);
|
|
for (const field of PINOKIO_CONFIG_FIELDS) {
|
|
environment[field] = String(configured[field] ?? PINOKIO_DEFAULTS[field] ?? '');
|
|
}
|
|
|
|
// Sharing is unsupported on Pinokio 8.0.40. Never let a global passcode
|
|
// enter the child even if the host's global Pinokio environment defines it.
|
|
environment.PINOKIO_SHARE_PASSCODE = '';
|
|
return configured;
|
|
}
|