Implement Phase 1 tasks 1.1-1.4 (scaffold + core shell + Teltonika framing)
- Project scaffold (Node 22 + TS 5 + pnpm + vitest + ESLint flat config) - Core shell: TCP server, session loop, adapter registry, types - Configuration (zod-validated env) and pino logger - Teltonika adapter: IMEI handshake, frame envelope, CRC-16/IBM, codec dispatch registry, DeviceAuthority seam (AllowAllAuthority default) Codec data parsers (1.5-1.7), Redis publisher (1.8), and downstream tasks remain. 36 tests covering CRC, framing, handshake, device authority, config, and core server. typecheck/lint/test/build all clean.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { z } from 'zod';
|
||||
|
||||
const ConfigSchema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
||||
INSTANCE_ID: z.string().min(1).default(() => `local-${randomUUID().slice(0, 8)}`),
|
||||
LOG_LEVEL: z
|
||||
.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace'])
|
||||
.default('info'),
|
||||
|
||||
// Vendor port bindings — extend as adapters are added
|
||||
TELTONIKA_PORT: z.coerce.number().int().min(1).max(65535).default(5027),
|
||||
|
||||
// Redis — required in all environments; no silent default
|
||||
REDIS_URL: z.string().url(),
|
||||
REDIS_TELEMETRY_STREAM: z.string().min(1).default('telemetry:teltonika'),
|
||||
REDIS_STREAM_MAXLEN: z.coerce.number().int().min(0).default(1_000_000),
|
||||
|
||||
// Observability
|
||||
METRICS_PORT: z.coerce.number().int().min(0).max(65535).default(9090),
|
||||
|
||||
// Device authority — off by default; opt-in for strict reject-on-unknown
|
||||
STRICT_DEVICE_AUTH: z
|
||||
.string()
|
||||
.transform((v) => v === 'true' || v === '1')
|
||||
.default('false'),
|
||||
});
|
||||
|
||||
export type Config = z.infer<typeof ConfigSchema>;
|
||||
|
||||
/**
|
||||
* Validates process.env at startup and returns a typed Config.
|
||||
* Throws with a human-readable error listing every missing/invalid key if
|
||||
* validation fails — the intent is loud, fast failure rather than running
|
||||
* with bad configuration.
|
||||
*/
|
||||
export function loadConfig(env: Record<string, string | undefined> = process.env): Config {
|
||||
const result = ConfigSchema.safeParse(env);
|
||||
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues
|
||||
.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`)
|
||||
.join('\n');
|
||||
throw new Error(`Configuration error — invalid or missing environment variables:\n${issues}`);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
Reference in New Issue
Block a user