(opts: BootOptions = {})
| 372 | } |
| 373 | |
| 374 | export async function boot(opts: BootOptions = {}): Promise<{ |
| 375 | stop: () => Promise<void>; |
| 376 | port: number; |
| 377 | /** |
| 378 | * Exposed for tests (R21 bucket-a): subscribers can observe |
| 379 | * `rules.reload.failed` etc. without reaching into module-private state. |
| 380 | * Production callers should not rely on this — it's an escape hatch and |
| 381 | * may tighten in the future. |
| 382 | */ |
| 383 | bus: ReturnType<typeof createEventBus>; |
| 384 | }> { |
| 385 | // R1-F2: hoist all fail-loud config validation to the TOP of boot() — |
| 386 | // BEFORE any pb client / bus / scheduler / writer / S3 uploader allocations. |
| 387 | // Pre-fix `loadWebhookSecrets` lived AFTER the entire scheduler+writer |
| 388 | // setup, and `POCKETBASE_URL` had a narrower predicate than SHARED_SECRET |
| 389 | // (only NODE_ENV=production), so a misconfigured staging boot allocated |
| 390 | // expensive resources, mounted file watchers, and registered scheduler |
| 391 | // entries before throwing. Now both checks fire here, before anything that |
| 392 | // would need teardown. |
| 393 | // |
| 394 | // R1-F3: `loadPocketbaseUrl` replaces the inline production-only guard |
| 395 | // with the same test-or-escape-hatch predicate `loadWebhookSecrets` uses, |
| 396 | // so staging/development/unset NODE_ENV fail loud on BOTH config misses |
| 397 | // instead of just SHARED_SECRET. |
| 398 | // |
| 399 | // R3-F1: hoist `OPS_TRIGGER_TOKEN` set-but-empty fail-loud here too. |
| 400 | // Pre-fix this check lived AFTER pb / bus / scheduler / writer / S3 |
| 401 | // uploader allocations, so a typo'd `OPS_TRIGGER_TOKEN=` allocated |
| 402 | // expensive resources before throwing. Now all three fail-loud config |
| 403 | // predicates fire at the top, before anything that would need teardown. |
| 404 | const pbUrl = loadPocketbaseUrl(logger); |
| 405 | const webhookSecrets = loadWebhookSecrets(logger); |
| 406 | const triggerToken = loadOpsTriggerToken(logger); |
| 407 | // These authenticate against `/api/collections/_superusers/auth-with-password` |
| 408 | // in pb-client, so the env var names intentionally mirror "superuser" — |
| 409 | // previously `POCKETBASE_WRITER_*`, renamed to eliminate the naming drift |
| 410 | // between auth endpoint and env contract. |
| 411 | const email = process.env.POCKETBASE_SUPERUSER_EMAIL; |
| 412 | const password = process.env.POCKETBASE_SUPERUSER_PASSWORD; |
| 413 | |
| 414 | const pb = createPbClient({ url: pbUrl, email, password, logger }); |
| 415 | const bus = createEventBus(); |
| 416 | const renderer = createRenderer(); |
| 417 | const stateStore = createAlertStateStore(pb); |
| 418 | // Writer identity: this is the legacy monolith scheduler's writer — the |
| 419 | // identity the cross-writer flip warn attributes legacy-vs-fleet fights to. |
| 420 | const writer = createStatusWriter({ pb, bus, logger, writtenBy: "legacy" }); |
| 421 | const scheduler = createScheduler({ logger }); |
| 422 | const metrics = createMetricsRegistry(); |
| 423 | |
| 424 | // Track all bus subscriptions so stop() can release them on repeated boot/stop. |
| 425 | const busUnsubs: Array<() => void> = []; |
| 426 | |
| 427 | // Observability: increment counters on bus events so /metrics stays fresh. |
| 428 | busUnsubs.push(bus.on("rules.reloaded", () => metrics.inc("rule_reloads"))); |
| 429 | // Ticks once per `status.changed` emit. NOT strictly 1:1 with probe runs: |
| 430 | // besides each successfully-written durable result, an ERROR tick whose |
| 431 | // observed_at refresh persisted on an existing row also emits |
no test coverage detected
searching dependent graphs…