@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,130 @@
1
+ /**
2
+ * A critical CVE alone must outrank everything else this function can weigh
3
+ * — the lot's own literal acceptance criterion ("un site avec une CVE
4
+ * critique passe devant"). Values are real point contributions, not display
5
+ * labels; a single critical CVE (100) already exceeds the `critical` tier
6
+ * threshold below on its own.
7
+ */
8
+ const CVE_URGENCY_POINTS = {
9
+ critical: 100,
10
+ high: 45,
11
+ medium: 15,
12
+ low: 4,
13
+ };
14
+ /** Points per drifted component, capped so a site with dozens of drifted plugins doesn't mathematically out-rank a single critical CVE — drift is real but genuinely less urgent than an exploitable vulnerability. */
15
+ const DRIFT_POINTS = {
16
+ behind: 6,
17
+ different: 4,
18
+ ahead: 1,
19
+ };
20
+ const MAX_DRIFT_POINTS = 30;
21
+ /** One unmatched-MFA admin account is a real, meaningful gap — scales with the real ratio, not just a flat penalty regardless of fleet size. */
22
+ const MFA_GAP_POINTS_PER_ACCOUNT = 8;
23
+ const TIER_THRESHOLDS = [
24
+ { tier: 'critical', min: 80 },
25
+ { tier: 'high', min: 35 },
26
+ { tier: 'medium', min: 10 },
27
+ { tier: 'low', min: 0 },
28
+ ];
29
+ function tierFor(score) {
30
+ const found = TIER_THRESHOLDS.find((entry) => score >= entry.min);
31
+ return found?.tier ?? 'low';
32
+ }
33
+ /**
34
+ * Real, deterministic per-site score — never touches `snapshot`'s
35
+ * shape-only fields. `null` `snapshot` (a paired site that has never
36
+ * reported) is real risk in itself: an unresponsive/never-seen site is not
37
+ * "score zero, all clear."
38
+ */
39
+ export function computeSiteRisk(site, snapshot, driftEntries) {
40
+ const reasons = [];
41
+ let score = 0;
42
+ if (snapshot === null) {
43
+ reasons.push({ code: 'cve', detail: 'no telemetry ever received', points: 20 });
44
+ score += 20;
45
+ }
46
+ else {
47
+ for (const cve of snapshot.payload.openCves) {
48
+ if (cve.status !== 'open')
49
+ continue;
50
+ const points = CVE_URGENCY_POINTS[cve.urgency];
51
+ reasons.push({ code: 'cve', detail: `${cve.id} (${cve.urgency}, open)`, points });
52
+ score += points;
53
+ }
54
+ const { count, mfaEnabledCount } = snapshot.payload.adminAccounts;
55
+ const withoutMfa = Math.max(0, count - mfaEnabledCount);
56
+ if (withoutMfa > 0) {
57
+ const points = Math.min(withoutMfa * MFA_GAP_POINTS_PER_ACCOUNT, 40);
58
+ reasons.push({
59
+ code: 'mfa-gap',
60
+ detail: `${withoutMfa}/${count} admin account(s) without MFA`,
61
+ points,
62
+ });
63
+ score += points;
64
+ }
65
+ }
66
+ const siteDrift = driftEntries.filter((entry) => entry.siteId === site.id);
67
+ if (siteDrift.length > 0) {
68
+ const driftPoints = Math.min(siteDrift.reduce((sum, entry) => sum + DRIFT_POINTS[entry.direction], 0), MAX_DRIFT_POINTS);
69
+ reasons.push({
70
+ code: 'version-drift',
71
+ detail: `${siteDrift.length} component(s) drifted from the fleet baseline`,
72
+ points: driftPoints,
73
+ });
74
+ score += driftPoints;
75
+ }
76
+ return {
77
+ siteId: site.id,
78
+ siteName: site.name,
79
+ client: site.client,
80
+ score,
81
+ tier: tierFor(score),
82
+ reasons,
83
+ };
84
+ }
85
+ /**
86
+ * Sorted highest-risk first — the dashboard's whole reason for existing
87
+ * ("tri par risque, pas par ordre alphabétique"). Ties break on site name,
88
+ * ascending, so the ordering is fully deterministic (never insertion order).
89
+ */
90
+ export function rankSitesByRisk(sites) {
91
+ return sites
92
+ .map(({ site, snapshot, driftEntries }) => computeSiteRisk(site, snapshot, driftEntries))
93
+ .sort((a, b) => b.score - a.score || a.siteName.localeCompare(b.siteName));
94
+ }
95
+ const TIER_RANK = { critical: 3, high: 2, medium: 1, low: 0 };
96
+ /**
97
+ * Real, composable query over an already-ranked list — designed for a
98
+ * hundred sites from the start ("## Pièges connus": "Concevoir directement
99
+ * pour cent"), not a client-side afterthought: a real O(n) predicate pass,
100
+ * the same shape a server-side query would use once a live control plane
101
+ * exists to run one against.
102
+ */
103
+ export function filterRisks(risks, filter) {
104
+ const search = filter.search?.trim().toLowerCase();
105
+ return risks.filter((risk) => {
106
+ if (filter.client !== undefined && risk.client !== filter.client)
107
+ return false;
108
+ if (filter.minTier !== undefined && TIER_RANK[risk.tier] < TIER_RANK[filter.minTier])
109
+ return false;
110
+ if (search !== undefined && search !== '') {
111
+ const haystack = `${risk.siteName} ${risk.client ?? ''}`.toLowerCase();
112
+ if (!haystack.includes(search))
113
+ return false;
114
+ }
115
+ return true;
116
+ });
117
+ }
118
+ /** Real grouping by client — `null` (no client set) is its own real group, never dropped or merged into a fake "unknown" label. */
119
+ export function groupRisksByClient(risks) {
120
+ const groups = new Map();
121
+ for (const risk of risks) {
122
+ const bucket = groups.get(risk.client);
123
+ if (bucket === undefined)
124
+ groups.set(risk.client, [risk]);
125
+ else
126
+ bucket.push(risk);
127
+ }
128
+ return groups;
129
+ }
130
+ //# sourceMappingURL=risk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"risk.js","sourceRoot":"","sources":["../../src/control/risk.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ import type { TelemetryPayload } from '../agent/types.js';
3
+ export interface TelemetrySnapshot {
4
+ readonly id: string;
5
+ readonly siteId: string;
6
+ readonly collectedAt: string;
7
+ readonly ingestedAt: string;
8
+ readonly payload: TelemetryPayload;
9
+ }
10
+ export interface SiteStateStore {
11
+ /** Records one verified snapshot for `siteId`, then prunes down to the retention limit — never called with an unverified payload (`./ingest.js` is the only real caller). */
12
+ recordSnapshot(siteId: string, payload: TelemetryPayload): Promise<TelemetrySnapshot>;
13
+ /** The most recent snapshot for one site, or `null` if none exists yet. Always scoped to exactly one `siteId` — there is no "latest across all sites" shape here. */
14
+ latest(siteId: string): Promise<TelemetrySnapshot | null>;
15
+ /** Every retained snapshot for one site, most recent first. Always scoped to exactly one `siteId`. */
16
+ history(siteId: string): Promise<readonly TelemetrySnapshot[]>;
17
+ }
18
+ export declare function createSiteStateStore(db: DatabaseHandle, now?: () => number, retain?: number): SiteStateStore;
19
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/control/state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA4C,MAAM,eAAe,CAAA;AAC7F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAsBzD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,6KAA6K;IAC7K,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAA;IACrF,qKAAqK;IACrK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAA;IACzD,sGAAsG;IACtG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAA;CAC/D;AAoBD,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,cAAc,EAClB,GAAG,GAAE,MAAM,MAAiB,EAC5B,MAAM,GAAE,MAAuB,GAC9B,cAAc,CA0ChB"}
@@ -0,0 +1,67 @@
1
+ import { identifier, newId, sql } from '@cogenta/core';
2
+ import { CONTROL_TABLES } from './tables.js';
3
+ /** `@cogenta/core`'s `sql` tag has no built-in list helper — nested fragments splice correctly, so a left-fold of one-value fragments joined by `, ` produces real, individually-bound placeholders, never an inlined value. */
4
+ function sqlList(values) {
5
+ return values.map((value) => sql `${value}`).reduce((acc, fragment) => sql `${acc}, ${fragment}`);
6
+ }
7
+ /**
8
+ * How many of a site's most recent telemetry snapshots the control plane
9
+ * keeps. "## Pièges connus" (`docs/lots/L8-flotte.md`): "La télémétrie qui
10
+ * grossit... Rétention et agrégation dès le départ" — a real, bounded number
11
+ * decided now rather than an unbounded table a later task has to notice and
12
+ * fix. 30 covers roughly a month of once-daily contact (the natural cadence
13
+ * for the fields this payload actually carries — CVE status, Core Web
14
+ * Vitals, backup results — none of which need finer-than-daily history for
15
+ * the dashboard/report tasks this lot still has ahead of it) without the
16
+ * unbounded growth the pitfall warns about. A real aggregation/rollup
17
+ * pipeline for longer-term trends is a later task's job, not this one's.
18
+ */
19
+ const DEFAULT_RETAIN = 30;
20
+ function toSnapshot(row) {
21
+ return {
22
+ id: row.id,
23
+ siteId: row.site_id,
24
+ collectedAt: row.collected_at,
25
+ ingestedAt: row.ingested_at,
26
+ payload: JSON.parse(row.payload_json),
27
+ };
28
+ }
29
+ export function createSiteStateStore(db, now = Date.now, retain = DEFAULT_RETAIN) {
30
+ const snapshots = identifier(CONTROL_TABLES.telemetrySnapshots, db.dialect);
31
+ return {
32
+ async recordSnapshot(siteId, payload) {
33
+ const id = newId(now);
34
+ const ingestedAt = new Date(now()).toISOString();
35
+ await db.query(sql `
36
+ insert into ${snapshots} (id, site_id, collected_at, ingested_at, payload_json)
37
+ values (${id}, ${siteId}, ${payload.collectedAt}, ${ingestedAt}, ${JSON.stringify(payload)})`);
38
+ // Bounded retention, enforced on every write, not a separate cron job
39
+ // a deployment could forget to schedule: delete anything for this site
40
+ // that falls outside its `retain` most-recent rows.
41
+ const keep = await db.query(sql `
42
+ select id from ${snapshots} where site_id = ${siteId}
43
+ order by collected_at desc limit ${retain}`);
44
+ const keepIds = keep.rows.map((row) => row.id);
45
+ if (keepIds.length > 0) {
46
+ await db.query(sql `
47
+ delete from ${snapshots}
48
+ where site_id = ${siteId} and id not in (${sqlList(keepIds)})`);
49
+ }
50
+ return { id, siteId, collectedAt: payload.collectedAt, ingestedAt, payload };
51
+ },
52
+ async latest(siteId) {
53
+ const result = await db.query(sql `
54
+ select id, site_id, collected_at, ingested_at, payload_json from ${snapshots}
55
+ where site_id = ${siteId} order by collected_at desc limit 1`);
56
+ const row = result.rows[0];
57
+ return row === undefined ? null : toSnapshot(row);
58
+ },
59
+ async history(siteId) {
60
+ const result = await db.query(sql `
61
+ select id, site_id, collected_at, ingested_at, payload_json from ${snapshots}
62
+ where site_id = ${siteId} order by collected_at desc`);
63
+ return result.rows.map(toSnapshot);
64
+ },
65
+ };
66
+ }
67
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/control/state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ export declare const CONTROL_TABLES: {
3
+ readonly telemetrySnapshots: 'cogenta_fleet_telemetry_snapshots';
4
+ readonly commands: 'cogenta_fleet_commands';
5
+ };
6
+ /**
7
+ * Owned by `@cogenta/fleet`'s control-plane side, same `create table if not
8
+ * exists` pattern as `./enrollment/tables.ts`'s `ensureFleetTables` — no
9
+ * separate migration file. One row per ingested, verified telemetry
10
+ * snapshot, keyed strictly by `site_id` — there is no table, index or query
11
+ * shape anywhere in `@cogenta/fleet` that spans more than one site's rows at
12
+ * once (`./state.ts`'s real API only ever takes a single `siteId`), which is
13
+ * what "keyed and scoped per-site from day one, structurally" means in
14
+ * practice: not a convention a caller is expected to honour, a shape that
15
+ * makes a cross-site query awkward to even attempt.
16
+ */
17
+ export declare function ensureControlTables(db: DatabaseHandle): Promise<void>;
18
+ //# sourceMappingURL=tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/control/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,eAAO,MAAM,cAAc;aACzB,kBAAkB,EAAE,mCAAmC;aACvD,QAAQ,EAAE,wBAAwB;CAC1B,CAAA;AAqBV;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAyC3E"}
@@ -0,0 +1,58 @@
1
+ import { identifier, sql, unsafeRaw, } from '@cogenta/core';
2
+ export const CONTROL_TABLES = {
3
+ telemetrySnapshots: 'cogenta_fleet_telemetry_snapshots',
4
+ commands: 'cogenta_fleet_commands',
5
+ };
6
+ function textColumn(dialect, length) {
7
+ return unsafeRaw(dialect === 'sqlite' ? 'text' : `varchar(${length})`);
8
+ }
9
+ function longTextColumn(dialect) {
10
+ return unsafeRaw(dialect === 'sqlite' ? 'text' : 'text');
11
+ }
12
+ async function createIndexIfMissing(db, name, table, columns) {
13
+ await db
14
+ .query(sql `create index ${identifier(name, db.dialect)} on ${table} ${columns}`)
15
+ .catch(() => undefined); // already there — no portable "if not exists" for indexes
16
+ }
17
+ /**
18
+ * Owned by `@cogenta/fleet`'s control-plane side, same `create table if not
19
+ * exists` pattern as `./enrollment/tables.ts`'s `ensureFleetTables` — no
20
+ * separate migration file. One row per ingested, verified telemetry
21
+ * snapshot, keyed strictly by `site_id` — there is no table, index or query
22
+ * shape anywhere in `@cogenta/fleet` that spans more than one site's rows at
23
+ * once (`./state.ts`'s real API only ever takes a single `siteId`), which is
24
+ * what "keyed and scoped per-site from day one, structurally" means in
25
+ * practice: not a convention a caller is expected to honour, a shape that
26
+ * makes a cross-site query awkward to even attempt.
27
+ */
28
+ export async function ensureControlTables(db) {
29
+ const d = db.dialect;
30
+ const t255 = textColumn(d, 255);
31
+ const payloadColumn = longTextColumn(d);
32
+ const snapshots = identifier(CONTROL_TABLES.telemetrySnapshots, d);
33
+ await db.query(sql `
34
+ create table if not exists ${snapshots} (
35
+ id ${t255} not null primary key,
36
+ site_id ${t255} not null,
37
+ collected_at ${t255} not null,
38
+ ingested_at ${t255} not null,
39
+ payload_json ${payloadColumn} not null
40
+ )`);
41
+ await createIndexIfMissing(db, 'cogenta_fleet_telemetry_snapshots_site', snapshots, sql `(site_id, collected_at)`);
42
+ // One row per queued command, strictly per-site (`site_id`) — the same
43
+ // structural per-site scoping as `telemetry_snapshots` above, mirrored for
44
+ // the opposite direction (control plane → site instead of site → control
45
+ // plane).
46
+ const commands = identifier(CONTROL_TABLES.commands, d);
47
+ await db.query(sql `
48
+ create table if not exists ${commands} (
49
+ id ${t255} not null primary key,
50
+ site_id ${t255} not null,
51
+ action ${t255} not null,
52
+ payload_json ${payloadColumn} not null,
53
+ queued_at ${t255} not null,
54
+ delivered_at ${t255}
55
+ )`);
56
+ await createIndexIfMissing(db, 'cogenta_fleet_commands_site', commands, sql `(site_id, delivered_at)`);
57
+ }
58
+ //# sourceMappingURL=tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.js","sourceRoot":"","sources":["../../src/control/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEV,GAAG,EACH,SAAS,GACV,MAAM,eAAe,CAAA;AAEtB,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,kBAAkB,EAAE,mCAAmC;IACvD,QAAQ,EAAE,wBAAwB;CAC1B,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,SAAS,cAAc,CAAC,OAAwB;IAC9C,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AAC1D,CAAC;AAED,KAAK,UAAU,oBAAoB,CACjC,EAAkB,EAClB,IAAY,EACZ,KAAkB,EAClB,OAAoB;IAEpB,MAAM,EAAE;SACL,KAAK,CAAC,GAAG,CAAA,gBAAgB,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,KAAK,IAAI,OAAO,EAAE,CAAC;SAC/E,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA,CAAC,0DAA0D;AACtF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAkB;IAC1D,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAA;IACpB,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC/B,MAAM,aAAa,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA;IAEvC,MAAM,SAAS,GAAG,UAAU,CAAC,cAAc,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAA;IAClE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,SAAS;WAC/B,IAAI;gBACC,IAAI;qBACC,IAAI;oBACL,IAAI;qBACH,aAAa;MAC5B,CAAC,CAAA;IACL,MAAM,oBAAoB,CACxB,EAAE,EACF,wCAAwC,EACxC,SAAS,EACT,GAAG,CAAA,yBAAyB,CAC7B,CAAA;IAED,uEAAuE;IACvE,2EAA2E;IAC3E,yEAAyE;IACzE,UAAU;IACV,MAAM,QAAQ,GAAG,UAAU,CAAC,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;IACvD,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,QAAQ;WAC9B,IAAI;gBACC,IAAI;eACL,IAAI;qBACE,aAAa;kBAChB,IAAI;qBACD,IAAI;MACnB,CAAC,CAAA;IACL,MAAM,oBAAoB,CACxB,EAAE,EACF,6BAA6B,EAC7B,QAAQ,EACR,GAAG,CAAA,yBAAyB,CAC7B,CAAA;AACH,CAAC"}
@@ -0,0 +1,84 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ import { type ControlPlaneIdentity } from '../control/identity.js';
3
+ export interface PairingToken {
4
+ /** The real bearer credential — returned once, at issuance, never recoverable afterward (only its hash is stored). */
5
+ readonly token: string;
6
+ readonly expiresAt: string;
7
+ /**
8
+ * The control plane's own real Ed25519 public key (`../control/identity.js`)
9
+ * — handed to the site at the same moment as the token, so it can verify
10
+ * every future signed command (task 6) without a second round trip. The
11
+ * reverse of the site's own public key, which the control plane only
12
+ * learns later, at consumption (`consumePairingToken`).
13
+ */
14
+ readonly controlPlanePublicKey: string;
15
+ }
16
+ export interface SiteRegistration {
17
+ readonly id: string;
18
+ readonly name: string;
19
+ /** Base64 SPKI Ed25519 public key the site submitted when it consumed its pairing token — every later mutually-authenticated exchange verifies against this. */
20
+ readonly publicKey: string;
21
+ readonly registeredAt: string;
22
+ readonly revoked: boolean;
23
+ readonly revokedAt: string | null;
24
+ /**
25
+ * Which agency client this site belongs to — real, minimal, optional (an
26
+ * agency running a single-client fleet has no reason to fill it in).
27
+ * `null` for a site enrolled without one. The one client concept this
28
+ * package has: needed for the dashboard's real "regroupement par client"
29
+ * (`docs/lots/L8-flotte.md`, "## Pièges connus"), which had nothing to
30
+ * group by before this field existed.
31
+ */
32
+ readonly client: string | null;
33
+ }
34
+ /**
35
+ * `consumePairingToken`'s outcome — a discriminated result, not a raw
36
+ * exception, following the same "already decided"/"expired"/"invalid" shape
37
+ * `@cogenta/channels`' linking codes and `@cogenta/plugins`' approval and
38
+ * registry-review flows already established this session: a caller (a real
39
+ * enrollment HTTP handler, once one exists) can render each case without
40
+ * parsing an error message.
41
+ */
42
+ export type PairingConsumeResult = {
43
+ readonly ok: true;
44
+ readonly site: SiteRegistration;
45
+ } | {
46
+ readonly ok: false;
47
+ readonly reason: 'invalid' | 'expired' | 'already_used';
48
+ };
49
+ export interface EnrollmentStore {
50
+ /**
51
+ * Real, single-use, time-limited — "## Appairage"'s exact requirement.
52
+ * `siteName` is fixed at issuance (an operator names the site they're
53
+ * about to pair, before handing the token to it), not supplied later by
54
+ * whoever presents the token — the token authorizes registering
55
+ * *this specific, pre-named* site, not an arbitrary one.
56
+ */
57
+ issuePairingToken(siteName: string, options?: {
58
+ ttlMs?: number;
59
+ client?: string;
60
+ }): Promise<PairingToken>;
61
+ /**
62
+ * The site's real Ed25519 public key, submitted at consumption time —
63
+ * this is the one moment a site's identity is established; every later
64
+ * contact authenticates against what gets recorded here.
65
+ */
66
+ consumePairingToken(token: string, sitePublicKey: string): Promise<PairingConsumeResult>;
67
+ revokeSite(siteId: string): Promise<void>;
68
+ isRevoked(siteId: string): Promise<boolean>;
69
+ getSite(siteId: string): Promise<SiteRegistration | null>;
70
+ /**
71
+ * Every currently-registered site's metadata (id/name/key/revocation
72
+ * state) — never anything content- or telemetry-shaped. Needed for real,
73
+ * fleet-wide operations (inventory/drift detection here; the dashboard and
74
+ * rollout campaigns later) that must enumerate sites without touching any
75
+ * per-site telemetry store, which structurally has no cross-site query at
76
+ * all (`../control/state.js`'s `SiteStateStore`) — this is the one real,
77
+ * narrow seam where "which sites exist" is legitimately fleet-wide.
78
+ */
79
+ listSites(): Promise<readonly SiteRegistration[]>;
80
+ /** The control plane's own public key — same value every `PairingToken` already carries, exposed directly for a caller that already has a site and needs to verify a command without re-deriving it from a stored token. */
81
+ getControlPlanePublicKey(): string;
82
+ }
83
+ export declare function createEnrollmentStore(db: DatabaseHandle, now?: () => number, controlPlaneIdentity?: ControlPlaneIdentity): EnrollmentStore;
84
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/enrollment/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,KAAK,oBAAoB,EAAgC,MAAM,wBAAwB,CAAA;AAOhG,MAAM,WAAW,YAAY;IAC3B,sHAAsH;IACtH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;CACvC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,gKAAgK;IAChK,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,cAAc,CAAA;CAAE,CAAA;AAEnF,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,iBAAiB,CACf,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5C,OAAO,CAAC,YAAY,CAAC,CAAA;IACxB;;;;OAIG;IACH,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAA;IACxF,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3C,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IACzD;;;;;;;;OAQG;IACH,SAAS,IAAI,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC,CAAA;IACjD,4NAA4N;IAC5N,wBAAwB,IAAI,MAAM,CAAA;CACnC;AAiCD,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,cAAc,EAClB,GAAG,GAAE,MAAM,MAAiB,EAC5B,oBAAoB,GAAE,oBAAqD,GAC1E,eAAe,CA8EjB"}
@@ -0,0 +1,81 @@
1
+ import { identifier, newId, sql } from '@cogenta/core';
2
+ import { generateControlPlaneIdentity } from '../control/identity.js';
3
+ import { FLEET_TABLES } from './tables.js';
4
+ import { generatePairingToken, hashPairingToken } from './tokens.js';
5
+ /** Default: long enough for an operator to copy a token into a site's config, short enough that a forgotten token stops mattering on its own. */
6
+ const DEFAULT_TTL_MS = 15 * 60 * 1000;
7
+ function toSite(row) {
8
+ return {
9
+ id: row.id,
10
+ name: row.name,
11
+ publicKey: row.public_key,
12
+ registeredAt: row.registered_at,
13
+ revoked: row.revoked_at !== null,
14
+ revokedAt: row.revoked_at,
15
+ client: row.client,
16
+ };
17
+ }
18
+ export function createEnrollmentStore(db, now = Date.now, controlPlaneIdentity = generateControlPlaneIdentity()) {
19
+ const tokens = identifier(FLEET_TABLES.pairingTokens, db.dialect);
20
+ const sites = identifier(FLEET_TABLES.sites, db.dialect);
21
+ return {
22
+ async issuePairingToken(siteName, options = {}) {
23
+ const token = generatePairingToken();
24
+ const expiresAt = new Date(now() + (options.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
25
+ const client = options.client ?? null;
26
+ await db.query(sql `
27
+ insert into ${tokens} (id, token_hash, site_name, expires_at, consumed_at, site_id, client)
28
+ values (${newId(now)}, ${hashPairingToken(token)}, ${siteName}, ${expiresAt}, ${null}, ${null}, ${client})`);
29
+ return { token, expiresAt, controlPlanePublicKey: controlPlaneIdentity.publicKey };
30
+ },
31
+ async consumePairingToken(token, sitePublicKey) {
32
+ const hash = hashPairingToken(token);
33
+ const result = await db.query(sql `select id, token_hash, site_name, expires_at, consumed_at, site_id, client from ${tokens} where token_hash = ${hash}`);
34
+ const row = result.rows[0];
35
+ if (row === undefined)
36
+ return { ok: false, reason: 'invalid' };
37
+ if (row.consumed_at !== null)
38
+ return { ok: false, reason: 'already_used' };
39
+ if (new Date(row.expires_at).getTime() <= now())
40
+ return { ok: false, reason: 'expired' };
41
+ const siteId = newId(now);
42
+ const registeredAt = new Date(now()).toISOString();
43
+ await db.query(sql `
44
+ insert into ${sites} (id, name, public_key, registered_at, revoked_at, client)
45
+ values (${siteId}, ${row.site_name}, ${sitePublicKey}, ${registeredAt}, ${null}, ${row.client})`);
46
+ await db.query(sql `update ${tokens} set consumed_at = ${registeredAt}, site_id = ${siteId} where id = ${row.id}`);
47
+ return {
48
+ ok: true,
49
+ site: {
50
+ id: siteId,
51
+ name: row.site_name,
52
+ publicKey: sitePublicKey,
53
+ registeredAt,
54
+ revoked: false,
55
+ revokedAt: null,
56
+ client: row.client,
57
+ },
58
+ };
59
+ },
60
+ async revokeSite(siteId) {
61
+ await db.query(sql `update ${sites} set revoked_at = ${new Date(now()).toISOString()} where id = ${siteId} and revoked_at is null`);
62
+ },
63
+ async isRevoked(siteId) {
64
+ const site = await this.getSite(siteId);
65
+ return site?.revoked ?? false;
66
+ },
67
+ async getSite(siteId) {
68
+ const result = await db.query(sql `select id, name, public_key, registered_at, revoked_at, client from ${sites} where id = ${siteId}`);
69
+ const row = result.rows[0];
70
+ return row === undefined ? null : toSite(row);
71
+ },
72
+ async listSites() {
73
+ const result = await db.query(sql `select id, name, public_key, registered_at, revoked_at, client from ${sites} order by registered_at asc`);
74
+ return result.rows.map(toSite);
75
+ },
76
+ getControlPlanePublicKey() {
77
+ return controlPlaneIdentity.publicKey;
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/enrollment/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ export declare const FLEET_TABLES: {
3
+ readonly pairingTokens: 'cogenta_fleet_pairing_tokens';
4
+ readonly sites: 'cogenta_fleet_sites';
5
+ };
6
+ /**
7
+ * Owned by `@cogenta/fleet`, `create table if not exists` pattern exactly as
8
+ * `@cogenta/plugins`'s `ensurePluginTables`/`@cogenta/channels`'s
9
+ * `ensureChannelTables` — no separate migration file.
10
+ *
11
+ * `pairing_tokens` holds only the token's SHA-256 hash — never the token
12
+ * itself, same discipline as `@cogenta/auth`'s session tokens and
13
+ * `@cogenta/channels`'s linking codes; a leaked table hands out nothing
14
+ * usable. `sites` is the real, persistent registration a consumed token
15
+ * produces — the control plane's own record of "this public key is site X",
16
+ * which every later task (télémétrie, commandes signées) authenticates
17
+ * against.
18
+ */
19
+ export declare function ensureFleetTables(db: DatabaseHandle): Promise<void>;
20
+ //# sourceMappingURL=tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/enrollment/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,eAAO,MAAM,YAAY;aACvB,aAAa,EAAE,8BAA8B;aAC7C,KAAK,EAAE,qBAAqB;CACpB,CAAA;AAiBV;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BzE"}
@@ -0,0 +1,55 @@
1
+ import { identifier, sql, unsafeRaw, } from '@cogenta/core';
2
+ export const FLEET_TABLES = {
3
+ pairingTokens: 'cogenta_fleet_pairing_tokens',
4
+ sites: 'cogenta_fleet_sites',
5
+ };
6
+ function textColumn(dialect, length) {
7
+ return unsafeRaw(dialect === 'sqlite' ? 'text' : `varchar(${length})`);
8
+ }
9
+ async function createIndexIfMissing(db, name, table, columns) {
10
+ await db
11
+ .query(sql `create index ${identifier(name, db.dialect)} on ${table} ${columns}`)
12
+ .catch(() => undefined); // already there — no portable "if not exists" for indexes
13
+ }
14
+ /**
15
+ * Owned by `@cogenta/fleet`, `create table if not exists` pattern exactly as
16
+ * `@cogenta/plugins`'s `ensurePluginTables`/`@cogenta/channels`'s
17
+ * `ensureChannelTables` — no separate migration file.
18
+ *
19
+ * `pairing_tokens` holds only the token's SHA-256 hash — never the token
20
+ * itself, same discipline as `@cogenta/auth`'s session tokens and
21
+ * `@cogenta/channels`'s linking codes; a leaked table hands out nothing
22
+ * usable. `sites` is the real, persistent registration a consumed token
23
+ * produces — the control plane's own record of "this public key is site X",
24
+ * which every later task (télémétrie, commandes signées) authenticates
25
+ * against.
26
+ */
27
+ export async function ensureFleetTables(db) {
28
+ const d = db.dialect;
29
+ const t255 = textColumn(d, 255);
30
+ const t1024 = textColumn(d, 1024);
31
+ const tokens = identifier(FLEET_TABLES.pairingTokens, d);
32
+ await db.query(sql `
33
+ create table if not exists ${tokens} (
34
+ id ${t255} not null primary key,
35
+ token_hash ${t255} not null,
36
+ site_name ${t255} not null,
37
+ expires_at ${t255} not null,
38
+ consumed_at ${t255},
39
+ site_id ${t255},
40
+ client ${t255}
41
+ )`);
42
+ await createIndexIfMissing(db, 'cogenta_fleet_pairing_tokens_hash', tokens, sql `(token_hash)`);
43
+ const sites = identifier(FLEET_TABLES.sites, d);
44
+ await db.query(sql `
45
+ create table if not exists ${sites} (
46
+ id ${t255} not null primary key,
47
+ name ${t255} not null,
48
+ public_key ${t1024} not null,
49
+ registered_at ${t255} not null,
50
+ revoked_at ${t255},
51
+ client ${t255}
52
+ )`);
53
+ await createIndexIfMissing(db, 'cogenta_fleet_sites_client', sites, sql `(client)`);
54
+ }
55
+ //# sourceMappingURL=tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.js","sourceRoot":"","sources":["../../src/enrollment/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEV,GAAG,EACH,SAAS,GACV,MAAM,eAAe,CAAA;AAEtB,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,aAAa,EAAE,8BAA8B;IAC7C,KAAK,EAAE,qBAAqB;CACpB,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,KAAK,UAAU,oBAAoB,CACjC,EAAkB,EAClB,IAAY,EACZ,KAAkB,EAClB,OAAoB;IAEpB,MAAM,EAAE;SACL,KAAK,CAAC,GAAG,CAAA,gBAAgB,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,KAAK,IAAI,OAAO,EAAE,CAAC;SAC/E,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA,CAAC,0DAA0D;AACtF,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,EAAkB;IACxD,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAA;IACpB,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC/B,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IAEjC,MAAM,MAAM,GAAG,UAAU,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC,CAAC,CAAA;IACxD,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,MAAM;WAC5B,IAAI;mBACI,IAAI;kBACL,IAAI;mBACH,IAAI;oBACH,IAAI;gBACR,IAAI;eACL,IAAI;MACb,CAAC,CAAA;IACL,MAAM,oBAAoB,CAAC,EAAE,EAAE,mCAAmC,EAAE,MAAM,EAAE,GAAG,CAAA,cAAc,CAAC,CAAA;IAE9F,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IAC/C,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,KAAK;WAC3B,IAAI;aACF,IAAI;mBACE,KAAK;sBACF,IAAI;mBACP,IAAI;eACR,IAAI;MACb,CAAC,CAAA;IACL,MAAM,oBAAoB,CAAC,EAAE,EAAE,4BAA4B,EAAE,KAAK,EAAE,GAAG,CAAA,UAAU,CAAC,CAAA;AACpF,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function generatePairingToken(): string;
2
+ /** Stored hashed, exactly like a session token — a leaked table hands out nothing usable. */
3
+ export declare function hashPairingToken(token: string): string;
4
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/enrollment/tokens.ts"],"names":[],"mappings":"AAcA,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEtD"}
@@ -0,0 +1,20 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ /**
3
+ * 32 random bytes (256 bits), base64url — the same shape as
4
+ * `@cogenta/auth`'s session tokens (`sessions.ts`'s `TOKEN_BYTES`), not the
5
+ * shorter 8-character human-typed code `@cogenta/channels`' linking flow
6
+ * uses (L6 task 2). A pairing token is copy-pasted into a site's own
7
+ * configuration during enrollment, never hand-typed character by character
8
+ * into a chat app — there's no reason to shrink its alphabet for
9
+ * readability, and every bit of entropy here defends a real, if short-lived,
10
+ * bearer credential: whoever presents it gets to register a site.
11
+ */
12
+ const TOKEN_BYTES = 32;
13
+ export function generatePairingToken() {
14
+ return randomBytes(TOKEN_BYTES).toString('base64url');
15
+ }
16
+ /** Stored hashed, exactly like a session token — a leaked table hands out nothing usable. */
17
+ export function hashPairingToken(token) {
18
+ return createHash('sha256').update(token).digest('base64url');
19
+ }
20
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/enrollment/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAErD;;;;;;;;;GASG;AACH,MAAM,WAAW,GAAG,EAAE,CAAA;AAEtB,MAAM,UAAU,oBAAoB;IAClC,OAAO,WAAW,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;AACvD,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;AAC/D,CAAC"}