docs-combiner 0.4.0 → 1.0.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 (52) hide show
  1. package/dist/agentApi/agentControlClaim.js +26 -0
  2. package/dist/agentApi/approachMatrixView.js +19 -0
  3. package/dist/agentApi/capabilitiesMeta.js +78 -0
  4. package/dist/agentApi/catalogUrlPresets.js +26 -0
  5. package/dist/agentApi/constants.js +11 -0
  6. package/dist/agentApi/creativeSelections.js +25 -0
  7. package/dist/agentApi/escalation.js +221 -0
  8. package/dist/agentApi/generatedPairsGuard.js +85 -0
  9. package/dist/agentApi/httpHelpers.js +77 -0
  10. package/dist/agentApi/imagesReadiness.js +60 -0
  11. package/dist/agentApi/jobStatus.js +113 -0
  12. package/dist/agentApi/productImageDrive.js +39 -0
  13. package/dist/agentApi/regenerateImages.js +125 -0
  14. package/dist/agentApi/rendererTypes.js +3 -0
  15. package/dist/agentApi/routes.js +440 -0
  16. package/dist/agentApi/sessionSnapshot.js +82 -0
  17. package/dist/agentApi/sessionTypes.js +2 -0
  18. package/dist/agentApi/spec.js +1045 -0
  19. package/dist/agentApi/types.js +2 -0
  20. package/dist/agentApi/validation.js +32 -0
  21. package/dist/campaignsCsv.js +420 -0
  22. package/dist/contentPairs.js +62 -0
  23. package/dist/flexcardBalance.js +88 -0
  24. package/dist/integrations/driveApi.js +420 -0
  25. package/dist/integrations/httpTimeout.js +116 -0
  26. package/dist/integrations/openrouter.js +532 -0
  27. package/dist/main.js +316 -1214
  28. package/dist/models.js +17 -0
  29. package/dist/offerSettings.js +19 -0
  30. package/dist/preload.js +9 -0
  31. package/dist/promptOverrides.js +437 -0
  32. package/dist/prompts.js +1243 -0
  33. package/dist/renderer.js +19 -19
  34. package/dist/renderer.js.map +1 -1
  35. package/dist/server/app.js +378 -0
  36. package/dist/server/auth.js +74 -0
  37. package/dist/server/contentJobs.js +367 -0
  38. package/dist/server/driveCatalog.js +122 -0
  39. package/dist/server/driveProxy.js +220 -0
  40. package/dist/server/flexcardMeta.js +29 -0
  41. package/dist/server/googleAuth.js +84 -0
  42. package/dist/server/imageAssets.js +87 -0
  43. package/dist/server/imageJobs.js +776 -0
  44. package/dist/server/index.js +38 -0
  45. package/dist/server/jobEvents.js +116 -0
  46. package/dist/server/jobs.js +358 -0
  47. package/dist/server/openRouterMeta.js +58 -0
  48. package/dist/server/spec.js +544 -0
  49. package/dist/server/storage.js +133 -0
  50. package/dist/server/telegram.js +53 -0
  51. package/dist/server/workspaceSnapshot.js +273 -0
  52. package/package.json +15 -3
@@ -0,0 +1,544 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getServerApiSpec = getServerApiSpec;
4
+ function getServerApiSpec(baseUrl, version) {
5
+ return {
6
+ openapi: '3.1.0',
7
+ info: {
8
+ title: 'Docs Combiner Server API',
9
+ version,
10
+ description: 'Single-user VPS API for Docs Combiner. Phase 1 skeleton: health, protected spec, write-only secrets and global settings.',
11
+ },
12
+ servers: [{ url: baseUrl }],
13
+ security: [{ bearerAuth: [] }],
14
+ paths: {
15
+ '/api/health': {
16
+ get: {
17
+ security: [],
18
+ summary: 'Public health check',
19
+ responses: {
20
+ '200': { description: 'Server is running' },
21
+ },
22
+ },
23
+ },
24
+ '/api/spec': {
25
+ get: {
26
+ summary: 'Return this API spec',
27
+ responses: {
28
+ '200': { description: 'OpenAPI-like JSON spec' },
29
+ '401': { description: 'Missing or invalid bearer token' },
30
+ },
31
+ },
32
+ },
33
+ '/api/secrets/status': {
34
+ get: {
35
+ summary: 'Return booleans for configured secrets; never returns secret values',
36
+ responses: {
37
+ '200': { description: 'Secret status map' },
38
+ '401': { description: 'Missing or invalid bearer token' },
39
+ },
40
+ },
41
+ },
42
+ '/api/secrets': {
43
+ put: {
44
+ summary: 'Write-only update for server secrets',
45
+ requestBody: {
46
+ required: true,
47
+ content: {
48
+ 'application/json': {
49
+ schema: {
50
+ type: 'object',
51
+ additionalProperties: { type: 'string' },
52
+ },
53
+ },
54
+ },
55
+ },
56
+ responses: {
57
+ '200': { description: 'Secrets updated; response contains only updated keys and status booleans' },
58
+ '400': { description: 'JSON object required' },
59
+ '401': { description: 'Missing or invalid bearer token' },
60
+ },
61
+ },
62
+ },
63
+ '/api/settings': {
64
+ get: {
65
+ summary: 'Return global non-secret server settings',
66
+ responses: {
67
+ '200': { description: 'Settings JSON' },
68
+ '401': { description: 'Missing or invalid bearer token' },
69
+ },
70
+ },
71
+ put: {
72
+ summary: 'Replace global non-secret server settings',
73
+ responses: {
74
+ '200': { description: 'Settings saved' },
75
+ '400': { description: 'JSON object required' },
76
+ '401': { description: 'Missing or invalid bearer token' },
77
+ },
78
+ },
79
+ },
80
+ '/api/catalog': {
81
+ get: {
82
+ summary: 'Read offers, campaign groups, workspaces and campaigns.csv from Google Drive',
83
+ description: 'If folder ids are omitted, the server uses settings.driveRoots from /api/settings.',
84
+ parameters: [
85
+ {
86
+ name: 'offersRootFolderId',
87
+ in: 'query',
88
+ required: false,
89
+ schema: { type: 'string' },
90
+ description: 'Google Drive folder id or folder URL for the offers root.',
91
+ },
92
+ {
93
+ name: 'campaignsRootFolderId',
94
+ in: 'query',
95
+ required: false,
96
+ schema: { type: 'string' },
97
+ description: 'Google Drive folder id or folder URL for the campaigns root.',
98
+ },
99
+ ],
100
+ responses: {
101
+ '200': { description: 'Drive catalog snapshot' },
102
+ '400': { description: 'Missing folder ids or Drive error' },
103
+ '401': { description: 'Missing or invalid bearer token' },
104
+ },
105
+ },
106
+ },
107
+ '/api/workspaces/{workspaceId}': {
108
+ get: {
109
+ summary: 'Read a single workspace snapshot from Google Drive',
110
+ description: 'Reads project-settings.json, campaign binding from campaigns.csv, offer-settings.json and product image availability. Read-only.',
111
+ parameters: [
112
+ {
113
+ name: 'workspaceId',
114
+ in: 'path',
115
+ required: true,
116
+ schema: { type: 'string' },
117
+ description: 'Google Drive workspace folder id.',
118
+ },
119
+ ],
120
+ responses: {
121
+ '200': { description: 'Workspace snapshot' },
122
+ '400': { description: 'Invalid workspace id' },
123
+ '401': { description: 'Missing or invalid bearer token' },
124
+ },
125
+ },
126
+ },
127
+ '/api/google/refresh': {
128
+ post: {
129
+ summary: 'Refresh server-side Google OAuth access token',
130
+ description: 'Uses server-side clientId/clientSecret/refreshToken, writes the fresh accessToken to server secrets, and returns only secret status booleans.',
131
+ responses: {
132
+ '200': { description: 'Google OAuth refreshed' },
133
+ '400': { description: 'Google OAuth secrets missing or refresh failed' },
134
+ '401': { description: 'Missing or invalid bearer token' },
135
+ },
136
+ },
137
+ },
138
+ '/api/drive/files/list': {
139
+ post: {
140
+ summary: 'Proxy Google Drive files.list through the VPS OAuth credentials',
141
+ requestBody: {
142
+ required: true,
143
+ content: {
144
+ 'application/json': {
145
+ schema: {
146
+ type: 'object',
147
+ required: ['q', 'fields'],
148
+ properties: {
149
+ q: { type: 'string' },
150
+ fields: { type: 'string' },
151
+ supportsAllDrives: { type: 'boolean' },
152
+ pageSize: { type: 'number' },
153
+ },
154
+ },
155
+ },
156
+ },
157
+ },
158
+ responses: {
159
+ '200': { description: 'Drive list response' },
160
+ '400': { description: 'Invalid payload or server Google OAuth missing' },
161
+ '401': { description: 'Missing or invalid bearer token' },
162
+ },
163
+ },
164
+ },
165
+ '/api/drive/files/{fileId}/metadata': {
166
+ get: {
167
+ summary: 'Proxy Google Drive files.get metadata',
168
+ responses: {
169
+ '200': { description: 'Drive metadata response' },
170
+ '400': { description: 'Missing fields or server Google OAuth missing' },
171
+ '401': { description: 'Missing or invalid bearer token' },
172
+ },
173
+ },
174
+ },
175
+ '/api/drive/files/{fileId}/download': {
176
+ get: {
177
+ summary: 'Download Drive file bytes through VPS as base64 JSON',
178
+ responses: {
179
+ '200': { description: 'Base64 file content and content type' },
180
+ '400': { description: 'Invalid file id or server Google OAuth missing' },
181
+ '401': { description: 'Missing or invalid bearer token' },
182
+ },
183
+ },
184
+ },
185
+ '/api/drive/folders': {
186
+ post: {
187
+ summary: 'Create a Google Drive folder through VPS',
188
+ responses: {
189
+ '200': { description: 'Drive folder create response' },
190
+ '400': { description: 'Invalid payload or server Google OAuth missing' },
191
+ '401': { description: 'Missing or invalid bearer token' },
192
+ },
193
+ },
194
+ },
195
+ '/api/drive/files/multipart': {
196
+ post: {
197
+ summary: 'Create or update a Drive file using multipart upload through VPS',
198
+ description: 'Accepts JSON multipart parts with text/base64 content to avoid exposing Google OAuth to the client.',
199
+ responses: {
200
+ '200': { description: 'Drive upload response' },
201
+ '400': { description: 'Invalid payload or server Google OAuth missing' },
202
+ '401': { description: 'Missing or invalid bearer token' },
203
+ },
204
+ },
205
+ },
206
+ '/api/drive/files/{fileId}/media': {
207
+ patch: {
208
+ summary: 'Replace Drive file media content through VPS',
209
+ responses: {
210
+ '200': { description: 'Drive media update response' },
211
+ '400': { description: 'Invalid payload or server Google OAuth missing' },
212
+ '401': { description: 'Missing or invalid bearer token' },
213
+ },
214
+ },
215
+ },
216
+ '/api/drive/files/{fileId}': {
217
+ delete: {
218
+ summary: 'Delete a Drive file through VPS',
219
+ responses: {
220
+ '200': { description: 'Drive delete response' },
221
+ '400': { description: 'Invalid file id or server Google OAuth missing' },
222
+ '401': { description: 'Missing or invalid bearer token' },
223
+ },
224
+ },
225
+ },
226
+ '/api/drive/files/{fileId}/permissions': {
227
+ post: {
228
+ summary: 'Create Drive file permission through VPS',
229
+ responses: {
230
+ '200': { description: 'Drive permission create response' },
231
+ '400': { description: 'Invalid payload or server Google OAuth missing' },
232
+ '401': { description: 'Missing or invalid bearer token' },
233
+ },
234
+ },
235
+ },
236
+ '/api/openrouter/models': {
237
+ get: {
238
+ summary: 'Fetch OpenRouter model metadata using the server-side API key',
239
+ description: 'Returns raw OpenRouter models plus pre-filtered image, chat and validation model lists. Secret keys are never returned.',
240
+ responses: {
241
+ '200': { description: 'OpenRouter models fetched and filtered' },
242
+ '400': { description: 'Server OpenRouter key is not configured' },
243
+ '401': { description: 'Missing or invalid bearer token' },
244
+ '502': { description: 'OpenRouter fetch failed' },
245
+ },
246
+ },
247
+ },
248
+ '/api/openrouter/balance': {
249
+ get: {
250
+ summary: 'Fetch OpenRouter account/key balance using the server-side API key',
251
+ responses: {
252
+ '200': { description: 'OpenRouter balance fetched' },
253
+ '400': { description: 'Server OpenRouter key is not configured' },
254
+ '401': { description: 'Missing or invalid bearer token' },
255
+ },
256
+ },
257
+ },
258
+ '/api/flexcard/balance': {
259
+ get: {
260
+ summary: 'Fetch FlexCard USD finance balance using the server-side API key',
261
+ responses: {
262
+ '200': { description: 'FlexCard USD balance fetched' },
263
+ '400': { description: 'Server FlexCard key is not configured' },
264
+ '401': { description: 'Missing or invalid bearer token' },
265
+ },
266
+ },
267
+ },
268
+ '/api/telegram/send': {
269
+ post: {
270
+ summary: 'Send a Telegram notification using server-side bot settings',
271
+ requestBody: {
272
+ required: true,
273
+ content: {
274
+ 'application/json': {
275
+ schema: {
276
+ type: 'object',
277
+ required: ['text'],
278
+ properties: {
279
+ text: { type: 'string' },
280
+ },
281
+ },
282
+ },
283
+ },
284
+ },
285
+ responses: {
286
+ '200': { description: 'Telegram message sent' },
287
+ '400': { description: 'Server Telegram settings missing or text empty' },
288
+ '401': { description: 'Missing or invalid bearer token' },
289
+ },
290
+ },
291
+ },
292
+ '/api/jobs/content': {
293
+ post: {
294
+ summary: 'Create a server-side content generation job',
295
+ description: 'Creates a content job owned by the VPS. Each market is generated as a slot using OpenRouter, parsed into title/text pairs, and retained in job memory for polling or WebSocket events.',
296
+ requestBody: {
297
+ required: true,
298
+ content: {
299
+ 'application/json': {
300
+ schema: {
301
+ type: 'object',
302
+ required: ['workspaceId', 'product', 'markets'],
303
+ properties: {
304
+ workspaceId: { type: 'string' },
305
+ task: { type: 'string', enum: ['generate-pairs', 'translate-ru'] },
306
+ product: { type: 'string' },
307
+ additionalInfo: { type: 'string' },
308
+ model: { type: 'string' },
309
+ count: { type: 'number' },
310
+ selectedPairApproaches: { type: 'array', items: { type: 'number' } },
311
+ pairs: {
312
+ type: 'array',
313
+ description: 'Required for task=translate-ru. Existing title/text pairs to translate to Russian.',
314
+ items: {
315
+ type: 'object',
316
+ properties: {
317
+ title: { type: 'string' },
318
+ text: { type: 'string' },
319
+ },
320
+ },
321
+ },
322
+ markets: {
323
+ type: 'array',
324
+ items: {
325
+ type: 'object',
326
+ required: ['geo'],
327
+ properties: {
328
+ index: { type: 'number' },
329
+ marketId: { type: 'string' },
330
+ marketKey: { type: 'string' },
331
+ geo: { type: 'string' },
332
+ priceWithCurrency: { type: 'string' },
333
+ },
334
+ },
335
+ },
336
+ metadata: { type: 'object' },
337
+ },
338
+ },
339
+ },
340
+ },
341
+ },
342
+ responses: {
343
+ '202': { description: 'Job accepted' },
344
+ '400': { description: 'Invalid payload' },
345
+ '401': { description: 'Missing or invalid bearer token' },
346
+ },
347
+ },
348
+ },
349
+ '/api/jobs/images': {
350
+ post: {
351
+ summary: 'Create a server-side in-memory image job',
352
+ description: 'Creates an image job owned by the VPS and starts OpenRouter image generation for slots with prompts. Generated data URLs stay in server memory and are emitted over WebSocket; validation and Drive upload are later phases.',
353
+ requestBody: {
354
+ required: true,
355
+ content: {
356
+ 'application/json': {
357
+ schema: {
358
+ type: 'object',
359
+ required: ['workspaceId'],
360
+ properties: {
361
+ workspaceId: { type: 'string' },
362
+ count: { type: 'number' },
363
+ model: { type: 'string' },
364
+ referenceImageUrls: { type: 'array', items: { type: 'string' } },
365
+ referenceImageJobSlots: {
366
+ type: 'array',
367
+ items: {
368
+ type: 'object',
369
+ properties: {
370
+ jobId: { type: 'string' },
371
+ slotIndex: { type: 'number' },
372
+ },
373
+ },
374
+ },
375
+ slots: {
376
+ type: 'array',
377
+ items: {
378
+ type: 'object',
379
+ properties: {
380
+ index: { type: 'number' },
381
+ prompt: { type: 'string' },
382
+ approach: { type: 'string' },
383
+ aspectRatio: { type: 'string', enum: ['1:1', '2:3', '4:5'] },
384
+ imageSize: { type: 'string', enum: ['1K', '2K', '4K'] },
385
+ model: { type: 'string' },
386
+ sourceImageUrl: { type: 'string' },
387
+ sourceImageJobSlot: {
388
+ type: 'object',
389
+ properties: {
390
+ jobId: { type: 'string' },
391
+ slotIndex: { type: 'number' },
392
+ },
393
+ },
394
+ referenceImageUrls: { type: 'array', items: { type: 'string' } },
395
+ referenceImageJobSlots: {
396
+ type: 'array',
397
+ items: {
398
+ type: 'object',
399
+ properties: {
400
+ jobId: { type: 'string' },
401
+ slotIndex: { type: 'number' },
402
+ },
403
+ },
404
+ },
405
+ validation: {
406
+ type: 'object',
407
+ description: 'Optional validation input. When present, server validates generated data URL in memory before upload.',
408
+ properties: {
409
+ product: { type: 'string' },
410
+ geo: { type: 'string' },
411
+ approachName: { type: 'string' },
412
+ productReferenceUrl: { type: 'string' },
413
+ priceBrief: { type: 'string' },
414
+ model: { type: 'string' },
415
+ disabled: { type: 'boolean' },
416
+ expectNoBullets: { type: 'boolean' },
417
+ },
418
+ },
419
+ upload: {
420
+ type: 'object',
421
+ description: 'Optional Drive upload config. Uploaded only after validation OK (or disabled validation OK).',
422
+ properties: {
423
+ enabled: { type: 'boolean' },
424
+ folderId: { type: 'string' },
425
+ filename: { type: 'string' },
426
+ },
427
+ },
428
+ },
429
+ },
430
+ },
431
+ metadata: { type: 'object' },
432
+ },
433
+ },
434
+ },
435
+ },
436
+ },
437
+ responses: {
438
+ '202': { description: 'Job accepted' },
439
+ '400': { description: 'Invalid payload' },
440
+ '401': { description: 'Missing or invalid bearer token' },
441
+ },
442
+ },
443
+ },
444
+ '/api/jobs/{jobId}': {
445
+ get: {
446
+ summary: 'Return server image job status',
447
+ parameters: [
448
+ {
449
+ name: 'jobId',
450
+ in: 'path',
451
+ required: true,
452
+ schema: { type: 'string' },
453
+ },
454
+ ],
455
+ responses: {
456
+ '200': { description: 'Job snapshot' },
457
+ '401': { description: 'Missing or invalid bearer token' },
458
+ '404': { description: 'Job not found or expired' },
459
+ },
460
+ },
461
+ delete: {
462
+ summary: 'Cancel a server image job',
463
+ parameters: [
464
+ {
465
+ name: 'jobId',
466
+ in: 'path',
467
+ required: true,
468
+ schema: { type: 'string' },
469
+ },
470
+ ],
471
+ responses: {
472
+ '200': { description: 'Cancelled job snapshot' },
473
+ '401': { description: 'Missing or invalid bearer token' },
474
+ '404': { description: 'Job not found or expired' },
475
+ },
476
+ },
477
+ },
478
+ '/api/jobs/status': {
479
+ post: {
480
+ summary: 'Return multiple server job statuses',
481
+ description: 'Batch-friendly polling endpoint. Accepts up to 100 job ids and returns found job snapshots plus missing ids.',
482
+ requestBody: {
483
+ required: true,
484
+ content: {
485
+ 'application/json': {
486
+ schema: {
487
+ type: 'object',
488
+ required: ['jobIds'],
489
+ properties: {
490
+ jobIds: { type: 'array', items: { type: 'string' } },
491
+ },
492
+ },
493
+ },
494
+ },
495
+ },
496
+ responses: {
497
+ '200': { description: 'Batch job snapshots' },
498
+ '400': { description: 'Invalid payload' },
499
+ '401': { description: 'Missing or invalid bearer token' },
500
+ },
501
+ },
502
+ },
503
+ '/api/jobs/{jobId}/events': {
504
+ get: {
505
+ summary: 'WebSocket stream for image job events',
506
+ description: 'Upgrade to WebSocket. Browser clients authenticate with ?token=...; non-browser clients may use Authorization: Bearer. Optional lastEventId replays retained events.',
507
+ parameters: [
508
+ {
509
+ name: 'jobId',
510
+ in: 'path',
511
+ required: true,
512
+ schema: { type: 'string' },
513
+ },
514
+ {
515
+ name: 'token',
516
+ in: 'query',
517
+ required: false,
518
+ schema: { type: 'string' },
519
+ },
520
+ {
521
+ name: 'lastEventId',
522
+ in: 'query',
523
+ required: false,
524
+ schema: { type: 'number' },
525
+ },
526
+ ],
527
+ responses: {
528
+ '101': { description: 'WebSocket upgrade accepted' },
529
+ '401': { description: 'Missing or invalid bearer token' },
530
+ '404': { description: 'Job not found or expired' },
531
+ },
532
+ },
533
+ },
534
+ },
535
+ components: {
536
+ securitySchemes: {
537
+ bearerAuth: {
538
+ type: 'http',
539
+ scheme: 'bearer',
540
+ },
541
+ },
542
+ },
543
+ };
544
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ServerStore = exports.SERVER_SETTINGS_FILENAME = exports.SERVER_CONFIG_FILENAME = void 0;
16
+ exports.getSecretKeys = getSecretKeys;
17
+ exports.resolveServerDataDir = resolveServerDataDir;
18
+ exports.buildSecretsStatus = buildSecretsStatus;
19
+ const fs_1 = require("fs");
20
+ const path_1 = __importDefault(require("path"));
21
+ exports.SERVER_CONFIG_FILENAME = 'config.json';
22
+ exports.SERVER_SETTINGS_FILENAME = 'settings.json';
23
+ const SECRET_KEYS = [
24
+ 'clientId',
25
+ 'clientSecret',
26
+ 'accessToken',
27
+ 'refreshToken',
28
+ 'openaiApiKey',
29
+ 'flexcardApiKey',
30
+ 'telegramBotToken',
31
+ 'telegramChatId',
32
+ 'apiBearerToken',
33
+ ];
34
+ function getSecretKeys() {
35
+ return [...SECRET_KEYS];
36
+ }
37
+ function resolveServerDataDir(env = process.env) {
38
+ return path_1.default.resolve(env.DOCS_COMBINER_SERVER_DATA_DIR || path_1.default.join(process.cwd(), 'server-data'));
39
+ }
40
+ function ensureDataDir(dataDir) {
41
+ return __awaiter(this, void 0, void 0, function* () {
42
+ yield fs_1.promises.mkdir(dataDir, { recursive: true, mode: 0o700 });
43
+ });
44
+ }
45
+ function readJsonFile(filePath, fallback) {
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ try {
48
+ const raw = yield fs_1.promises.readFile(filePath, 'utf8');
49
+ const parsed = JSON.parse(raw);
50
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
51
+ }
52
+ catch (err) {
53
+ if ((err === null || err === void 0 ? void 0 : err.code) === 'ENOENT')
54
+ return fallback;
55
+ throw err;
56
+ }
57
+ });
58
+ }
59
+ function writeJsonFile(filePath, data, mode) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
62
+ yield fs_1.promises.writeFile(tmpPath, JSON.stringify(data, null, 2) + '\n', { mode });
63
+ yield fs_1.promises.rename(tmpPath, filePath);
64
+ yield fs_1.promises.chmod(filePath, mode);
65
+ });
66
+ }
67
+ class ServerStore {
68
+ constructor(dataDir = resolveServerDataDir()) {
69
+ this.dataDir = dataDir;
70
+ this.configPath = path_1.default.join(dataDir, exports.SERVER_CONFIG_FILENAME);
71
+ this.settingsPath = path_1.default.join(dataDir, exports.SERVER_SETTINGS_FILENAME);
72
+ }
73
+ init() {
74
+ return __awaiter(this, void 0, void 0, function* () {
75
+ yield ensureDataDir(this.dataDir);
76
+ });
77
+ }
78
+ readConfig() {
79
+ return __awaiter(this, void 0, void 0, function* () {
80
+ yield this.init();
81
+ return readJsonFile(this.configPath, {});
82
+ });
83
+ }
84
+ writeConfig(config) {
85
+ return __awaiter(this, void 0, void 0, function* () {
86
+ yield this.init();
87
+ yield writeJsonFile(this.configPath, config, 0o600);
88
+ });
89
+ }
90
+ patchSecrets(input) {
91
+ return __awaiter(this, void 0, void 0, function* () {
92
+ const current = yield this.readConfig();
93
+ const updated = [];
94
+ const ignored = [];
95
+ for (const [key, value] of Object.entries(input || {})) {
96
+ if (!SECRET_KEYS.includes(key)) {
97
+ ignored.push(key);
98
+ continue;
99
+ }
100
+ if (typeof value !== 'string') {
101
+ ignored.push(key);
102
+ continue;
103
+ }
104
+ current[key] = value;
105
+ updated.push(key);
106
+ }
107
+ if (updated.length > 0) {
108
+ yield this.writeConfig(current);
109
+ }
110
+ return { updated, ignored };
111
+ });
112
+ }
113
+ readSettings() {
114
+ return __awaiter(this, void 0, void 0, function* () {
115
+ yield this.init();
116
+ return readJsonFile(this.settingsPath, {});
117
+ });
118
+ }
119
+ writeSettings(settings) {
120
+ return __awaiter(this, void 0, void 0, function* () {
121
+ yield this.init();
122
+ yield writeJsonFile(this.settingsPath, settings, 0o644);
123
+ });
124
+ }
125
+ }
126
+ exports.ServerStore = ServerStore;
127
+ function buildSecretsStatus(config) {
128
+ return SECRET_KEYS.reduce((acc, key) => {
129
+ var _a;
130
+ acc[key] = Boolean((_a = config[key]) === null || _a === void 0 ? void 0 : _a.trim());
131
+ return acc;
132
+ }, {});
133
+ }