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:
2026-04-30 15:47:26 +02:00
parent c8a5f4cd68
commit 1e9219d14a
35 changed files with 6217 additions and 0 deletions
+48
View File
@@ -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;
}