fraim 2.0.203 → 2.0.204

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 (79) hide show
  1. package/README.md +3 -3
  2. package/bin/fraim.js +8 -18
  3. package/dist/src/cli/commands/first-run.js +2 -37
  4. package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +1 -1
  5. package/dist/src/cli/fraim.js +0 -2
  6. package/dist/src/cli/setup/ide-invocation-surfaces.js +6 -6
  7. package/dist/src/cli/utils/managed-agent-paths.js +76 -6
  8. package/dist/src/cli/utils/version-utils.js +2 -2
  9. package/dist/src/config/persona-capability-bundles.js +3 -3
  10. package/dist/src/core/job-phases.js +168 -0
  11. package/dist/src/core/utils/ports.js +32 -0
  12. package/dist/src/core/utils/stub-generator.js +24 -2
  13. package/dist/src/first-run/session-service.js +106 -24
  14. package/dist/src/local-mcp-server/learning-context-builder.js +1 -1
  15. package/dist/src/local-mcp-server/stdio-server.js +41 -9
  16. package/dist/src/routes/analytics.js +2 -2
  17. package/dist/src/services/registry-service.js +2 -2
  18. package/index.js +1 -1
  19. package/package.json +61 -181
  20. package/dist/src/ai-hub/catalog.js +0 -406
  21. package/dist/src/ai-hub/cert-store.js +0 -70
  22. package/dist/src/ai-hub/conversation-store.js +0 -359
  23. package/dist/src/ai-hub/desktop-main.js +0 -449
  24. package/dist/src/ai-hub/hosts.js +0 -1664
  25. package/dist/src/ai-hub/hub-latest-version.js +0 -52
  26. package/dist/src/ai-hub/hub-launch-decision.js +0 -57
  27. package/dist/src/ai-hub/hub-runtime-file.js +0 -43
  28. package/dist/src/ai-hub/managed-browser.js +0 -269
  29. package/dist/src/ai-hub/manager-turns.js +0 -64
  30. package/dist/src/ai-hub/office-sideload.js +0 -156
  31. package/dist/src/ai-hub/openclaw-bridge.js +0 -250
  32. package/dist/src/ai-hub/preferences.js +0 -201
  33. package/dist/src/ai-hub/remote-hub-gateway.js +0 -88
  34. package/dist/src/ai-hub/server.js +0 -3827
  35. package/dist/src/ai-hub/types.js +0 -2
  36. package/dist/src/ai-hub/word-sideload.js +0 -95
  37. package/dist/src/cli/commands/hub.js +0 -238
  38. package/dist/src/cli/commands/test-mcp.js +0 -171
  39. package/dist/src/cli/setup/first-run.js +0 -242
  40. package/dist/src/core/config-writer.js +0 -75
  41. package/dist/src/core/utils/job-aliases.js +0 -47
  42. package/dist/src/core/utils/workflow-parser.js +0 -174
  43. package/dist/src/services/email-service-clean.js +0 -782
  44. package/extensions/office-word/favicon.ico +0 -0
  45. package/extensions/office-word/icon-64.png +0 -0
  46. package/extensions/office-word/manifest.xml +0 -33
  47. package/extensions/office-word/taskpane.html +0 -242
  48. package/public/ai-hub/index.html +0 -894
  49. package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
  50. package/public/ai-hub/powerpoint-taskpane/index.html +0 -236
  51. package/public/ai-hub/powerpoint-taskpane/manifest.xml +0 -30
  52. package/public/ai-hub/review.css +0 -381
  53. package/public/ai-hub/script.js +0 -11015
  54. package/public/ai-hub/styles.css +0 -4873
  55. package/public/portfolio/ashley.html +0 -523
  56. package/public/portfolio/auditya.html +0 -83
  57. package/public/portfolio/banke.html +0 -83
  58. package/public/portfolio/beza.html +0 -659
  59. package/public/portfolio/careena.html +0 -632
  60. package/public/portfolio/casey.html +0 -568
  61. package/public/portfolio/celia.html +0 -490
  62. package/public/portfolio/deidre.html +0 -642
  63. package/public/portfolio/gautam.html +0 -597
  64. package/public/portfolio/hari.html +0 -469
  65. package/public/portfolio/huxley.html +0 -1354
  66. package/public/portfolio/index.html +0 -741
  67. package/public/portfolio/maestro.html +0 -518
  68. package/public/portfolio/mandy.html +0 -590
  69. package/public/portfolio/mona.html +0 -597
  70. package/public/portfolio/pam.html +0 -887
  71. package/public/portfolio/procella.html +0 -107
  72. package/public/portfolio/qasm.html +0 -569
  73. package/public/portfolio/ricardo.html +0 -489
  74. package/public/portfolio/sade.html +0 -560
  75. package/public/portfolio/sam.html +0 -654
  76. package/public/portfolio/sechar.html +0 -580
  77. package/public/portfolio/sreya.html +0 -599
  78. package/public/portfolio/swen.html +0 -601
  79. /package/dist/src/{ai-hub → core}/brand-store.js +0 -0
@@ -1,406 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.summarizeProject = summarizeProject;
7
- exports.discoverEmployeeJobs = discoverEmployeeJobs;
8
- exports.discoverManagerTemplates = discoverManagerTemplates;
9
- exports.loadJobPhases = loadJobPhases;
10
- exports.loadAllJobPhaseIds = loadAllJobPhaseIds;
11
- exports.labelForPhaseId = labelForPhaseId;
12
- exports.getAiHubCategories = getAiHubCategories;
13
- const fs_1 = __importDefault(require("fs"));
14
- const path_1 = __importDefault(require("path"));
15
- const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
16
- // Directories scanned for employee jobs at runtime, in lowest-to-highest
17
- // precedence order. Later entries win on {categoryId, jobId} collision.
18
- //
19
- // FRAIM execution normally reads only the two `fraim/` layers: the synced
20
- // baseline and the personalized override. The Hub can explicitly opt into the
21
- // source `registry/` fallback when running against the FRAIM repo before sync.
22
- // - <project>/fraim/ai-employee/jobs/<category>/ - synced baseline
23
- // - <project>/fraim/personalized-employee/jobs/<category>/ - taught/customized override
24
- // Only the personalized-employee layer is "personalized" (issue #566).
25
- const EMPLOYEE_JOB_LAYERS = [
26
- { segments: ['ai-employee', 'jobs'], personalized: false },
27
- { segments: ['personalized-employee', 'jobs'], personalized: true },
28
- ];
29
- // Manager templates use the matching layer model.
30
- const MANAGER_JOB_LAYERS = [
31
- { segments: ['ai-manager', 'jobs'], personalized: false },
32
- { segments: ['personalized-employee', 'manager-jobs'], personalized: true },
33
- ];
34
- const REGISTRY_EMPLOYEE_JOB_LAYERS = [
35
- { base: 'project', segments: ['registry', 'jobs', 'ai-employee'], personalized: false },
36
- ];
37
- const REGISTRY_MANAGER_JOB_LAYERS = [
38
- { base: 'project', segments: ['registry', 'jobs', 'ai-manager'], personalized: false },
39
- ];
40
- const KNOWN_LABEL_OVERRIDES = {
41
- // Project conventions where a directory name should render as a recognized
42
- // short form rather than its mechanical title-case spelling.
43
- 'go-to-market': 'GTM',
44
- gtm: 'GTM',
45
- 'qa': 'QA',
46
- rfp: 'RFP',
47
- ai: 'AI',
48
- // Manager-side groups whose directory names are already idiomatic and
49
- // shouldn't be split on dashes by the default humanizer.
50
- '1-1': '1:1',
51
- };
52
- const sectionValue = (content, heading) => {
53
- const match = content.match(new RegExp(`## ${heading}\\r?\\n([\\s\\S]*?)(?:\\r?\\n## |\\r?\\n---|$)`));
54
- if (!match)
55
- return [];
56
- return match[1]
57
- .split(/\r?\n/)
58
- .map((line) => line.trim())
59
- .filter(Boolean)
60
- .map((line) => line.replace(/^[-*]\s*/, '').trim());
61
- };
62
- const humanizeName = (raw) => {
63
- const lower = raw.toLowerCase();
64
- if (KNOWN_LABEL_OVERRIDES[lower])
65
- return KNOWN_LABEL_OVERRIDES[lower];
66
- return raw
67
- .split(/[-_\s]+/)
68
- .filter((part) => part.length > 0)
69
- .map((part) => {
70
- const partLower = part.toLowerCase();
71
- if (KNOWN_LABEL_OVERRIDES[partLower])
72
- return KNOWN_LABEL_OVERRIDES[partLower];
73
- return `${part[0].toUpperCase()}${part.slice(1).toLowerCase()}`;
74
- })
75
- .join(' ');
76
- };
77
- const toPosix = (value) => value.replace(/\\/g, '/');
78
- function readSubdirectoryNames(dirPath) {
79
- if (!fs_1.default.existsSync(dirPath))
80
- return [];
81
- return fs_1.default
82
- .readdirSync(dirPath, { withFileTypes: true })
83
- .filter((entry) => entry.isDirectory())
84
- .map((entry) => entry.name)
85
- .sort((a, b) => a.localeCompare(b));
86
- }
87
- function readMarkdownFileNames(dirPath) {
88
- if (!fs_1.default.existsSync(dirPath))
89
- return [];
90
- return fs_1.default
91
- .readdirSync(dirPath, { withFileTypes: true })
92
- .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
93
- .map((entry) => entry.name)
94
- .sort((a, b) => a.localeCompare(b));
95
- }
96
- function parseJobStub(filePath, categoryId, categoryLabel, projectPath, personalized) {
97
- const content = fs_1.default.readFileSync(filePath, 'utf8');
98
- const fileName = path_1.default.basename(filePath, '.md');
99
- const intent = sectionValue(content, 'Intent')[0] || 'No intent summary available.';
100
- const outcome = sectionValue(content, 'Outcome');
101
- return {
102
- id: fileName,
103
- title: humanizeName(fileName),
104
- categoryId,
105
- categoryLabel,
106
- intent,
107
- outcome,
108
- stubPath: toPosix(path_1.default.relative(projectPath, filePath)),
109
- personalized: !!personalized,
110
- };
111
- }
112
- function parseManagerStub(filePath, groupId, groupLabel, projectPath) {
113
- const content = fs_1.default.readFileSync(filePath, 'utf8');
114
- const fileName = path_1.default.basename(filePath, '.md');
115
- const intent = sectionValue(content, 'Intent')[0] || 'No intent summary available.';
116
- return {
117
- id: fileName,
118
- title: humanizeName(fileName),
119
- groupId,
120
- groupLabel,
121
- intent,
122
- stubPath: toPosix(path_1.default.relative(projectPath, filePath)),
123
- };
124
- }
125
- function summarizeProject(projectPath) {
126
- if (!projectPath) {
127
- return {
128
- path: '',
129
- exists: false,
130
- hasFraim: false,
131
- message: 'Choose a project path to load FRAIM jobs.',
132
- };
133
- }
134
- if (!fs_1.default.existsSync(projectPath)) {
135
- return {
136
- path: projectPath,
137
- exists: false,
138
- hasFraim: false,
139
- message: 'Project path does not exist on this machine.',
140
- };
141
- }
142
- const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath);
143
- const hasFraim = fs_1.default.existsSync(fraimDir);
144
- if (!hasFraim) {
145
- return {
146
- path: projectPath,
147
- exists: true,
148
- hasFraim: false,
149
- needsOnboarding: true,
150
- message: 'This folder has not been set up with FRAIM yet. Onboarding will get your AI employee started.',
151
- };
152
- }
153
- return {
154
- path: projectPath,
155
- exists: true,
156
- hasFraim: true,
157
- };
158
- }
159
- function resolveLayerRoot(projectPath, layer) {
160
- if (layer.base === 'project')
161
- return path_1.default.join(projectPath, ...layer.segments);
162
- return path_1.default.join((0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath), ...layer.segments);
163
- }
164
- function discoverLayers(projectPath, layers) {
165
- const out = [];
166
- for (const layer of layers) {
167
- const layerRoot = resolveLayerRoot(projectPath, layer);
168
- for (const categoryName of readSubdirectoryNames(layerRoot)) {
169
- out.push({
170
- layerRoot,
171
- categoryId: categoryName,
172
- categoryDir: path_1.default.join(layerRoot, categoryName),
173
- personalized: layer.personalized,
174
- });
175
- }
176
- }
177
- return out;
178
- }
179
- function discoverEmployeeJobs(projectPath, options = {}) {
180
- const project = summarizeProject(projectPath);
181
- if (!project.exists || (!project.hasFraim && !options.includeRegistry))
182
- return [];
183
- const layers = discoverLayers(projectPath, options.includeRegistry
184
- ? [...REGISTRY_EMPLOYEE_JOB_LAYERS, ...EMPLOYEE_JOB_LAYERS]
185
- : EMPLOYEE_JOB_LAYERS);
186
- // Group by categoryId so all layers contribute to the same labelled category.
187
- const jobsByKey = new Map();
188
- for (const layer of layers) {
189
- const categoryLabel = humanizeName(layer.categoryId);
190
- for (const fileName of readMarkdownFileNames(layer.categoryDir)) {
191
- const filePath = path_1.default.join(layer.categoryDir, fileName);
192
- const job = parseJobStub(filePath, layer.categoryId, categoryLabel, projectPath, layer.personalized);
193
- // Later layers override earlier layers on {category, jobId} collision —
194
- // personalized-employee wins over the synced ai-employee baseline. The
195
- // winning job carries its own layer's `personalized` flag (issue #566).
196
- jobsByKey.set(`${job.categoryId}::${job.id}`, job);
197
- }
198
- }
199
- // Stable sort: category label, then job title.
200
- return [...jobsByKey.values()].sort((a, b) => {
201
- if (a.categoryLabel !== b.categoryLabel)
202
- return a.categoryLabel.localeCompare(b.categoryLabel);
203
- return a.title.localeCompare(b.title);
204
- });
205
- }
206
- function discoverManagerTemplates(projectPath, options = {}) {
207
- const project = summarizeProject(projectPath);
208
- if (!project.exists || (!project.hasFraim && !options.includeRegistry))
209
- return [];
210
- const layers = discoverLayers(projectPath, options.includeRegistry
211
- ? [...REGISTRY_MANAGER_JOB_LAYERS, ...MANAGER_JOB_LAYERS]
212
- : MANAGER_JOB_LAYERS);
213
- const templatesByKey = new Map();
214
- for (const layer of layers) {
215
- const groupLabel = humanizeName(layer.categoryId);
216
- for (const fileName of readMarkdownFileNames(layer.categoryDir)) {
217
- const filePath = path_1.default.join(layer.categoryDir, fileName);
218
- const template = parseManagerStub(filePath, layer.categoryId, groupLabel, projectPath);
219
- templatesByKey.set(`${template.groupId}::${template.id}`, template);
220
- }
221
- }
222
- return [...templatesByKey.values()].sort((a, b) => {
223
- if (a.groupLabel !== b.groupLabel)
224
- return a.groupLabel.localeCompare(b.groupLabel);
225
- return a.title.localeCompare(b.title);
226
- });
227
- }
228
- // Issue #347 — load the phase list for a job, in declaration order, with
229
- // only the phases reachable on the active run's path. The Hub UI uses
230
- // this to render the pizza tracker.
231
- // Namespace prefixes the friendly-label formatter strips. Order matters
232
- // only for tie-breaking; we always pick the longest match.
233
- const KNOWN_PHASE_PREFIXES = [
234
- 'implement-',
235
- 'address-',
236
- 'spec-',
237
- 'design-',
238
- 'context-',
239
- ];
240
- function friendlyPhaseLabel(phaseId, override) {
241
- if (override)
242
- return override;
243
- let body = phaseId;
244
- for (const prefix of KNOWN_PHASE_PREFIXES) {
245
- if (body.startsWith(prefix) && body.length > prefix.length) {
246
- body = body.slice(prefix.length);
247
- break;
248
- }
249
- }
250
- const spaced = body.replace(/-/g, ' ').trim();
251
- if (spaced.length === 0)
252
- return phaseId;
253
- // Spec R1.2: first letter uppercase, rest lowercase.
254
- return spaced[0].toUpperCase() + spaced.slice(1).toLowerCase();
255
- }
256
- function readJobFrontmatter(filePath) {
257
- const raw = fs_1.default.readFileSync(filePath, 'utf8');
258
- const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
259
- if (!match)
260
- return null;
261
- const block = match[1].trim();
262
- if (!block.startsWith('{'))
263
- return null;
264
- try {
265
- return JSON.parse(block);
266
- }
267
- catch {
268
- return null;
269
- }
270
- }
271
- function findJobStubPath(projectPath, jobId) {
272
- // Walk both employee and manager layer sets. The analytics surfaces can
273
- // inspect any FRAIM job run, including manager-side jobs such as
274
- // sleep-on-learnings.
275
- const layers = discoverLayers(projectPath, [
276
- ...EMPLOYEE_JOB_LAYERS,
277
- ...MANAGER_JOB_LAYERS,
278
- ]);
279
- let resolved = null;
280
- for (const layer of layers) {
281
- const candidate = path_1.default.join(layer.categoryDir, `${jobId}.md`);
282
- if (fs_1.default.existsSync(candidate)) {
283
- resolved = candidate; // do not break — later layers override
284
- }
285
- }
286
- if (resolved)
287
- return resolved;
288
- // Fallback: look in the hub's own built-in stubs. This covers first-run
289
- // jobs (e.g. project-onboarding) that run against a project that hasn't
290
- // been synced yet, so no local stubs exist under the project directory.
291
- // In the compiled dist, __dirname is dist/ai-hub/ so this resolves to
292
- // dist/registry/stubs/jobs/.
293
- const hubStubsDir = path_1.default.resolve(__dirname, '..', 'registry', 'stubs', 'jobs');
294
- for (const hostType of ['ai-employee', 'ai-manager']) {
295
- const hostDir = path_1.default.join(hubStubsDir, hostType);
296
- for (const category of readSubdirectoryNames(hostDir)) {
297
- const candidate = path_1.default.join(hostDir, category, `${jobId}.md`);
298
- if (fs_1.default.existsSync(candidate))
299
- return candidate;
300
- }
301
- }
302
- return null;
303
- }
304
- // Resolve a phase's `onSuccess` edge to the next phase id given the run's
305
- // discriminant. Returns null when the edge is absent or terminal.
306
- function nextPhase(edge, discriminant) {
307
- if (edge == null)
308
- return null;
309
- if (typeof edge === 'string')
310
- return edge;
311
- if (typeof edge === 'object') {
312
- if (typeof edge[discriminant] === 'string')
313
- return edge[discriminant];
314
- if (typeof edge.default === 'string')
315
- return edge.default;
316
- }
317
- return null;
318
- }
319
- // Parse the ordered phase list from a job stub's ## Steps section.
320
- // Real FRAIM job stubs use Markdown steps rather than JSON frontmatter;
321
- // this is the fallback parser that makes the pizza tracker work for them.
322
- function loadJobPhasesFromSteps(filePath) {
323
- const raw = fs_1.default.readFileSync(filePath, 'utf8');
324
- const stepsMatch = raw.match(/## Steps\r?\n([\s\S]*?)(?:\r?\n## |\r?\n---|$)/);
325
- if (!stepsMatch)
326
- return [];
327
- const phases = [];
328
- for (const line of stepsMatch[1].split(/\r?\n/)) {
329
- const m = line.match(/`([a-z][a-z0-9-]*)`/);
330
- if (m)
331
- phases.push({ id: m[1], label: friendlyPhaseLabel(m[1]) });
332
- }
333
- return phases;
334
- }
335
- function loadJobPhases(jobId, projectPath, discriminant = 'feature') {
336
- const stubPath = findJobStubPath(projectPath, jobId);
337
- if (!stubPath)
338
- return [];
339
- const fm = readJobFrontmatter(stubPath);
340
- if (!fm || !fm.initialPhase || !fm.phases)
341
- return loadJobPhasesFromSteps(stubPath);
342
- const visited = new Set();
343
- const ordered = [];
344
- let cursor = fm.initialPhase;
345
- // Walk onSuccess edges to compute the path; guard against cycles by
346
- // tracking visited phase ids. A phase can only appear once in the path.
347
- while (cursor && !visited.has(cursor)) {
348
- visited.add(cursor);
349
- ordered.push(cursor);
350
- const phaseDef = fm.phases[cursor];
351
- if (!phaseDef)
352
- break;
353
- cursor = nextPhase(phaseDef.onSuccess, discriminant);
354
- }
355
- const labels = fm.phaseLabels || {};
356
- return ordered.map((id) => ({ id, label: friendlyPhaseLabel(id, labels[id]) }));
357
- }
358
- // Issue #347 — return the set of ALL phase ids declared in the job's
359
- // frontmatter (not just the onSuccess-reachable path). Used by
360
- // deriveStages to vet whether a visited phase id genuinely belongs to
361
- // this job before appending it to the tracker, so cross-job pollution
362
- // (agent calling seekMentoring for a different job mid-run) cannot
363
- // surface stages from elsewhere.
364
- function loadAllJobPhaseIds(jobId, projectPath) {
365
- const stubPath = findJobStubPath(projectPath, jobId);
366
- if (!stubPath)
367
- return new Set();
368
- const fm = readJobFrontmatter(stubPath);
369
- if (!fm || !fm.phases) {
370
- const phases = loadJobPhasesFromSteps(stubPath);
371
- return new Set(phases.map((p) => p.id));
372
- }
373
- return new Set(Object.keys(fm.phases));
374
- }
375
- // Issue #347 — public exposure of the friendly-label rule so callers
376
- // outside catalog.ts can render labels for phases that exist in the
377
- // frontmatter but weren't on the reachable path returned by loadJobPhases.
378
- function labelForPhaseId(phaseId, jobId, projectPath) {
379
- const stubPath = findJobStubPath(projectPath, jobId);
380
- if (stubPath) {
381
- const fm = readJobFrontmatter(stubPath);
382
- if (fm?.phaseLabels?.[phaseId])
383
- return fm.phaseLabels[phaseId];
384
- }
385
- return friendlyPhaseLabel(phaseId);
386
- }
387
- function getAiHubCategories(projectPath, options = {}) {
388
- const project = summarizeProject(projectPath);
389
- if (!project.exists || (!project.hasFraim && !options.includeRegistry))
390
- return [];
391
- // A category is any directory found at any layer; deduplicate by id.
392
- const layers = discoverLayers(projectPath, options.includeRegistry
393
- ? [...REGISTRY_EMPLOYEE_JOB_LAYERS, ...EMPLOYEE_JOB_LAYERS]
394
- : EMPLOYEE_JOB_LAYERS);
395
- const seen = new Map();
396
- for (const layer of layers) {
397
- if (!seen.has(layer.categoryId)) {
398
- seen.set(layer.categoryId, {
399
- id: layer.categoryId,
400
- label: humanizeName(layer.categoryId),
401
- description: `${humanizeName(layer.categoryId)} jobs discovered in this project.`,
402
- });
403
- }
404
- }
405
- return [...seen.values()].sort((a, b) => a.label.localeCompare(b.label));
406
- }
@@ -1,70 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.loadOrCreateCert = loadOrCreateCert;
7
- exports.trustCert = trustCert;
8
- const fs_1 = __importDefault(require("fs"));
9
- const path_1 = __importDefault(require("path"));
10
- const os_1 = __importDefault(require("os"));
11
- const child_process_1 = require("child_process");
12
- function certDir() {
13
- const base = process.env.APPDATA ||
14
- (process.platform === 'darwin'
15
- ? path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support')
16
- : path_1.default.join(os_1.default.homedir(), '.config'));
17
- return path_1.default.join(base, 'FRAIM', 'certs');
18
- }
19
- function certPaths() {
20
- const dir = certDir();
21
- return { keyPath: path_1.default.join(dir, 'localhost.key'), certPath: path_1.default.join(dir, 'localhost.crt') };
22
- }
23
- async function loadOrCreateCert() {
24
- const { keyPath, certPath } = certPaths();
25
- if (fs_1.default.existsSync(keyPath) && fs_1.default.existsSync(certPath)) {
26
- return { key: fs_1.default.readFileSync(keyPath, 'utf8'), cert: fs_1.default.readFileSync(certPath, 'utf8') };
27
- }
28
- // eslint-disable-next-line @typescript-eslint/no-require-imports
29
- const selfsigned = require('selfsigned');
30
- const pem = await selfsigned.generate([{ name: 'commonName', value: 'localhost' }], {
31
- keySize: 2048,
32
- days: 3650,
33
- algorithm: 'sha256',
34
- extensions: [
35
- { name: 'subjectAltName', altNames: [{ type: 2, value: 'localhost' }, { type: 7, ip: '127.0.0.1' }] },
36
- { name: 'keyUsage', keyCertSign: true, digitalSignature: true, keyEncipherment: true },
37
- { name: 'extKeyUsage', serverAuth: true },
38
- ],
39
- });
40
- fs_1.default.mkdirSync(path_1.default.dirname(keyPath), { recursive: true });
41
- fs_1.default.writeFileSync(keyPath, pem.private, { mode: 0o600 });
42
- fs_1.default.writeFileSync(certPath, pem.cert, { mode: 0o644 });
43
- // NOTE: we deliberately do NOT install this into the OS trusted-root store here.
44
- // Desktop Word/PowerPoint reach the pane over plain HTTP loopback and need no cert.
45
- // Trusting a root CA triggers a Windows security popup on every add/remove and is
46
- // invasive, so it is reserved for explicit Word-Online opt-in via trustCert() below.
47
- return { key: pem.private, cert: pem.cert };
48
- }
49
- /**
50
- * Installs the cert as a trusted root CA in the user's OS certificate store.
51
- * ONLY call this for explicit Word-Online support — it prompts a Windows security
52
- * dialog. It is idempotent: it no-ops if a matching localhost cert is already
53
- * trusted, so it never produces the delete/add popup churn.
54
- * Windows: certutil -addstore -user Root (skipped if already present)
55
- * macOS: security add-trusted-cert -r trustRoot -k login.keychain
56
- */
57
- function trustCert(certPath) {
58
- if (process.platform === 'win32') {
59
- // Skip if a localhost cert is already trusted — avoids the confirmation popup.
60
- const check = (0, child_process_1.spawnSync)('certutil', ['-user', '-store', 'Root', 'localhost'], { encoding: 'utf8' });
61
- if (check.status === 0 && /localhost/i.test(check.stdout))
62
- return;
63
- (0, child_process_1.spawnSync)('certutil', ['-addstore', '-user', 'Root', certPath], { stdio: 'ignore' });
64
- return;
65
- }
66
- if (process.platform === 'darwin') {
67
- const keychain = path_1.default.join(os_1.default.homedir(), 'Library', 'Keychains', 'login.keychain-db');
68
- (0, child_process_1.spawnSync)('security', ['add-trusted-cert', '-r', 'trustRoot', '-k', keychain, certPath], { stdio: 'ignore' });
69
- }
70
- }