@cogenta/fleet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/agent/assert.d.ts +9 -0
  2. package/dist/agent/assert.d.ts.map +1 -0
  3. package/dist/agent/assert.js +64 -0
  4. package/dist/agent/assert.js.map +1 -0
  5. package/dist/agent/build.d.ts +21 -0
  6. package/dist/agent/build.d.ts.map +1 -0
  7. package/dist/agent/build.js +43 -0
  8. package/dist/agent/build.js.map +1 -0
  9. package/dist/agent/commands.d.ts +39 -0
  10. package/dist/agent/commands.d.ts.map +1 -0
  11. package/dist/agent/commands.js +43 -0
  12. package/dist/agent/commands.js.map +1 -0
  13. package/dist/agent/rollback-handler.d.ts +28 -0
  14. package/dist/agent/rollback-handler.d.ts.map +1 -0
  15. package/dist/agent/rollback-handler.js +52 -0
  16. package/dist/agent/rollback-handler.js.map +1 -0
  17. package/dist/agent/sign.d.ts +18 -0
  18. package/dist/agent/sign.d.ts.map +1 -0
  19. package/dist/agent/sign.js +19 -0
  20. package/dist/agent/sign.js.map +1 -0
  21. package/dist/agent/types.d.ts +86 -0
  22. package/dist/agent/types.d.ts.map +1 -0
  23. package/dist/agent/types.js +2 -0
  24. package/dist/agent/types.js.map +1 -0
  25. package/dist/alerts/alerts.d.ts +42 -0
  26. package/dist/alerts/alerts.d.ts.map +1 -0
  27. package/dist/alerts/alerts.js +98 -0
  28. package/dist/alerts/alerts.js.map +1 -0
  29. package/dist/alerts/conditions.d.ts +25 -0
  30. package/dist/alerts/conditions.d.ts.map +1 -0
  31. package/dist/alerts/conditions.js +28 -0
  32. package/dist/alerts/conditions.js.map +1 -0
  33. package/dist/alerts/tables.d.ts +13 -0
  34. package/dist/alerts/tables.d.ts.map +1 -0
  35. package/dist/alerts/tables.js +27 -0
  36. package/dist/alerts/tables.js.map +1 -0
  37. package/dist/control/commands.d.ts +38 -0
  38. package/dist/control/commands.d.ts.map +1 -0
  39. package/dist/control/commands.js +51 -0
  40. package/dist/control/commands.js.map +1 -0
  41. package/dist/control/identity.d.ts +13 -0
  42. package/dist/control/identity.d.ts.map +1 -0
  43. package/dist/control/identity.js +5 -0
  44. package/dist/control/identity.js.map +1 -0
  45. package/dist/control/ingest.d.ts +46 -0
  46. package/dist/control/ingest.d.ts.map +1 -0
  47. package/dist/control/ingest.js +56 -0
  48. package/dist/control/ingest.js.map +1 -0
  49. package/dist/control/risk.d.ts +66 -0
  50. package/dist/control/risk.d.ts.map +1 -0
  51. package/dist/control/risk.js +130 -0
  52. package/dist/control/risk.js.map +1 -0
  53. package/dist/control/state.d.ts +19 -0
  54. package/dist/control/state.d.ts.map +1 -0
  55. package/dist/control/state.js +67 -0
  56. package/dist/control/state.js.map +1 -0
  57. package/dist/control/tables.d.ts +18 -0
  58. package/dist/control/tables.d.ts.map +1 -0
  59. package/dist/control/tables.js +58 -0
  60. package/dist/control/tables.js.map +1 -0
  61. package/dist/enrollment/store.d.ts +84 -0
  62. package/dist/enrollment/store.d.ts.map +1 -0
  63. package/dist/enrollment/store.js +81 -0
  64. package/dist/enrollment/store.js.map +1 -0
  65. package/dist/enrollment/tables.d.ts +20 -0
  66. package/dist/enrollment/tables.d.ts.map +1 -0
  67. package/dist/enrollment/tables.js +55 -0
  68. package/dist/enrollment/tables.js.map +1 -0
  69. package/dist/enrollment/tokens.d.ts +4 -0
  70. package/dist/enrollment/tokens.d.ts.map +1 -0
  71. package/dist/enrollment/tokens.js +20 -0
  72. package/dist/enrollment/tokens.js.map +1 -0
  73. package/dist/index.d.ts +42 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +25 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/inventory/drift.d.ts +73 -0
  78. package/dist/inventory/drift.d.ts.map +1 -0
  79. package/dist/inventory/drift.js +91 -0
  80. package/dist/inventory/drift.js.map +1 -0
  81. package/dist/reporting/report.d.ts +65 -0
  82. package/dist/reporting/report.d.ts.map +1 -0
  83. package/dist/reporting/report.js +137 -0
  84. package/dist/reporting/report.js.map +1 -0
  85. package/dist/reporting/schedule.d.ts +10 -0
  86. package/dist/reporting/schedule.d.ts.map +1 -0
  87. package/dist/reporting/schedule.js +34 -0
  88. package/dist/reporting/schedule.js.map +1 -0
  89. package/dist/reporting/tables.d.ts +11 -0
  90. package/dist/reporting/tables.d.ts.map +1 -0
  91. package/dist/reporting/tables.js +22 -0
  92. package/dist/reporting/tables.js.map +1 -0
  93. package/dist/rollout/campaign.d.ts +103 -0
  94. package/dist/rollout/campaign.d.ts.map +1 -0
  95. package/dist/rollout/campaign.js +258 -0
  96. package/dist/rollout/campaign.js.map +1 -0
  97. package/dist/rollout/rollback.d.ts +51 -0
  98. package/dist/rollout/rollback.d.ts.map +1 -0
  99. package/dist/rollout/rollback.js +60 -0
  100. package/dist/rollout/rollback.js.map +1 -0
  101. package/dist/rollout/tables.d.ts +19 -0
  102. package/dist/rollout/tables.d.ts.map +1 -0
  103. package/dist/rollout/tables.js +65 -0
  104. package/dist/rollout/tables.js.map +1 -0
  105. package/package.json +45 -0
@@ -0,0 +1,98 @@
1
+ import { buildAlert } from '@cogenta/channels';
2
+ /**
3
+ * "Alertes de flotte sur les canaux" (`docs/lots/L8-flotte.md`) — real,
4
+ * operator-facing alerts for the agency, distinct from `../reporting/report.js`'s
5
+ * commercial, end-client-facing monthly reports. Reuses `@cogenta/channels`'s
6
+ * real `buildAlert` constructor (L6 task 6) exactly, the same reuse
7
+ * discipline task 9 already established for `buildReport` — no hand-built
8
+ * `AlertChannelMessage` literal anywhere in this module.
9
+ */
10
+ const ADMIN_URL_FALLBACK = 'https://admin.invalid/fleet';
11
+ /**
12
+ * A site crossing into `critical` risk (task 5's real `computeSiteRisk`) —
13
+ * "un site avec une CVE critique passe devant" on the dashboard is the same
14
+ * real signal an operator needs pushed to them immediately, not only seen
15
+ * next time they open it.
16
+ */
17
+ export function detectCriticalRiskAlert(risk) {
18
+ if (risk.tier !== 'critical')
19
+ return null;
20
+ const topReason = [...risk.reasons].sort((a, b) => b.points - a.points)[0];
21
+ return buildAlert({
22
+ title: `${risk.siteName} — risque critique`,
23
+ severity: 'critical',
24
+ context: topReason === undefined ? 'Risque critique détecté.' : topReason.detail,
25
+ expectedAction: 'Examiner ce site dans le tableau de bord de flotte et agir sans délai.',
26
+ adminUrl: ADMIN_URL_FALLBACK,
27
+ });
28
+ }
29
+ /**
30
+ * A rollout campaign transitioning to `halted` (task 7's real
31
+ * `CampaignRecord`) — "un échec arrête toute la campagne" is exactly the
32
+ * kind of event an operator needs to know about the moment it happens, not
33
+ * discover by noticing an update never finished.
34
+ */
35
+ export function detectCampaignHaltedAlert(campaign, siteNameById) {
36
+ if (campaign.status !== 'halted')
37
+ return null;
38
+ const failedNames = campaign.failedSiteIds.map((id) => siteNameById.get(id) ?? id).join(', ');
39
+ return buildAlert({
40
+ title: `Campagne "${campaign.componentName}" arrêtée`,
41
+ severity: 'critical',
42
+ context: campaign.haltedReason ??
43
+ `La mise à jour vers ${campaign.targetVersion} a échoué sur : ${failedNames || 'un site'}.`,
44
+ expectedAction: 'Vérifier les sites en échec et envisager un retour arrière.',
45
+ adminUrl: ADMIN_URL_FALLBACK,
46
+ });
47
+ }
48
+ /**
49
+ * "## Pièges connus": "Les faux positifs de disponibilité. Un réseau
50
+ * instable produit des alertes de panne inexistantes. Exiger plusieurs
51
+ * échecs consécutifs depuis plusieurs points avant d'alerter."
52
+ *
53
+ * This architecture has no active probing at all — "le plan de contrôle
54
+ * n'ouvre jamais de connexion vers un site" (absolute since task 1, reused
55
+ * by task 7's async-verification design). "Plusieurs points" (multiple
56
+ * vantage points) has no honest equivalent here: there is only ONE real
57
+ * observation channel, a site's own periodic telemetry contact. The
58
+ * architecturally-consistent reading of the same anti-flapping INTENT is
59
+ * "require sustained absence across multiple real expected-contact
60
+ * windows" — a real, multi-cycle threshold, not a single missed window.
61
+ */
62
+ const CONTACT_WINDOW_MS = 24 * 60 * 60 * 1000; // one real daily contact cycle, same reasoning as `../control/state.ts`'s retention and `../rollout/campaign.ts`'s DEFAULT_TIMEOUT_MS
63
+ const MISSED_WINDOWS_THRESHOLD = 3; // a single missed window is real network noise; three consecutive is a real, sustained absence
64
+ export function detectSiteSilentAlert(site, latestSnapshot, now = Date.now) {
65
+ const referenceTime = latestSnapshot === null
66
+ ? new Date(site.registeredAt).getTime()
67
+ : new Date(latestSnapshot.ingestedAt).getTime();
68
+ const silentMs = now() - referenceTime;
69
+ const missedWindows = Math.floor(silentMs / CONTACT_WINDOW_MS);
70
+ if (missedWindows < MISSED_WINDOWS_THRESHOLD)
71
+ return null;
72
+ return buildAlert({
73
+ title: `${site.name} — silencieux`,
74
+ severity: 'warning',
75
+ context: `Aucun contact reçu depuis plus de ${missedWindows} jours.`,
76
+ expectedAction: "Vérifier que le site est joignable et que son processus d'envoi fonctionne.",
77
+ adminUrl: ADMIN_URL_FALLBACK,
78
+ });
79
+ }
80
+ /**
81
+ * Wraps any detection function with the real de-duplication layer
82
+ * (`./conditions.js`): the message is only returned — and thus only ever
83
+ * sent — on the check where the condition genuinely transitions from
84
+ * inactive to active. Every subsequent check while it remains active
85
+ * returns `null`, even though the underlying condition still holds.
86
+ */
87
+ export async function raiseIfNew(conditionStore, siteId, conditionType, message, now) {
88
+ if (message === null) {
89
+ await conditionStore.clear(siteId, conditionType, now);
90
+ return null;
91
+ }
92
+ const { fired } = await conditionStore.raise(siteId, conditionType, now);
93
+ return fired ? message : null;
94
+ }
95
+ export async function dispatchAlert(sender, message) {
96
+ await sender.send(message);
97
+ }
98
+ //# sourceMappingURL=alerts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alerts.js","sourceRoot":"","sources":["../../src/alerts/alerts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ /**
3
+ * The three real alert conditions built in this task, closed — a caller
4
+ * cannot invent a fourth condition type without a matching detection
5
+ * function to go with it (`./alerts.js`).
6
+ */
7
+ export type AlertConditionType = 'critical-risk' | 'campaign-halted' | 'site-silent';
8
+ export interface AlertConditionStore {
9
+ /** `true` when this exact (site, condition) pair has an unresolved, unraised-since-clear alert. */
10
+ isActive(siteId: string, conditionType: AlertConditionType): Promise<boolean>;
11
+ /**
12
+ * Raises the condition if — and only if — it is not already active:
13
+ * de-duplication is the whole point. Returns `{fired: true}` the first
14
+ * time a condition becomes active, `{fired: false}` on every repeat check
15
+ * while it remains active — a caller sends a real alert only when `fired`
16
+ * is `true`, never once per check cycle.
17
+ */
18
+ raise(siteId: string, conditionType: AlertConditionType, now?: () => number): Promise<{
19
+ readonly fired: boolean;
20
+ }>;
21
+ /** Marks the condition resolved — its next `raise()` fires again, a real "was this cleared" check, never a permanent one-time suppression. */
22
+ clear(siteId: string, conditionType: AlertConditionType, now?: () => number): Promise<void>;
23
+ }
24
+ export declare function createAlertConditionStore(db: DatabaseHandle, now?: () => number): AlertConditionStore;
25
+ //# sourceMappingURL=conditions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditions.d.ts","sourceRoot":"","sources":["../../src/alerts/conditions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAG3E;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG,iBAAiB,GAAG,aAAa,CAAA;AAEpF,MAAM,WAAW,mBAAmB;IAClC,mGAAmG;IACnG,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7E;;;;;;OAMG;IACH,KAAK,CACH,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,kBAAkB,EACjC,GAAG,CAAC,EAAE,MAAM,MAAM,GACjB,OAAO,CAAC;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IACvC,8IAA8I;IAC9I,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,kBAAkB,EAAE,GAAG,CAAC,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5F;AAUD,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,cAAc,EAClB,GAAG,GAAE,MAAM,MAAiB,GAC3B,mBAAmB,CA0BrB"}
@@ -0,0 +1,28 @@
1
+ import { identifier, newId, sql } from '@cogenta/core';
2
+ import { ALERT_TABLES } from './tables.js';
3
+ export function createAlertConditionStore(db, now = Date.now) {
4
+ const conditions = identifier(ALERT_TABLES.conditions, db.dialect);
5
+ return {
6
+ async isActive(siteId, conditionType) {
7
+ const result = await db.query(sql `
8
+ select id from ${conditions}
9
+ where site_id = ${siteId} and condition_type = ${conditionType} and cleared_at is null`);
10
+ return result.rows.length > 0;
11
+ },
12
+ async raise(siteId, conditionType, clock = now) {
13
+ const alreadyActive = await this.isActive(siteId, conditionType);
14
+ if (alreadyActive)
15
+ return { fired: false };
16
+ await db.query(sql `
17
+ insert into ${conditions} (id, site_id, condition_type, raised_at, cleared_at)
18
+ values (${newId(clock)}, ${siteId}, ${conditionType}, ${new Date(clock()).toISOString()}, ${null})`);
19
+ return { fired: true };
20
+ },
21
+ async clear(siteId, conditionType, clock = now) {
22
+ await db.query(sql `
23
+ update ${conditions} set cleared_at = ${new Date(clock()).toISOString()}
24
+ where site_id = ${siteId} and condition_type = ${conditionType} and cleared_at is null`);
25
+ },
26
+ };
27
+ }
28
+ //# sourceMappingURL=conditions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditions.js","sourceRoot":"","sources":["../../src/alerts/conditions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAoC1C,MAAM,UAAU,yBAAyB,CACvC,EAAkB,EAClB,GAAG,GAAiB,IAAI,CAAC,GAAG;IAE5B,MAAM,UAAU,GAAG,UAAU,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;IAElE,OAAO;QACL,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,aAAa;YAClC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAAe,GAAG,CAAA;yBAC5B,UAAU;0BACT,MAAM,yBAAyB,aAAa,yBAAyB,CAAC,CAAA;YAC1F,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAA;QAC/B,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,GAAG,GAAG;YAC5C,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;YAChE,IAAI,aAAa;gBAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;YAC1C,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;sBACF,UAAU;kBACd,KAAK,CAAC,KAAK,CAAC,KAAK,MAAM,KAAK,aAAa,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,IAAI,GAAG,CAAC,CAAA;YACtG,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;QACxB,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,GAAG,GAAG;YAC5C,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iBACP,UAAU,qBAAqB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE;0BACrD,MAAM,yBAAyB,aAAa,yBAAyB,CAAC,CAAA;QAC5F,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ export declare const ALERT_TABLES: {
3
+ readonly conditions: 'cogenta_fleet_alert_conditions';
4
+ };
5
+ /**
6
+ * One row per (site, condition type) currently-or-previously-raised alert —
7
+ * `cleared_at is null` means still active. Same `create table if not exists`
8
+ * pattern as `../reporting/tables.ts`/`../control/tables.ts`. Strictly
9
+ * per-site: no query here ever spans more than one site's row at once,
10
+ * matching every other L8 store's per-site isolation discipline.
11
+ */
12
+ export declare function ensureAlertTables(db: DatabaseHandle): Promise<void>;
13
+ //# sourceMappingURL=tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/alerts/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,eAAO,MAAM,YAAY;aACvB,UAAU,EAAE,gCAAgC;CACpC,CAAA;AAMV;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAWzE"}
@@ -0,0 +1,27 @@
1
+ import { identifier, sql, unsafeRaw, } from '@cogenta/core';
2
+ export const ALERT_TABLES = {
3
+ conditions: 'cogenta_fleet_alert_conditions',
4
+ };
5
+ function textColumn(dialect, length) {
6
+ return unsafeRaw(dialect === 'sqlite' ? 'text' : `varchar(${length})`);
7
+ }
8
+ /**
9
+ * One row per (site, condition type) currently-or-previously-raised alert —
10
+ * `cleared_at is null` means still active. Same `create table if not exists`
11
+ * pattern as `../reporting/tables.ts`/`../control/tables.ts`. Strictly
12
+ * per-site: no query here ever spans more than one site's row at once,
13
+ * matching every other L8 store's per-site isolation discipline.
14
+ */
15
+ export async function ensureAlertTables(db) {
16
+ const t255 = textColumn(db.dialect, 255);
17
+ const conditions = identifier(ALERT_TABLES.conditions, db.dialect);
18
+ await db.query(sql `
19
+ create table if not exists ${conditions} (
20
+ id ${t255} not null primary key,
21
+ site_id ${t255} not null,
22
+ condition_type ${t255} not null,
23
+ raised_at ${t255} not null,
24
+ cleared_at ${t255}
25
+ )`);
26
+ }
27
+ //# sourceMappingURL=tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.js","sourceRoot":"","sources":["../../src/alerts/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEV,GAAG,EACH,SAAS,GACV,MAAM,eAAe,CAAA;AAEtB,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,UAAU,EAAE,gCAAgC;CACpC,CAAA;AAEV,SAAS,UAAU,CAAC,OAAwB,EAAE,MAAc;IAC1D,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,MAAM,GAAG,CAAC,CAAA;AACxE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,EAAkB;IACxD,MAAM,IAAI,GAAG,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;IACxC,MAAM,UAAU,GAAG,UAAU,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;IAClE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU;WAChC,IAAI;gBACC,IAAI;uBACG,IAAI;kBACT,IAAI;mBACH,IAAI;MACjB,CAAC,CAAA;AACP,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ import type { ControlPlaneIdentity } from './identity.js';
3
+ /**
4
+ * The complete, closed set of things a control plane may ever ask a site to
5
+ * do — "## Appairage": "commandes... exécutables uniquement dans une liste
6
+ * blanche d'actions." `update` and `rollback` are named now, ahead of their
7
+ * real execution logic (tasks 7/8), so this task's transport/verification
8
+ * layer is the real, non-bypassable seam those tasks plug handlers into —
9
+ * not a whitelist widened later under a design already shipped.
10
+ */
11
+ export declare const FLEET_COMMAND_ACTIONS: readonly ['update', 'rollback'];
12
+ export type FleetCommandAction = (typeof FLEET_COMMAND_ACTIONS)[number];
13
+ export interface FleetCommand {
14
+ readonly id: string;
15
+ readonly siteId: string;
16
+ readonly action: FleetCommandAction;
17
+ readonly payload: unknown;
18
+ readonly queuedAt: string;
19
+ }
20
+ export interface SignedFleetCommand {
21
+ readonly command: FleetCommand;
22
+ /** Base64 Ed25519 signature over the command's canonical content, made with the control plane's own private key — never the site's. */
23
+ readonly signatureBase64: string;
24
+ }
25
+ export interface CommandQueueStore {
26
+ /** Real, persisted, strictly per-site — no cross-site query shape exists anywhere in this store. */
27
+ enqueue(siteId: string, action: FleetCommandAction, payload: unknown): Promise<FleetCommand>;
28
+ /**
29
+ * A site's real, pending commands, signed and handed over. Fetching marks
30
+ * every returned command delivered — a site's next fetch never sees it
31
+ * again, closing the "re-fetch the same command forever" gap without
32
+ * pretending to know whether the site went on to execute it (that
33
+ * bookkeeping belongs with tasks 7/8's real execution logic, not here).
34
+ */
35
+ fetchPending(siteId: string): Promise<readonly SignedFleetCommand[]>;
36
+ }
37
+ export declare function createCommandQueueStore(db: DatabaseHandle, controlPlaneIdentity: ControlPlaneIdentity, now?: () => number): CommandQueueStore;
38
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/control/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAE3E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAGzD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,YAAI,QAAQ,EAAE,UAAU,CAAU,CAAA;AACpE,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;IACnC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAC9B,uIAAuI;IACvI,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,oGAAoG;IACpG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC5F;;;;;;OAMG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAA;CACrE;AAqBD,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,cAAc,EAClB,oBAAoB,EAAE,oBAAoB,EAC1C,GAAG,GAAE,MAAM,MAAiB,GAC3B,iBAAiB,CAiCnB"}
@@ -0,0 +1,51 @@
1
+ import { identifier, newId, sql } from '@cogenta/core';
2
+ import { signContent } from '@cogenta/plugins';
3
+ import { CONTROL_TABLES } from './tables.js';
4
+ /**
5
+ * The complete, closed set of things a control plane may ever ask a site to
6
+ * do — "## Appairage": "commandes... exécutables uniquement dans une liste
7
+ * blanche d'actions." `update` and `rollback` are named now, ahead of their
8
+ * real execution logic (tasks 7/8), so this task's transport/verification
9
+ * layer is the real, non-bypassable seam those tasks plug handlers into —
10
+ * not a whitelist widened later under a design already shipped.
11
+ */
12
+ export const FLEET_COMMAND_ACTIONS = ['update', 'rollback'];
13
+ function toCommand(row) {
14
+ return {
15
+ id: row.id,
16
+ siteId: row.site_id,
17
+ action: row.action,
18
+ payload: JSON.parse(row.payload_json),
19
+ queuedAt: row.queued_at,
20
+ };
21
+ }
22
+ export function createCommandQueueStore(db, controlPlaneIdentity, now = Date.now) {
23
+ const commands = identifier(CONTROL_TABLES.commands, db.dialect);
24
+ return {
25
+ async enqueue(siteId, action, payload) {
26
+ const id = newId(now);
27
+ const queuedAt = new Date(now()).toISOString();
28
+ await db.query(sql `
29
+ insert into ${commands} (id, site_id, action, payload_json, queued_at, delivered_at)
30
+ values (${id}, ${siteId}, ${action}, ${JSON.stringify(payload)}, ${queuedAt}, ${null})`);
31
+ return { id, siteId, action, payload, queuedAt };
32
+ },
33
+ async fetchPending(siteId) {
34
+ const result = await db.query(sql `
35
+ select id, site_id, action, payload_json, queued_at, delivered_at from ${commands}
36
+ where site_id = ${siteId} and delivered_at is null order by queued_at asc`);
37
+ const pending = result.rows.map(toCommand);
38
+ if (pending.length === 0)
39
+ return [];
40
+ const deliveredAt = new Date(now()).toISOString();
41
+ for (const command of pending) {
42
+ await db.query(sql `update ${commands} set delivered_at = ${deliveredAt} where id = ${command.id}`);
43
+ }
44
+ return pending.map((command) => ({
45
+ command,
46
+ signatureBase64: signContent(command, controlPlaneIdentity.privateKey),
47
+ }));
48
+ },
49
+ };
50
+ }
51
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/control/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE5C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAU,CAAA;AAuCpE,SAAS,SAAS,CAAC,GAAe;IAChC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,MAAM,EAAE,GAAG,CAAC,OAAO;QACnB,MAAM,EAAE,GAAG,CAAC,MAA4B;QACxC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAY;QAChD,QAAQ,EAAE,GAAG,CAAC,SAAS;KACxB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,EAAkB,EAClB,oBAA0C,EAC1C,GAAG,GAAiB,IAAI,CAAC,GAAG;IAE5B,MAAM,QAAQ,GAAG,UAAU,CAAC,cAAc,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;IAEhE,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO;YACnC,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;YACrB,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;YAC9C,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;sBACF,QAAQ;kBACZ,EAAE,KAAK,MAAM,KAAK,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,QAAQ,KAAK,IAAI,GAAG,CAAC,CAAA;YAC1F,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAA;QAClD,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,MAAM;YACvB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAAa,GAAG,CAAA;iFAC8B,QAAQ;0BAC/D,MAAM,kDAAkD,CAAC,CAAA;YAC7E,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAC1C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAA;YAEnC,MAAM,WAAW,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;YACjD,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;gBAC9B,MAAM,EAAE,CAAC,KAAK,CACZ,GAAG,CAAA,UAAU,QAAQ,uBAAuB,WAAW,eAAe,OAAO,CAAC,EAAE,EAAE,CACnF,CAAA;YACH,CAAC;YAED,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBAC/B,OAAO;gBACP,eAAe,EAAE,WAAW,CAAC,OAAO,EAAE,oBAAoB,CAAC,UAAU,CAAC;aACvE,CAAC,CAAC,CAAA;QACL,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { type SigningKeyPair } from '@cogenta/plugins';
2
+ /**
3
+ * The control plane's own real Ed25519 identity — distinct from any site's.
4
+ * A site signs its outgoing telemetry with ITS key (`../agent/sign.ts`); the
5
+ * control plane signs outgoing commands with ITS OWN key, and a site
6
+ * verifies those against the public half recorded here, learned once at
7
+ * pairing time (`../enrollment/store.ts`'s `PairingToken.controlPlanePublicKey`).
8
+ * Two independent keypairs, two independent verification directions — never
9
+ * the same key checked both ways.
10
+ */
11
+ export type ControlPlaneIdentity = SigningKeyPair;
12
+ export declare function generateControlPlaneIdentity(): ControlPlaneIdentity;
13
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/control/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE9E;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAA;AAEjD,wBAAgB,4BAA4B,IAAI,oBAAoB,CAEnE"}
@@ -0,0 +1,5 @@
1
+ import { generateSigningKeyPair } from '@cogenta/plugins';
2
+ export function generateControlPlaneIdentity() {
3
+ return generateSigningKeyPair();
4
+ }
5
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/control/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAuB,MAAM,kBAAkB,CAAA;AAa9E,MAAM,UAAU,4BAA4B;IAC1C,OAAO,sBAAsB,EAAE,CAAA;AACjC,CAAC"}
@@ -0,0 +1,46 @@
1
+ import type { SignedTelemetry } from '../agent/sign.js';
2
+ import type { EnrollmentStore } from '../enrollment/store.js';
3
+ import type { SiteStateStore, TelemetrySnapshot } from './state.js';
4
+ /**
5
+ * "Le plan de contrôle est un observateur... pas un propriétaire" only holds
6
+ * if it never trusts a sender's own claims — a discriminated result, not a
7
+ * raw exception, matching every other "here's exactly why this was refused"
8
+ * shape this session's L6/L7 forks established (`@cogenta/channels`' link
9
+ * codes, `@cogenta/plugins`' registries).
10
+ */
11
+ export type IngestResult = {
12
+ readonly ok: true;
13
+ readonly snapshot: TelemetrySnapshot;
14
+ } | {
15
+ readonly ok: false;
16
+ readonly reason: 'unknown_site' | 'revoked' | 'invalid_signature' | 'forbidden_field';
17
+ readonly message: string;
18
+ };
19
+ /**
20
+ * The control plane's real ingestion boundary — the receiving-side half of
21
+ * "## Ce qui remonte, et ce qui ne remonte pas"'s "critère d'acceptation
22
+ * testé, pas une intention." Three independent checks, in this order:
23
+ *
24
+ * 1. **The claimed site is actually paired, and not revoked** — cheap, no
25
+ * cryptography needed, and refusing here first means an unpaired or
26
+ * revoked sender never gets far enough to learn whether its signature
27
+ * would otherwise have been accepted. Revocation is checked as its own
28
+ * condition, never folded into "does the signature verify": a site
29
+ * paired before being revoked still holds a cryptographically valid
30
+ * keypair, so signature validity alone can never be what keeps a revoked
31
+ * site out.
32
+ * 2. **The signature genuinely verifies against THAT site's registered
33
+ * public key** (`../enrollment/store.js`, task 1) — a payload signed
34
+ * with any other key, or tampered with after signing, is rejected here.
35
+ * 3. **The payload, independently re-inspected on receipt, still contains
36
+ * only the real, closed shape** (`assertNoForbiddenFields`, task 2's own
37
+ * defense-in-depth check, re-run here rather than trusted from the
38
+ * sender) — a compromised or buggy site is exactly the threat model this
39
+ * step defends against; trusting that a sender's own type system kept it
40
+ * honest is not a real security boundary.
41
+ *
42
+ * A rejection at any step never touches `stateStore` — a partially-applied,
43
+ * unverified update is worse than none.
44
+ */
45
+ export declare function ingestTelemetry(signed: SignedTelemetry, enrollmentStore: EnrollmentStore, stateStore: SiteStateStore): Promise<IngestResult>;
46
+ //# sourceMappingURL=ingest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/control/ingest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD,OAAO,KAAK,EAAE,eAAe,EAAoB,MAAM,wBAAwB,CAAA;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnE;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,GAC3D;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;IAClB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,GAAG,mBAAmB,GAAG,iBAAiB,CAAA;IACrF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAEL;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,eAAe,EACvB,eAAe,EAAE,eAAe,EAChC,UAAU,EAAE,cAAc,GACzB,OAAO,CAAC,YAAY,CAAC,CA6BvB"}
@@ -0,0 +1,56 @@
1
+ import { CogentaError } from '@cogenta/core';
2
+ import { assertNoForbiddenFields } from '../agent/assert.js';
3
+ import { verifyTelemetrySignature } from '../agent/sign.js';
4
+ /**
5
+ * The control plane's real ingestion boundary — the receiving-side half of
6
+ * "## Ce qui remonte, et ce qui ne remonte pas"'s "critère d'acceptation
7
+ * testé, pas une intention." Three independent checks, in this order:
8
+ *
9
+ * 1. **The claimed site is actually paired, and not revoked** — cheap, no
10
+ * cryptography needed, and refusing here first means an unpaired or
11
+ * revoked sender never gets far enough to learn whether its signature
12
+ * would otherwise have been accepted. Revocation is checked as its own
13
+ * condition, never folded into "does the signature verify": a site
14
+ * paired before being revoked still holds a cryptographically valid
15
+ * keypair, so signature validity alone can never be what keeps a revoked
16
+ * site out.
17
+ * 2. **The signature genuinely verifies against THAT site's registered
18
+ * public key** (`../enrollment/store.js`, task 1) — a payload signed
19
+ * with any other key, or tampered with after signing, is rejected here.
20
+ * 3. **The payload, independently re-inspected on receipt, still contains
21
+ * only the real, closed shape** (`assertNoForbiddenFields`, task 2's own
22
+ * defense-in-depth check, re-run here rather than trusted from the
23
+ * sender) — a compromised or buggy site is exactly the threat model this
24
+ * step defends against; trusting that a sender's own type system kept it
25
+ * honest is not a real security boundary.
26
+ *
27
+ * A rejection at any step never touches `stateStore` — a partially-applied,
28
+ * unverified update is worse than none.
29
+ */
30
+ export async function ingestTelemetry(signed, enrollmentStore, stateStore) {
31
+ const siteId = signed.payload.siteId;
32
+ const site = await enrollmentStore.getSite(siteId);
33
+ if (site === null) {
34
+ return { ok: false, reason: 'unknown_site', message: `No paired site "${siteId}".` };
35
+ }
36
+ if (site.revoked) {
37
+ return { ok: false, reason: 'revoked', message: `Site "${siteId}" is revoked.` };
38
+ }
39
+ if (!verifyTelemetrySignature(signed, site.publicKey)) {
40
+ return {
41
+ ok: false,
42
+ reason: 'invalid_signature',
43
+ message: `Telemetry signature for site "${siteId}" does not verify against its registered key.`,
44
+ };
45
+ }
46
+ try {
47
+ assertNoForbiddenFields(signed.payload);
48
+ }
49
+ catch (error) {
50
+ const message = error instanceof CogentaError ? error.message : 'Telemetry payload failed the shape check.';
51
+ return { ok: false, reason: 'forbidden_field', message };
52
+ }
53
+ const snapshot = await stateStore.recordSnapshot(siteId, signed.payload);
54
+ return { ok: true, snapshot };
55
+ }
56
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.js","sourceRoot":"","sources":["../../src/control/ingest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAmB3D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAuB,EACvB,eAAgC,EAChC,UAA0B;IAE1B,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAA;IACpC,MAAM,IAAI,GAA4B,MAAM,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAE3E,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,mBAAmB,MAAM,IAAI,EAAE,CAAA;IACtF,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,MAAM,eAAe,EAAE,CAAA;IAClF,CAAC;IAED,IAAI,CAAC,wBAAwB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACtD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,mBAAmB;YAC3B,OAAO,EAAE,iCAAiC,MAAM,+CAA+C;SAChG,CAAA;IACH,CAAC;IAED,IAAI,CAAC;QACH,uBAAuB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACzC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GACX,KAAK,YAAY,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,2CAA2C,CAAA;QAC7F,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAA;IAC1D,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IACxE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;AAC/B,CAAC"}
@@ -0,0 +1,66 @@
1
+ import type { SiteRegistration } from '../enrollment/store.js';
2
+ import type { DriftEntry } from '../inventory/drift.js';
3
+ import type { TelemetrySnapshot } from './state.js';
4
+ /**
5
+ * "Tableau de bord de flotte : tri par risque, pas par ordre alphabétique"
6
+ * (`docs/lots/L8-flotte.md`) — real, deterministic scoring over exactly the
7
+ * `TelemetryPayload` fields that have a real data source today
8
+ * (`../agent/types.js`'s own field-by-field doc comments): `openCves`
9
+ * (real, security-agent pipeline) and `adminAccounts`' MFA coverage (real,
10
+ * `@cogenta/auth`). Version drift (`../inventory/drift.js`, real) also
11
+ * contributes. Fields still "shape only" — `availability`, `backups`,
12
+ * `certificateExpiry`, `aggregatedErrors` — contribute NOTHING here: a risk
13
+ * function that weighed a field with no real data source behind it would be
14
+ * fabricating a signal, not computing one. Each of those slots into the
15
+ * score the moment a later task wires up its real data source, without
16
+ * redesigning this function — the weight table below just grows.
17
+ */
18
+ export type RiskTier = 'critical' | 'high' | 'medium' | 'low';
19
+ export interface RiskReason {
20
+ readonly code: 'cve' | 'mfa-gap' | 'version-drift';
21
+ readonly detail: string;
22
+ readonly points: number;
23
+ }
24
+ export interface SiteRisk {
25
+ readonly siteId: string;
26
+ readonly siteName: string;
27
+ readonly client: string | null;
28
+ readonly score: number;
29
+ readonly tier: RiskTier;
30
+ readonly reasons: readonly RiskReason[];
31
+ }
32
+ /**
33
+ * Real, deterministic per-site score — never touches `snapshot`'s
34
+ * shape-only fields. `null` `snapshot` (a paired site that has never
35
+ * reported) is real risk in itself: an unresponsive/never-seen site is not
36
+ * "score zero, all clear."
37
+ */
38
+ export declare function computeSiteRisk(site: SiteRegistration, snapshot: TelemetrySnapshot | null, driftEntries: readonly DriftEntry[]): SiteRisk;
39
+ /**
40
+ * Sorted highest-risk first — the dashboard's whole reason for existing
41
+ * ("tri par risque, pas par ordre alphabétique"). Ties break on site name,
42
+ * ascending, so the ordering is fully deterministic (never insertion order).
43
+ */
44
+ export declare function rankSitesByRisk(sites: readonly {
45
+ readonly site: SiteRegistration;
46
+ readonly snapshot: TelemetrySnapshot | null;
47
+ readonly driftEntries: readonly DriftEntry[];
48
+ }[]): readonly SiteRisk[];
49
+ export interface FleetFilter {
50
+ readonly client?: string;
51
+ /** Only sites at or above this tier — "critical" is the narrowest, "low" is everyone. */
52
+ readonly minTier?: RiskTier;
53
+ /** Case-insensitive substring match against site name or client. */
54
+ readonly search?: string;
55
+ }
56
+ /**
57
+ * Real, composable query over an already-ranked list — designed for a
58
+ * hundred sites from the start ("## Pièges connus": "Concevoir directement
59
+ * pour cent"), not a client-side afterthought: a real O(n) predicate pass,
60
+ * the same shape a server-side query would use once a live control plane
61
+ * exists to run one against.
62
+ */
63
+ export declare function filterRisks(risks: readonly SiteRisk[], filter: FleetFilter): readonly SiteRisk[];
64
+ /** Real grouping by client — `null` (no client set) is its own real group, never dropped or merged into a fake "unknown" label. */
65
+ export declare function groupRisksByClient(risks: readonly SiteRisk[]): ReadonlyMap<string | null, readonly SiteRisk[]>;
66
+ //# sourceMappingURL=risk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"risk.d.ts","sourceRoot":"","sources":["../../src/control/risk.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAA;AAE7D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,GAAG,eAAe,CAAA;IAClD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;CACxC;AAuCD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,iBAAiB,GAAG,IAAI,EAClC,YAAY,EAAE,SAAS,UAAU,EAAE,GAClC,QAAQ,CAkDV;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS;IACd,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAC3C,QAAQ,CAAC,YAAY,EAAE,SAAS,UAAU,EAAE,CAAA;CAC7C,EAAE,GACF,SAAS,QAAQ,EAAE,CAIrB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAA;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAID;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,QAAQ,EAAE,CAYhG;AAED,mIAAmI;AACnI,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,QAAQ,EAAE,GACzB,WAAW,CAAC,MAAM,GAAG,IAAI,EAAE,SAAS,QAAQ,EAAE,CAAC,CAQjD"}