home-hosted 0.8.1 → 0.8.3
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.
- package/AGENTS.md +4 -1
- package/README.md +34 -9
- package/dist/cli.js +619 -83
- package/dist/cli.js.map +1 -1
- package/docs/SERVERS.md +19 -1
- package/docs/UI_CREATION.md +29 -1
- package/docs/branding/logo.svg +33 -0
- package/docs/branding/social-preview.png +0 -0
- package/package.json +7 -2
- package/uis/stock/dist/assets/de-CiT_tMnf.js +1 -0
- package/uis/stock/dist/assets/es-JJDGEbt0.js +1 -0
- package/uis/stock/dist/assets/fr-CYIAuJgb.js +1 -0
- package/uis/stock/dist/assets/index-BCPp9MUj.js +46 -0
- package/uis/stock/dist/assets/{index-DwE9bYyS.css → index-Skf2FzpK.css} +1 -1
- package/uis/stock/dist/assets/ja-DmykOijW.js +1 -0
- package/uis/stock/dist/assets/ko-C4EcOJB0.js +1 -0
- package/uis/stock/dist/assets/ru-Dlbre_Q1.js +1 -0
- package/uis/stock/dist/assets/vi-DhBWYY9I.js +1 -0
- package/uis/stock/dist/assets/zh-CN-BI0ZDO_Y.js +1 -0
- package/uis/stock/dist/favicon.ico +0 -0
- package/uis/stock/dist/favicon.svg +14 -0
- package/uis/stock/dist/index.html +6 -2
- package/uis/stock/dist/ui.json +3 -3
- package/uis/stock/dist/assets/de-DRY6VVqP.js +0 -1
- package/uis/stock/dist/assets/es-CKPLbx5t.js +0 -1
- package/uis/stock/dist/assets/fr-CQ7_4vtN.js +0 -1
- package/uis/stock/dist/assets/index-HT6jUD-L.js +0 -46
- package/uis/stock/dist/assets/ja-bRUylgOt.js +0 -1
- package/uis/stock/dist/assets/ko-DWqbA99M.js +0 -1
- package/uis/stock/dist/assets/ru-DUFxhVp3.js +0 -1
- package/uis/stock/dist/assets/vi-B0-81iVf.js +0 -1
- package/uis/stock/dist/assets/zh-CN-CeoyD9BM.js +0 -1
package/dist/cli.js
CHANGED
|
@@ -709,7 +709,7 @@ function parseBind(value) {
|
|
|
709
709
|
const parsed = bindSchema(value);
|
|
710
710
|
return parsed instanceof type.errors ? null : parsed;
|
|
711
711
|
}
|
|
712
|
-
var bindSchema, workspaceIdSchema, workspaceSchema, workspaceCreateSchema, workspaceRenameSchema, portSchema, onPortConflictSchema, onPortConflictDefaultSchema, restartSchema, httpCheckSchema, resourcesSchema, healthSchema, stopSchema, bootstrapSchema, bootstrapOrNullSchema, labelOrNullSchema, logBufferLinesSchema, serverSchema, authSchema, telegramSchema, notificationsSchema, logsSchema, tlsSchema, hostSchema, ddnsRecordTypeSchema, ddnsAccountSchema, ddnsDomainSchema, ddnsIpv4Schema, ddnsIpv6Schema, ddnsConfigSchema, ddnsRecordStateSchema, ddnsRecordViewSchema, ddnsStatusSchema, ddnsProviderFieldSchema, ddnsProviderInfoSchema, ddnsViewSchema, ddnsCredentialsSchema, backupsSchema, controlSchema, defaultsSchema, restartPatchSchema, httpCheckPatchSchema, resourcesPatchSchema, healthPatchSchema, stopPatchSchema, authPatchSchema, telegramPatchSchema, notificationsPatchSchema, hostPatchSchema, backupsPatchSchema, logsPatchSchema, tlsPatchSchema, controlPatchSchema, defaultsPatchSchema, editableFields, serverPatchSchema, serverCreateSchema, nannySpecSchema, nannyExitSchema, nannyStateSchema, settingsPatchSchema, workspaceSettingsPatchSchema, authStatusSchema, tlsStatusSchema, proxyEngineSchema, proxyTargetSchema, proxyTlsModeSchema, proxyRouteSchema, proxyCertificateSchema, proxyCertificateStateSchema, proxyDns01Schema, proxyConfigSchema, proxyEngineSourceSchema, proxyEngineStatusSchema, proxyEngineInfoSchema, proxyRouteStatusSchema, proxyRouteViewSchema, proxyRunStateSchema, proxyStatusSchema, proxyCertificateViewSchema, proxyDnsAccountViewSchema, proxyViewSchema, proxyRoutePatchSchema, proxyPatchSchema, proxyCertificateUploadSchema, proxyEngineInstallSchema, telegramStatusSchema, telegramChatsSchema, notificationViewSchema, sessionViewSchema, loginSchema, passwordValueSchema, passwordSchema, serverStatusSchema, healthStateSchema, portStateSchema, logStreamSchema, logLineSchema, historyEventSchema, serverHistorySchema, processResourcesSchema, hostDiskSchema, hostViewSchema, backupFileSchema, backupEntrySchema, backupsViewSchema, restoreItemSchema, restorePlanSchema, backupCreateSchema, restoreRequestSchema, serverViewSchema, healthResponseSchema, controlViewSchema, uiMetaSchema, uiStatusSchema, workspaceViewSchema, appStateSchema, logQuerySchema, freePortResultSchema, logHistoryQuerySchema, logFileInfoSchema, logServerViewSchema, logServersViewSchema, notificationActionSchema, telegramTokenSchema, apiErrorSchema, tlsUploadSchema, settingsViewSchema, settingsSavedSchema, workspaceSettingsViewSchema, workspaceSettingsSavedSchema;
|
|
712
|
+
var bindSchema, workspaceIdSchema, workspaceSchema, workspaceCreateSchema, workspaceRenameSchema, portSchema, onPortConflictSchema, onPortConflictDefaultSchema, restartSchema, httpCheckSchema, resourcesSchema, healthSchema, stopSchema, bootstrapSchema, bootstrapOrNullSchema, labelOrNullSchema, logBufferLinesSchema, serverSchema, authSchema, telegramSchema, notificationsSchema, logsSchema, tlsSchema, hostSchema, ddnsRecordTypeSchema, ddnsAccountSchema, ddnsDomainSchema, ddnsIpv4Schema, ddnsIpv6Schema, ddnsConfigSchema, ddnsRecordStateSchema, ddnsRecordViewSchema, ddnsStatusSchema, ddnsProviderFieldSchema, ddnsProviderInfoSchema, ddnsViewSchema, ddnsCredentialsSchema, backupsSchema, controlSchema, defaultsSchema, restartPatchSchema, httpCheckPatchSchema, resourcesPatchSchema, healthPatchSchema, stopPatchSchema, authPatchSchema, telegramPatchSchema, notificationsPatchSchema, hostPatchSchema, backupsPatchSchema, logsPatchSchema, tlsPatchSchema, controlPatchSchema, defaultsPatchSchema, editableFields, serverPatchSchema, serverCreateSchema, nannySpecSchema, nannyExitSchema, nannyStateSchema, entryStateSchema, settingsPatchSchema, workspaceSettingsPatchSchema, authStatusSchema, tlsStatusSchema, proxyEngineSchema, proxyTargetSchema, proxyTlsModeSchema, proxyRouteSchema, proxyCertificateSchema, proxyCertificateStateSchema, proxyDns01Schema, proxyConfigSchema, proxyEngineSourceSchema, proxyEngineStatusSchema, proxyEngineInfoSchema, proxyRouteStatusSchema, proxyRouteViewSchema, proxyRunStateSchema, proxyStatusSchema, proxyCertificateViewSchema, proxyDnsAccountViewSchema, proxyViewSchema, proxyRoutePatchSchema, proxyPatchSchema, proxyCertificateUploadSchema, proxyEngineInstallSchema, telegramStatusSchema, telegramChatsSchema, notificationViewSchema, sessionViewSchema, loginSchema, passwordValueSchema, passwordSchema, serverStatusSchema, healthStateSchema, portStateSchema, logStreamSchema, logLineSchema, historyActorSchema, historyEventSchema, serverHistorySchema, processResourcesSchema, hostDiskSchema, hostViewSchema, backupFileSchema, backupEntrySchema, backupsViewSchema, restoreItemSchema, restoreSkipSchema, restorePlanSchema, backupCreateSchema, restoreRequestSchema, serverViewSchema, healthResponseSchema, controlViewSchema, uiMetaSchema, uiStatusSchema, workspaceViewSchema, appStateSchema, logQuerySchema, freePortResultSchema, logHistoryQuerySchema, logFileInfoSchema, logServerViewSchema, logServersViewSchema, notificationActionSchema, telegramTokenSchema, apiErrorSchema, tlsUploadSchema, settingsViewSchema, settingsSavedSchema, workspaceSettingsViewSchema, workspaceSettingsSavedSchema;
|
|
713
713
|
var init_contracts = __esmMin((() => {
|
|
714
714
|
bindSchema = type("\"local\" | \"lan\" | /^\\d{1,3}(?:\\.\\d{1,3}){3}$/");
|
|
715
715
|
workspaceIdSchema = type("/^[a-z0-9][a-z0-9_-]*$/");
|
|
@@ -1154,6 +1154,19 @@ maxRssBytes: "number.integer >= 0 = 0" }).onUndeclaredKey("reject");
|
|
|
1154
1154
|
"heartbeatAt": "number",
|
|
1155
1155
|
"lastExit?": nannyExitSchema
|
|
1156
1156
|
}).onUndeclaredKey("reject");
|
|
1157
|
+
entryStateSchema = type({
|
|
1158
|
+
/** Attempts spent on the current crash sequence; reset once the process stays up. */
|
|
1159
|
+
"restarts": "number.integer >= 0",
|
|
1160
|
+
/** The `runOnce` bootstrap already ran for this entry. */
|
|
1161
|
+
"bootstrapDone": "boolean",
|
|
1162
|
+
/**
|
|
1163
|
+
* The last child this panel spawned for a non-persistent entry. A detached child
|
|
1164
|
+
* outlives the panel, so this is the only way a later boot recognizes its own
|
|
1165
|
+
* survivor instead of spawning a second copy beside it.
|
|
1166
|
+
*/
|
|
1167
|
+
"lastPid?": "number | null",
|
|
1168
|
+
"lastStartedAt?": "number | null"
|
|
1169
|
+
});
|
|
1157
1170
|
settingsPatchSchema = type({
|
|
1158
1171
|
control: controlPatchSchema.optional(),
|
|
1159
1172
|
host: hostPatchSchema.optional(),
|
|
@@ -1419,13 +1432,16 @@ maxRssBytes: "number.integer >= 0 = 0" }).onUndeclaredKey("reject");
|
|
|
1419
1432
|
stream: logStreamSchema,
|
|
1420
1433
|
text: "string"
|
|
1421
1434
|
});
|
|
1435
|
+
historyActorSchema = type.enumerated("policy", "user", "agent", "boot");
|
|
1422
1436
|
historyEventSchema = type({
|
|
1423
|
-
serverId: "string",
|
|
1424
|
-
ts: "number",
|
|
1425
|
-
type: "\"start\" | \"exit\" | \"crash\" | \"forced-restart\" | \"unhealthy\" | \"recovered\"",
|
|
1426
|
-
detail: "string",
|
|
1437
|
+
"serverId": "string",
|
|
1438
|
+
"ts": "number",
|
|
1439
|
+
"type": "\"start\" | \"exit\" | \"crash\" | \"forced-restart\" | \"unhealthy\" | \"recovered\"",
|
|
1440
|
+
"detail": "string",
|
|
1427
1441
|
/** How long the process had been up, recorded on exit and crash. */
|
|
1428
|
-
runtimeMs: "number?"
|
|
1442
|
+
"runtimeMs": "number?",
|
|
1443
|
+
/** Who caused it; absent on events from before this field existed. */
|
|
1444
|
+
"actor?": historyActorSchema
|
|
1429
1445
|
});
|
|
1430
1446
|
serverHistorySchema = type({
|
|
1431
1447
|
windowMs: "number",
|
|
@@ -1475,21 +1491,37 @@ maxRssBytes: "number.integer >= 0 = 0" }).onUndeclaredKey("reject");
|
|
|
1475
1491
|
backupEntrySchema = type({
|
|
1476
1492
|
/** `global:settings` | `global:secrets` | `global:tls` | `workspace:<id>`. */
|
|
1477
1493
|
"id": "string",
|
|
1494
|
+
/**
|
|
1495
|
+
* The panel's own English wording. Kept because it is what an older UI reads, and what a
|
|
1496
|
+
* single-language UI wants; a translated UI prefers the `*Key` beside it.
|
|
1497
|
+
*/
|
|
1478
1498
|
"label": "string",
|
|
1499
|
+
/** A catalog key for `label`, with its placeholder values, so a UI can render its own language. */
|
|
1500
|
+
"labelKey?": "string",
|
|
1501
|
+
"labelParams?": "Record<string, string>",
|
|
1479
1502
|
"kind": "\"settings\" | \"secrets\" | \"tls\" | \"workspace\"",
|
|
1480
1503
|
"workspaceId?": "string",
|
|
1481
1504
|
"note": "string | null",
|
|
1505
|
+
/** The same pair for `note`: the panel knows which reason it is, the UI knows the wording. */
|
|
1506
|
+
"noteKey?": "string",
|
|
1507
|
+
"noteParams?": "Record<string, string>",
|
|
1482
1508
|
/** What this entry would capture; a workspace carries several leaves. */
|
|
1483
1509
|
"items": type({
|
|
1484
1510
|
"id": "string",
|
|
1485
1511
|
"label": "string",
|
|
1512
|
+
"labelKey?": "string",
|
|
1513
|
+
"labelParams?": "Record<string, string>",
|
|
1486
1514
|
/** `settings`/`servers`/`secrets` for a workspace, `data` for a declared path. */
|
|
1487
1515
|
"kind": "\"settings\" | \"servers\" | \"secrets\" | \"data\"",
|
|
1516
|
+
/** Which secret a `secrets` leaf carries (`password`, `apiToken`, `telegram`, `ddns`, …). */
|
|
1517
|
+
"secret?": "string",
|
|
1488
1518
|
"path?": "string",
|
|
1489
1519
|
/** Who declared a data path: `global`, `<serverId>:backupPaths` or `<serverId>:<ENV>`. */
|
|
1490
1520
|
"origin?": "string",
|
|
1491
1521
|
"included": "boolean",
|
|
1492
1522
|
"note": "string | null",
|
|
1523
|
+
"noteKey?": "string",
|
|
1524
|
+
"noteParams?": "Record<string, string>",
|
|
1493
1525
|
"ignoreGenerated?": "boolean"
|
|
1494
1526
|
}).array()
|
|
1495
1527
|
});
|
|
@@ -1507,27 +1539,53 @@ maxRssBytes: "number.integer >= 0 = 0" }).onUndeclaredKey("reject");
|
|
|
1507
1539
|
/** A backup entry or leaf id (`workspace:<id>:servers`, `data:<path>`, ...). */
|
|
1508
1540
|
"id": "string",
|
|
1509
1541
|
"label": "string",
|
|
1542
|
+
"labelKey?": "string",
|
|
1543
|
+
"labelParams?": "Record<string, string>",
|
|
1510
1544
|
"kind": "\"settings\" | \"secrets\" | \"servers\" | \"tls\" | \"data\"",
|
|
1511
1545
|
"workspaceId?": "string",
|
|
1512
1546
|
/** false when the current instance does not declare it, or it is not in the archive. */
|
|
1513
1547
|
"restorable": "boolean",
|
|
1514
1548
|
/** Echo of the request's selection, so the checkboxes round-trip. */
|
|
1515
1549
|
"selected": "boolean",
|
|
1516
|
-
"note": "string | null"
|
|
1550
|
+
"note": "string | null",
|
|
1551
|
+
"noteKey?": "string",
|
|
1552
|
+
"noteParams?": "Record<string, string>"
|
|
1553
|
+
});
|
|
1554
|
+
restoreSkipSchema = type({
|
|
1555
|
+
"label": "string",
|
|
1556
|
+
"labelKey?": "string",
|
|
1557
|
+
"labelParams?": "Record<string, string>",
|
|
1558
|
+
/** A catalog key for the parenthesis, e.g. `not selected`; absent when there is none. */
|
|
1559
|
+
"reasonKey?": "string",
|
|
1560
|
+
"reasonParams?": "Record<string, string>"
|
|
1517
1561
|
});
|
|
1518
1562
|
restorePlanSchema = type({
|
|
1519
|
-
dryRun: "boolean",
|
|
1520
|
-
encrypted: "boolean",
|
|
1563
|
+
"dryRun": "boolean",
|
|
1564
|
+
"encrypted": "boolean",
|
|
1521
1565
|
/** The archive needs a password (none or a wrong one was supplied). */
|
|
1522
|
-
needsPassword: "boolean",
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1566
|
+
"needsPassword": "boolean",
|
|
1567
|
+
/**
|
|
1568
|
+
* Where the archive says it came from. Optional: an archive written before this
|
|
1569
|
+
* stamp existed carries neither field, and an older panel never sends the row.
|
|
1570
|
+
*/
|
|
1571
|
+
"archive?": type({
|
|
1572
|
+
"writtenBy?": "string",
|
|
1573
|
+
"schema?": "number"
|
|
1574
|
+
}),
|
|
1575
|
+
"items": restoreItemSchema.array(),
|
|
1576
|
+
"applied": type("string[]"),
|
|
1577
|
+
/**
|
|
1578
|
+
* The ids `applied` names, so a UI resolves each through `items` and renders its own label.
|
|
1579
|
+
* `applied` itself stays: it is what a UI with one language reads.
|
|
1580
|
+
*/
|
|
1581
|
+
"appliedIds?": type("string[]"),
|
|
1582
|
+
"skipped": type("string[]"),
|
|
1583
|
+
"skippedItems?": restoreSkipSchema.array(),
|
|
1526
1584
|
/** Only the panel's own listener needs a restart; its servers are re-read live. */
|
|
1527
|
-
restartRequired: "boolean",
|
|
1585
|
+
"restartRequired": "boolean",
|
|
1528
1586
|
/** The panel re-read the restored config within this same restore. */
|
|
1529
|
-
reloaded: "boolean",
|
|
1530
|
-
error: "string?"
|
|
1587
|
+
"reloaded": "boolean",
|
|
1588
|
+
"error": "string?"
|
|
1531
1589
|
});
|
|
1532
1590
|
backupCreateSchema = type({
|
|
1533
1591
|
/** Optional: encrypts the archive. Never stored. */
|
|
@@ -2603,6 +2661,14 @@ function createAuthGuard(deps) {
|
|
|
2603
2661
|
await next();
|
|
2604
2662
|
};
|
|
2605
2663
|
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Who is behind a request, as a history event names them: a cookie is a person at
|
|
2666
|
+
* the panel, a bearer token is a script or an agent. Anonymous callers can only be
|
|
2667
|
+
* loopback requests while auth is unarmed, which is the browser's own origin.
|
|
2668
|
+
*/
|
|
2669
|
+
function requestActor(c, auth) {
|
|
2670
|
+
return requestIdentity(c, auth).method === "token" ? "agent" : "user";
|
|
2671
|
+
}
|
|
2606
2672
|
var PUBLIC_PATHS, AUTH_REQUIRED_CODE;
|
|
2607
2673
|
var init_auth = __esmMin((() => {
|
|
2608
2674
|
init_loopback();
|
|
@@ -3611,7 +3677,7 @@ function createControlRoute(deps) {
|
|
|
3611
3677
|
}
|
|
3612
3678
|
}), validate("query", workspaceQuerySchema), async (c) => {
|
|
3613
3679
|
assertLocalCall(c, "start or stop a server");
|
|
3614
|
-
const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.start(c.req.param("id"));
|
|
3680
|
+
const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.start(c.req.param("id"), { actor: "user" });
|
|
3615
3681
|
return c.json(result, statusForAction(result));
|
|
3616
3682
|
}).post("/servers/:id/stop", describeRoute({
|
|
3617
3683
|
tags: ["panel"],
|
|
@@ -3635,7 +3701,7 @@ function createControlRoute(deps) {
|
|
|
3635
3701
|
}
|
|
3636
3702
|
}), validate("query", workspaceQuerySchema), async (c) => {
|
|
3637
3703
|
assertLocalCall(c, "restart a server");
|
|
3638
|
-
const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.restart(c.req.param("id"));
|
|
3704
|
+
const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.restart(c.req.param("id"), { actor: "user" });
|
|
3639
3705
|
return c.json(result, statusForAction(result));
|
|
3640
3706
|
});
|
|
3641
3707
|
}
|
|
@@ -6391,8 +6457,8 @@ var init_events = __esmMin((() => {
|
|
|
6391
6457
|
/**
|
|
6392
6458
|
* Liveness for external monitors. Mounted outside `/api`, so it answers without a
|
|
6393
6459
|
* session: it reports whether the panel itself is serving, and 503 when an
|
|
6394
|
-
* autostart server has crashed
|
|
6395
|
-
* authenticated caller.
|
|
6460
|
+
* autostart server has crashed, or a running one is failing its own health check.
|
|
6461
|
+
* Server details are only included for an authenticated caller.
|
|
6396
6462
|
*/
|
|
6397
6463
|
function createHealthRoute(deps) {
|
|
6398
6464
|
return appFactory.createApp().get("/healthz", describeRoute({
|
|
@@ -6403,12 +6469,12 @@ function createHealthRoute(deps) {
|
|
|
6403
6469
|
description: "Serving",
|
|
6404
6470
|
content: jsonBody(healthResponseSchema)
|
|
6405
6471
|
},
|
|
6406
|
-
503: { description: "An autostart server has crashed" }
|
|
6472
|
+
503: { description: "An autostart server has crashed, or a running one is unhealthy" }
|
|
6407
6473
|
}
|
|
6408
6474
|
}), (c) => {
|
|
6409
6475
|
const state = deps.panel.getState();
|
|
6410
6476
|
const servers = state.workspaces.flatMap((workspace) => workspace.servers);
|
|
6411
|
-
const broken = servers.filter((server) => server.config.autostart && server.status === "crashed");
|
|
6477
|
+
const broken = servers.filter((server) => server.config.autostart && server.status === "crashed" || server.status === "running" && server.health === "unhealthy");
|
|
6412
6478
|
const authenticated = requestIdentity(c, deps.auth).authenticated;
|
|
6413
6479
|
return c.json({
|
|
6414
6480
|
status: broken.length > 0 ? "degraded" : "ok",
|
|
@@ -10329,7 +10395,7 @@ function createServersRoute(deps) {
|
|
|
10329
10395
|
404: ERROR_RESPONSES[404]
|
|
10330
10396
|
}
|
|
10331
10397
|
}), validate("query", workspaceQuerySchema), validate("param", idParam$1), async (c) => {
|
|
10332
|
-
const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.start(c.req.valid("param").id);
|
|
10398
|
+
const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.start(c.req.valid("param").id, { actor: requestActor(c, deps.auth) });
|
|
10333
10399
|
return c.json(result, statusForAction(result));
|
|
10334
10400
|
}).post("/:id/stop", describeRoute({
|
|
10335
10401
|
tags: ["servers"],
|
|
@@ -10349,7 +10415,7 @@ function createServersRoute(deps) {
|
|
|
10349
10415
|
404: ERROR_RESPONSES[404]
|
|
10350
10416
|
}
|
|
10351
10417
|
}), validate("query", workspaceQuerySchema), validate("param", idParam$1), async (c) => {
|
|
10352
|
-
const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.restart(c.req.valid("param").id);
|
|
10418
|
+
const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.restart(c.req.valid("param").id, { actor: requestActor(c, deps.auth) });
|
|
10353
10419
|
return c.json(result, statusForAction(result));
|
|
10354
10420
|
}).post("/:id/clear-logs", describeRoute({
|
|
10355
10421
|
tags: ["servers"],
|
|
@@ -10437,6 +10503,7 @@ var init_$_routes$1 = __esmMin((() => {
|
|
|
10437
10503
|
init_openapi$1();
|
|
10438
10504
|
init_validator();
|
|
10439
10505
|
init_workspace();
|
|
10506
|
+
init_auth();
|
|
10440
10507
|
init_events$1();
|
|
10441
10508
|
init_contracts();
|
|
10442
10509
|
idParam$1 = type({ id: "string >= 1" });
|
|
@@ -11395,6 +11462,90 @@ var init_acme_challenge = __esmMin((() => {
|
|
|
11395
11462
|
};
|
|
11396
11463
|
}));
|
|
11397
11464
|
//#endregion
|
|
11465
|
+
//#region src/config/secret-parts.ts
|
|
11466
|
+
/**
|
|
11467
|
+
* The parts of one scope, in a stable order: the known parts first in the fixed order
|
|
11468
|
+
* password, apiToken (global) / telegram, ddns (workspace) — then any other top-level
|
|
11469
|
+
* key present in `present`, sorted, as its own part with `fields: [key]`.
|
|
11470
|
+
*
|
|
11471
|
+
* An unknown key is taken as belonging to the scope being asked about, because the file
|
|
11472
|
+
* it was read from is the only evidence of where it lives: this release does not know the
|
|
11473
|
+
* key, so classifying it by name would drop a future *global* secret on the floor.
|
|
11474
|
+
*/
|
|
11475
|
+
function secretParts(scope, present) {
|
|
11476
|
+
const keys = new Set(present);
|
|
11477
|
+
const known = KNOWN[scope];
|
|
11478
|
+
const parts = known.filter((key) => keys.has(key)).map((key) => ({
|
|
11479
|
+
key,
|
|
11480
|
+
fields: EXTRA_FIELDS[key] ?? [key],
|
|
11481
|
+
scope
|
|
11482
|
+
}));
|
|
11483
|
+
const consumed = new Set(parts.flatMap((part) => part.fields));
|
|
11484
|
+
const rest = [...keys].filter((key) => KNOWN_SCOPES[key] === void 0 && !known.includes(key) && !consumed.has(key)).sort();
|
|
11485
|
+
return [...parts, ...rest.map((key) => ({
|
|
11486
|
+
key,
|
|
11487
|
+
fields: [key],
|
|
11488
|
+
scope
|
|
11489
|
+
}))];
|
|
11490
|
+
}
|
|
11491
|
+
/** The top-level keys of a raw secrets document that hold a non-null value. */
|
|
11492
|
+
function presentSecretKeys(source) {
|
|
11493
|
+
return Object.entries(source).filter(([, value]) => value !== null && value !== void 0).map(([key]) => key);
|
|
11494
|
+
}
|
|
11495
|
+
/**
|
|
11496
|
+
* A JSON string carrying only `fields`, taken from `source`. Returns null when `source`
|
|
11497
|
+
* carries none of them. Always includes a field whose value is `null` ONLY if it is
|
|
11498
|
+
* present as a key and non-null; a null/absent field is omitted.
|
|
11499
|
+
*/
|
|
11500
|
+
function pickSecrets(source, fields) {
|
|
11501
|
+
const picked = {};
|
|
11502
|
+
for (const field of fields) {
|
|
11503
|
+
const value = source[field];
|
|
11504
|
+
if (value !== null && value !== void 0) picked[field] = value;
|
|
11505
|
+
}
|
|
11506
|
+
return Object.keys(picked).length === 0 ? null : JSON.stringify(picked);
|
|
11507
|
+
}
|
|
11508
|
+
/** Parses a secrets document, tolerating anything: not-an-object, or broken JSON returns {}. */
|
|
11509
|
+
function parseSecrets(text) {
|
|
11510
|
+
try {
|
|
11511
|
+
const parsed = JSON.parse(text);
|
|
11512
|
+
return isRecord(parsed) ? parsed : {};
|
|
11513
|
+
} catch {
|
|
11514
|
+
return {};
|
|
11515
|
+
}
|
|
11516
|
+
}
|
|
11517
|
+
/**
|
|
11518
|
+
* Merges `piece` into `current`, changing ONLY the fields in `fields` — every other key
|
|
11519
|
+
* of `current` is preserved (this is what stops restoring the Telegram token from wiping
|
|
11520
|
+
* the DDNS credentials). A field that is absent from `piece` is also DELETED from the
|
|
11521
|
+
* result, so restoring an older archive that predates a secret clears it rather than
|
|
11522
|
+
* leaving a stale value. Returns a new object; does not mutate its arguments.
|
|
11523
|
+
*/
|
|
11524
|
+
function mergeSecrets(current, piece, fields) {
|
|
11525
|
+
const merged = { ...current };
|
|
11526
|
+
for (const field of fields) {
|
|
11527
|
+
const value = piece[field];
|
|
11528
|
+
if (value === null || value === void 0) delete merged[field];
|
|
11529
|
+
else merged[field] = value;
|
|
11530
|
+
}
|
|
11531
|
+
return merged;
|
|
11532
|
+
}
|
|
11533
|
+
var KNOWN, KNOWN_SCOPES, EXTRA_FIELDS;
|
|
11534
|
+
var init_secret_parts = __esmMin((() => {
|
|
11535
|
+
init_shape();
|
|
11536
|
+
KNOWN = {
|
|
11537
|
+
global: ["password", "apiToken"],
|
|
11538
|
+
workspace: ["telegram", "ddns"]
|
|
11539
|
+
};
|
|
11540
|
+
KNOWN_SCOPES = {
|
|
11541
|
+
password: "global",
|
|
11542
|
+
apiToken: "global",
|
|
11543
|
+
telegram: "workspace",
|
|
11544
|
+
ddns: "workspace"
|
|
11545
|
+
};
|
|
11546
|
+
EXTRA_FIELDS = { ddns: ["ddns", "ddnsKdf"] };
|
|
11547
|
+
}));
|
|
11548
|
+
//#endregion
|
|
11398
11549
|
//#region src/helpers/env-file.ts
|
|
11399
11550
|
/** `KEY=value` files, with optional `export `, `#` comments and quoted values. */
|
|
11400
11551
|
function parseEnvFile(text) {
|
|
@@ -11652,6 +11803,41 @@ function computeBackoff(attempt, options) {
|
|
|
11652
11803
|
}
|
|
11653
11804
|
var init_backoff = __esmMin((() => {}));
|
|
11654
11805
|
//#endregion
|
|
11806
|
+
//#region src/providers/entry-state.ts
|
|
11807
|
+
/**
|
|
11808
|
+
* The small slice of a server's runtime the panel has to remember across its own
|
|
11809
|
+
* restarts: how much of the retry budget is spent, and whether a `runOnce` bootstrap
|
|
11810
|
+
* already ran.
|
|
11811
|
+
*
|
|
11812
|
+
* Both lived only in memory, so a panel restart — a `restart`, a crash, a reboot —
|
|
11813
|
+
* handed a crash-looping server a fresh `maxRetries` and re-ran its bootstrap. The
|
|
11814
|
+
* retry counter has to outlive the process that increments it to bound anything.
|
|
11815
|
+
*/
|
|
11816
|
+
/** One JSON file per entry, beside the nanny's own state. */
|
|
11817
|
+
function entryStatePath(dir, id) {
|
|
11818
|
+
return path.join(dir, `${id}.run.json`);
|
|
11819
|
+
}
|
|
11820
|
+
function readEntryState(file) {
|
|
11821
|
+
try {
|
|
11822
|
+
const parsed = entryStateSchema(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
11823
|
+
return parsed instanceof type.errors ? null : parsed;
|
|
11824
|
+
} catch {
|
|
11825
|
+
return null;
|
|
11826
|
+
}
|
|
11827
|
+
}
|
|
11828
|
+
function writeEntryState(file, state) {
|
|
11829
|
+
try {
|
|
11830
|
+
writeFileAtomic(file, `${JSON.stringify(state)}\n`);
|
|
11831
|
+
} catch {}
|
|
11832
|
+
}
|
|
11833
|
+
function clearEntryState(file) {
|
|
11834
|
+
fs.rmSync(file, { force: true });
|
|
11835
|
+
}
|
|
11836
|
+
var init_entry_state = __esmMin((() => {
|
|
11837
|
+
init_atomic();
|
|
11838
|
+
init_contracts();
|
|
11839
|
+
}));
|
|
11840
|
+
//#endregion
|
|
11655
11841
|
//#region src/providers/health-check.ts
|
|
11656
11842
|
/** TCP connect timing, used for the default `port` mode and readiness. */
|
|
11657
11843
|
async function probeTcp(host, port, timeoutMs) {
|
|
@@ -12272,6 +12458,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
12272
12458
|
init_paths();
|
|
12273
12459
|
init_runtime();
|
|
12274
12460
|
init_template();
|
|
12461
|
+
init_entry_state();
|
|
12275
12462
|
init_health_check();
|
|
12276
12463
|
init_identity();
|
|
12277
12464
|
init_nanny$2();
|
|
@@ -12339,7 +12526,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
12339
12526
|
}
|
|
12340
12527
|
}
|
|
12341
12528
|
const targets = [...this.entries.values()].filter((entry) => !options.autostartOnly || entry.config.autostart).map((entry) => entry.config);
|
|
12342
|
-
for (const config of orderByDependencies(targets)) await this.start(config.id);
|
|
12529
|
+
for (const config of orderByDependencies(targets)) await this.start(config.id, { actor: "boot" });
|
|
12343
12530
|
}
|
|
12344
12531
|
/**
|
|
12345
12532
|
* Stops every entry this command owns. A persistent entry is deliberately not among
|
|
@@ -12370,11 +12557,13 @@ var init_supervisor = __esmMin((() => {
|
|
|
12370
12557
|
error: `server "${id}" is stopping`
|
|
12371
12558
|
};
|
|
12372
12559
|
entry.starting = true;
|
|
12560
|
+
entry.actor = options.retry ? "policy" : options.actor ?? "policy";
|
|
12373
12561
|
this.clearRetry(entry);
|
|
12374
12562
|
if (!options.retry) {
|
|
12375
12563
|
entry.restarts = 0;
|
|
12376
12564
|
entry.healthFailures = 0;
|
|
12377
12565
|
entry.unhealthySince = null;
|
|
12566
|
+
this.persistEntryState(entry);
|
|
12378
12567
|
}
|
|
12379
12568
|
try {
|
|
12380
12569
|
await this.startDependencies(entry);
|
|
@@ -12387,6 +12576,14 @@ var init_supervisor = __esmMin((() => {
|
|
|
12387
12576
|
entry.health = entry.config.health.enabled ? "unknown" : "disabled";
|
|
12388
12577
|
this.publishServer(entry);
|
|
12389
12578
|
if (entry.config.persistent && await this.resumePersistent(entry)) return { ok: true };
|
|
12579
|
+
if (!entry.config.persistent) {
|
|
12580
|
+
const recorded = await this.resolveRecordedChild(entry);
|
|
12581
|
+
if (recorded.kind === "blocked") return {
|
|
12582
|
+
ok: false,
|
|
12583
|
+
error: recorded.error
|
|
12584
|
+
};
|
|
12585
|
+
if (recorded.kind === "adopt") return this.adoptEntry(entry, recorded.pid, "the child this panel started before it died is still running");
|
|
12586
|
+
}
|
|
12390
12587
|
await this.runBootstrap(entry);
|
|
12391
12588
|
if (entry.stopping) return {
|
|
12392
12589
|
ok: false,
|
|
@@ -12401,7 +12598,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
12401
12598
|
ok: false,
|
|
12402
12599
|
error: conflict.error
|
|
12403
12600
|
};
|
|
12404
|
-
if (conflict.kind === "adopt") return this.adoptEntry(entry, conflict.pid);
|
|
12601
|
+
if (conflict.kind === "adopt") return this.adoptEntry(entry, conflict.pid, `a detached restart of this entry is already serving port ${entry.config.port}`);
|
|
12405
12602
|
return this.spawnEntry(entry);
|
|
12406
12603
|
} finally {
|
|
12407
12604
|
entry.starting = false;
|
|
@@ -12412,9 +12609,9 @@ var init_supervisor = __esmMin((() => {
|
|
|
12412
12609
|
if (!entry) return missingServer(id);
|
|
12413
12610
|
return this.stopEntry(entry);
|
|
12414
12611
|
}
|
|
12415
|
-
async restart(id) {
|
|
12612
|
+
async restart(id, options = {}) {
|
|
12416
12613
|
await this.stop(id);
|
|
12417
|
-
return this.start(id);
|
|
12614
|
+
return this.start(id, options);
|
|
12418
12615
|
}
|
|
12419
12616
|
/**
|
|
12420
12617
|
* Frees the port this entry wants, by asking whatever listens on it to leave.
|
|
@@ -12646,6 +12843,10 @@ var init_supervisor = __esmMin((() => {
|
|
|
12646
12843
|
entry.pid = null;
|
|
12647
12844
|
entry.adopted = false;
|
|
12648
12845
|
entry.nannyExit = null;
|
|
12846
|
+
if (entry.lastChild !== null && !isProcessAlive(entry.lastChild.pid)) {
|
|
12847
|
+
entry.lastChild = null;
|
|
12848
|
+
this.persistEntryState(entry);
|
|
12849
|
+
}
|
|
12649
12850
|
entry.status = survivor === null ? "stopped" : "conflict";
|
|
12650
12851
|
if (survivor !== null) entry.lastError = `pid ${survivor} survived the stop`;
|
|
12651
12852
|
this.log(entry, "system", survivor === null ? "stopped" : entry.lastError);
|
|
@@ -12656,6 +12857,8 @@ var init_supervisor = __esmMin((() => {
|
|
|
12656
12857
|
};
|
|
12657
12858
|
}
|
|
12658
12859
|
createEntry(config) {
|
|
12860
|
+
const persisted = readEntryState(entryStatePath(this.options.nannyDir, config.id));
|
|
12861
|
+
const lastPid = persisted?.lastPid ?? null;
|
|
12659
12862
|
return {
|
|
12660
12863
|
config,
|
|
12661
12864
|
status: "stopped",
|
|
@@ -12664,9 +12867,13 @@ var init_supervisor = __esmMin((() => {
|
|
|
12664
12867
|
child: null,
|
|
12665
12868
|
pid: null,
|
|
12666
12869
|
startedAt: null,
|
|
12870
|
+
lastChild: !config.persistent && lastPid !== null ? {
|
|
12871
|
+
pid: lastPid,
|
|
12872
|
+
startedAt: persisted?.lastStartedAt ?? 0
|
|
12873
|
+
} : null,
|
|
12667
12874
|
exitCode: null,
|
|
12668
12875
|
exitSignal: null,
|
|
12669
|
-
restarts: 0,
|
|
12876
|
+
restarts: persisted?.restarts ?? 0,
|
|
12670
12877
|
lastError: null,
|
|
12671
12878
|
nextRetryAt: null,
|
|
12672
12879
|
retryTimer: null,
|
|
@@ -12679,8 +12886,9 @@ var init_supervisor = __esmMin((() => {
|
|
|
12679
12886
|
nannyExit: null,
|
|
12680
12887
|
starting: false,
|
|
12681
12888
|
stopping: false,
|
|
12889
|
+
actor: "policy",
|
|
12682
12890
|
removed: false,
|
|
12683
|
-
bootstrapDone: !config.bootstrap,
|
|
12891
|
+
bootstrapDone: persisted?.bootstrapDone ?? !config.bootstrap,
|
|
12684
12892
|
logs: new LogBuffer(config.logBufferLines),
|
|
12685
12893
|
echoed: /* @__PURE__ */ new Set(),
|
|
12686
12894
|
responseMs: null,
|
|
@@ -12698,6 +12906,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
12698
12906
|
this.relay.unfollow(id);
|
|
12699
12907
|
this.options.logFiles.clear(id);
|
|
12700
12908
|
this.options.history.forget(id);
|
|
12909
|
+
clearEntryState(entryStatePath(this.options.nannyDir, id));
|
|
12701
12910
|
entry.removed = true;
|
|
12702
12911
|
this.stopEntry(entry).catch((error) => {
|
|
12703
12912
|
logger.error(`could not stop the removed server ${id}`, error);
|
|
@@ -12821,16 +13030,6 @@ var init_supervisor = __esmMin((() => {
|
|
|
12821
13030
|
return { kind: "free" };
|
|
12822
13031
|
}
|
|
12823
13032
|
/**
|
|
12824
|
-
* Takes over a detached successor: no spawn, no duplicate. The pid is supervised
|
|
12825
|
-
* from here on (liveness, health probe, resources, stop), while its output stays
|
|
12826
|
-
* wherever it was redirected.
|
|
12827
|
-
*
|
|
12828
|
-
* For a persistent entry that output is not a mystery: its nanny is still writing
|
|
12829
|
-
* the entry's log file, so the relay is started here too. Without this, adopting a
|
|
12830
|
-
* persistent entry whose state file was lost would show its history but never
|
|
12831
|
-
* another live line.
|
|
12832
|
-
*/
|
|
12833
|
-
/**
|
|
12834
13033
|
* Stops the listed port holders and reports whether the port came free.
|
|
12835
13034
|
*
|
|
12836
13035
|
* `kill` and `reclaim` are the same sequence over a different pid list — the foreign
|
|
@@ -12855,7 +13054,21 @@ var init_supervisor = __esmMin((() => {
|
|
|
12855
13054
|
error: entry.lastError
|
|
12856
13055
|
};
|
|
12857
13056
|
}
|
|
12858
|
-
|
|
13057
|
+
/**
|
|
13058
|
+
* Takes over a detached successor: no spawn, no duplicate. The pid is supervised
|
|
13059
|
+
* from here on (liveness, health probe, resources, stop), while its output stays
|
|
13060
|
+
* wherever it was redirected.
|
|
13061
|
+
*
|
|
13062
|
+
* For a persistent entry that output is not a mystery: its nanny is still writing
|
|
13063
|
+
* the entry's log file, so the relay is started here too. Without this, adopting a
|
|
13064
|
+
* persistent entry whose state file was lost would show its history but never
|
|
13065
|
+
* another live line.
|
|
13066
|
+
*
|
|
13067
|
+
* `reason` is the caller's sentence for *why* this pid is ours: a port holder and a
|
|
13068
|
+
* recorded child that survived a hard panel death are the same adoption, said
|
|
13069
|
+
* differently, and the log line is the only place a person sees which happened.
|
|
13070
|
+
*/
|
|
13071
|
+
adoptEntry(entry, pid, reason) {
|
|
12859
13072
|
entry.adopted = true;
|
|
12860
13073
|
entry.pid = pid;
|
|
12861
13074
|
entry.child = null;
|
|
@@ -12864,12 +13077,60 @@ var init_supervisor = __esmMin((() => {
|
|
|
12864
13077
|
entry.startedAt = Date.now();
|
|
12865
13078
|
entry.lastError = null;
|
|
12866
13079
|
if (entry.config.persistent) this.followPersistent(entry, true);
|
|
12867
|
-
this.log(entry, "system", `adopted pid ${pid}:
|
|
13080
|
+
this.log(entry, "system", `adopted pid ${pid}: ${reason}`);
|
|
12868
13081
|
this.publishServer(entry);
|
|
12869
13082
|
return { ok: true };
|
|
12870
13083
|
}
|
|
13084
|
+
/**
|
|
13085
|
+
* The child this entry's state file recorded, when it is still that same process.
|
|
13086
|
+
*
|
|
13087
|
+
* A non-persistent child is spawned detached, so it survives a panel killed hard while
|
|
13088
|
+
* nothing else remembers it — `preflight` cannot help a portless entry, which is how a
|
|
13089
|
+
* portless worker got a second copy beside the running one. A pid is recycled, so
|
|
13090
|
+
* liveness alone proves nothing: the birth time and the marker decide, and a pid this
|
|
13091
|
+
* platform cannot place is never adopted on a guess.
|
|
13092
|
+
*
|
|
13093
|
+
* The record is cleared only once its process is known to be gone or to be somebody
|
|
13094
|
+
* else, so a stale pid is never chased and a live one is never forgotten into a twin.
|
|
13095
|
+
*/
|
|
13096
|
+
async resolveRecordedChild(entry) {
|
|
13097
|
+
const recorded = entry.lastChild;
|
|
13098
|
+
if (recorded === null) return { kind: "none" };
|
|
13099
|
+
if (recorded.pid === process.pid) {
|
|
13100
|
+
entry.lastChild = null;
|
|
13101
|
+
this.persistEntryState(entry);
|
|
13102
|
+
return { kind: "none" };
|
|
13103
|
+
}
|
|
13104
|
+
const liveness = recorded.startedAt > 0 ? await processLiveness(recorded.pid, recorded.startedAt) : isProcessAlive(recorded.pid) ? "unknown" : "gone";
|
|
13105
|
+
if (liveness === "gone" || liveness === "recycled") {
|
|
13106
|
+
entry.lastChild = null;
|
|
13107
|
+
this.persistEntryState(entry);
|
|
13108
|
+
return { kind: "none" };
|
|
13109
|
+
}
|
|
13110
|
+
if (await processCarriesServerId(recorded.pid, entry.config.id)) return {
|
|
13111
|
+
kind: "adopt",
|
|
13112
|
+
pid: recorded.pid
|
|
13113
|
+
};
|
|
13114
|
+
if (entry.config.port !== null) return { kind: "none" };
|
|
13115
|
+
const error = `pid ${recorded.pid} may be a child this panel started for "${entry.config.id}" before it died, and this platform cannot confirm whose it is — not starting a second copy, stop pid ${recorded.pid} to clear it`;
|
|
13116
|
+
entry.status = "conflict";
|
|
13117
|
+
entry.lastError = error;
|
|
13118
|
+
this.log(entry, "system", error);
|
|
13119
|
+
this.publishServer(entry);
|
|
13120
|
+
return {
|
|
13121
|
+
kind: "blocked",
|
|
13122
|
+
error
|
|
13123
|
+
};
|
|
13124
|
+
}
|
|
13125
|
+
/** Clears the durable record of the last child, once that pid is known to be gone. */
|
|
13126
|
+
forgetRecordedChild(entry, pid) {
|
|
13127
|
+
if (pid === null || entry.lastChild === null || entry.lastChild.pid !== pid) return;
|
|
13128
|
+
entry.lastChild = null;
|
|
13129
|
+
this.persistEntryState(entry);
|
|
13130
|
+
}
|
|
12871
13131
|
/** An adopted successor disappeared: fall back to the normal lifecycle. */
|
|
12872
13132
|
handleAdoptedExit(entry) {
|
|
13133
|
+
const adoptedPid = entry.pid;
|
|
12873
13134
|
if (entry.pid !== null) this.sampler.forget(entry.pid);
|
|
12874
13135
|
if (entry.config.persistent) this.consumeNannyExit(entry);
|
|
12875
13136
|
const ranForMs = entry.nannyExit?.runtimeMs ?? (entry.startedAt === null ? 0 : Date.now() - entry.startedAt);
|
|
@@ -12877,6 +13138,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
12877
13138
|
entry.nannyExit = null;
|
|
12878
13139
|
entry.adopted = false;
|
|
12879
13140
|
entry.pid = null;
|
|
13141
|
+
this.forgetRecordedChild(entry, adoptedPid);
|
|
12880
13142
|
entry.resources = null;
|
|
12881
13143
|
entry.responseMs = null;
|
|
12882
13144
|
entry.exitCode = null;
|
|
@@ -12917,6 +13179,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
12917
13179
|
} catch (error) {
|
|
12918
13180
|
this.log(entry, "system", `bootstrap could not start: ${error.message} — continuing anyway`);
|
|
12919
13181
|
entry.bootstrapDone = true;
|
|
13182
|
+
this.persistEntryState(entry);
|
|
12920
13183
|
return;
|
|
12921
13184
|
}
|
|
12922
13185
|
child.stdout?.on("data", (chunk) => splitter.push("stdout", chunk));
|
|
@@ -12939,6 +13202,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
12939
13202
|
});
|
|
12940
13203
|
});
|
|
12941
13204
|
entry.bootstrapDone = true;
|
|
13205
|
+
this.persistEntryState(entry);
|
|
12942
13206
|
if (code === 0) this.log(entry, "system", "bootstrap finished");
|
|
12943
13207
|
else if (code !== null) this.log(entry, "system", `bootstrap exited with code ${code} — continuing anyway`);
|
|
12944
13208
|
}
|
|
@@ -13080,9 +13344,17 @@ var init_supervisor = __esmMin((() => {
|
|
|
13080
13344
|
entry.child = child;
|
|
13081
13345
|
entry.pid = child.pid ?? null;
|
|
13082
13346
|
entry.startedAt = Date.now();
|
|
13347
|
+
if (!entry.config.persistent && child.pid !== void 0) {
|
|
13348
|
+
entry.lastChild = {
|
|
13349
|
+
pid: child.pid,
|
|
13350
|
+
startedAt: entry.startedAt
|
|
13351
|
+
};
|
|
13352
|
+
this.persistEntryState(entry);
|
|
13353
|
+
}
|
|
13083
13354
|
this.options.history.record(entry.config.id, {
|
|
13084
13355
|
type: "start",
|
|
13085
|
-
detail
|
|
13356
|
+
detail,
|
|
13357
|
+
actor: entry.actor
|
|
13086
13358
|
});
|
|
13087
13359
|
entry.exitCode = null;
|
|
13088
13360
|
entry.exitSignal = null;
|
|
@@ -13272,6 +13544,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
13272
13544
|
entry.responseMs = null;
|
|
13273
13545
|
entry.exitCode = code;
|
|
13274
13546
|
entry.exitSignal = signal;
|
|
13547
|
+
this.forgetRecordedChild(entry, child.pid ?? null);
|
|
13275
13548
|
const neverStarted = nannyExit !== null ? nannyExit.code === null && nannyExit.signal === null : code === null && signal === null && entry.lastError !== null;
|
|
13276
13549
|
const detail = neverStarted ? entry.lastError ?? "the entry could not start" : nannyExit !== null ? describeNannyExit(nannyExit) : describeNannyExit({
|
|
13277
13550
|
code,
|
|
@@ -13300,10 +13573,12 @@ var init_supervisor = __esmMin((() => {
|
|
|
13300
13573
|
}
|
|
13301
13574
|
const restart = entry.config.restart;
|
|
13302
13575
|
if (ranForMs >= restart.resetAfterMs) entry.restarts = 0;
|
|
13576
|
+
this.persistEntryState(entry);
|
|
13303
13577
|
this.log(entry, "system", neverStarted ? `did not start: ${detail}` : `exited with ${detail} after ${ranFor}`);
|
|
13304
13578
|
entry.lastError = neverStarted ? detail : `exited with ${detail}`;
|
|
13305
13579
|
if (restart.enabled && entry.restarts < restart.maxRetries) {
|
|
13306
13580
|
entry.restarts += 1;
|
|
13581
|
+
this.persistEntryState(entry);
|
|
13307
13582
|
const backoffMs = computeBackoff(entry.restarts, restart);
|
|
13308
13583
|
entry.status = "backoff";
|
|
13309
13584
|
entry.nextRetryAt = Date.now() + backoffMs;
|
|
@@ -13439,6 +13714,15 @@ var init_supervisor = __esmMin((() => {
|
|
|
13439
13714
|
entry.retryTimer = null;
|
|
13440
13715
|
}
|
|
13441
13716
|
}
|
|
13717
|
+
/** The decisions that have to survive this panel, and nothing else. */
|
|
13718
|
+
persistEntryState(entry) {
|
|
13719
|
+
writeEntryState(entryStatePath(this.options.nannyDir, entry.config.id), {
|
|
13720
|
+
restarts: entry.restarts,
|
|
13721
|
+
bootstrapDone: entry.bootstrapDone,
|
|
13722
|
+
lastPid: entry.config.persistent ? null : entry.lastChild?.pid ?? null,
|
|
13723
|
+
lastStartedAt: entry.config.persistent ? null : entry.lastChild?.startedAt ?? null
|
|
13724
|
+
});
|
|
13725
|
+
}
|
|
13442
13726
|
buildVars(entry) {
|
|
13443
13727
|
return serverTemplateVars(entry.config);
|
|
13444
13728
|
}
|
|
@@ -13666,11 +13950,15 @@ function resolveBackupPaths(servers, includePaths = [], vars) {
|
|
|
13666
13950
|
ignoreGenerated: entry.ignoreGenerated
|
|
13667
13951
|
};
|
|
13668
13952
|
const note = parent.path === entry.path ? `already declared by ${parent.origin}` : `covered by ${parent.path}`;
|
|
13953
|
+
const noteKey = parent.path === entry.path ? "settings.backup.noteAlreadyDeclared" : "settings.backup.noteCovered";
|
|
13954
|
+
const noteParams = parent.path === entry.path ? { origin: parent.origin } : { path: parent.path };
|
|
13669
13955
|
return {
|
|
13670
13956
|
path: entry.path,
|
|
13671
13957
|
origin: entry.origin,
|
|
13672
13958
|
included: false,
|
|
13673
13959
|
note,
|
|
13960
|
+
noteKey,
|
|
13961
|
+
noteParams,
|
|
13674
13962
|
ignoreGenerated: entry.ignoreGenerated
|
|
13675
13963
|
};
|
|
13676
13964
|
});
|
|
@@ -13718,6 +14006,28 @@ function labelOf(plan, id) {
|
|
|
13718
14006
|
return plan.items.find((item) => item.id === id)?.label ?? id;
|
|
13719
14007
|
}
|
|
13720
14008
|
/**
|
|
14009
|
+
* Restores only `fields` from an archived secrets document into a live one.
|
|
14010
|
+
*
|
|
14011
|
+
* The archive may predate a secret the live file holds, and the live file may hold one
|
|
14012
|
+
* the archive never carried: merging by field is what makes restoring the Telegram token
|
|
14013
|
+
* leave the DDNS credentials alone, and what clears a secret the archive does not carry
|
|
14014
|
+
* rather than leaving the newer value behind as if it had been restored.
|
|
14015
|
+
*/
|
|
14016
|
+
function mergeIntoFile(target, archived, fields) {
|
|
14017
|
+
const merged = mergeSecrets(parseSecrets(readText(target) ?? ""), parseSecrets(archived), fields);
|
|
14018
|
+
writeFileAtomic(target, `${JSON.stringify(merged, null, 2)}\n`, { mode: 384 });
|
|
14019
|
+
}
|
|
14020
|
+
/**
|
|
14021
|
+
* What an archive says about where it came from. Both fields are absent on an archive
|
|
14022
|
+
* written before they existed, so a UI shows the row only when there is something to show.
|
|
14023
|
+
*/
|
|
14024
|
+
function manifestInfo(manifest) {
|
|
14025
|
+
return {
|
|
14026
|
+
...manifest.writtenBy === void 0 ? {} : { writtenBy: manifest.writtenBy },
|
|
14027
|
+
...manifest.schema === void 0 ? {} : { schema: manifest.schema }
|
|
14028
|
+
};
|
|
14029
|
+
}
|
|
14030
|
+
/**
|
|
13721
14031
|
* `servers.config.json` is only a readability gate here: the store applies the
|
|
13722
14032
|
* workspace's own defaults on the next load, and an archive from another machine
|
|
13723
14033
|
* need not match this one's default shapes exactly.
|
|
@@ -13738,13 +14048,16 @@ function readableWorkspaceSettings(text) {
|
|
|
13738
14048
|
return false;
|
|
13739
14049
|
}
|
|
13740
14050
|
}
|
|
13741
|
-
var MANIFEST, ALLOWED_ROOTS, SUFFIX, RELOADABLE, BackupService;
|
|
14051
|
+
var MANIFEST, ALLOWED_ROOTS, SUFFIX, RELOADABLE, SECRET_LABELS, BackupService;
|
|
13742
14052
|
var init_backups = __esmMin((() => {
|
|
14053
|
+
init_migrations();
|
|
13743
14054
|
init_parse();
|
|
14055
|
+
init_secret_parts();
|
|
13744
14056
|
init_atomic();
|
|
13745
14057
|
init_env_file();
|
|
13746
14058
|
init_paths();
|
|
13747
14059
|
init_template();
|
|
14060
|
+
init_version();
|
|
13748
14061
|
init_archive();
|
|
13749
14062
|
init_supervisor();
|
|
13750
14063
|
init_error_message();
|
|
@@ -13758,6 +14071,24 @@ var init_backups = __esmMin((() => {
|
|
|
13758
14071
|
]);
|
|
13759
14072
|
SUFFIX = ".zip";
|
|
13760
14073
|
RELOADABLE = /^global:settings$|^workspace:[^:]+:(?:settings|servers)$/;
|
|
14074
|
+
SECRET_LABELS = {
|
|
14075
|
+
password: {
|
|
14076
|
+
en: "Password",
|
|
14077
|
+
key: "settings.backup.secretPassword"
|
|
14078
|
+
},
|
|
14079
|
+
apiToken: {
|
|
14080
|
+
en: "API token",
|
|
14081
|
+
key: "settings.backup.secretApiToken"
|
|
14082
|
+
},
|
|
14083
|
+
telegram: {
|
|
14084
|
+
en: "Telegram bot token",
|
|
14085
|
+
key: "settings.backup.secretTelegram"
|
|
14086
|
+
},
|
|
14087
|
+
ddns: {
|
|
14088
|
+
en: "DDNS credentials",
|
|
14089
|
+
key: "settings.backup.secretDdns"
|
|
14090
|
+
}
|
|
14091
|
+
};
|
|
13761
14092
|
BackupService = class {
|
|
13762
14093
|
options;
|
|
13763
14094
|
/**
|
|
@@ -13820,6 +14151,16 @@ var init_backups = __esmMin((() => {
|
|
|
13820
14151
|
const wanted = options.include === void 0 ? null : new Set(options.include);
|
|
13821
14152
|
const capturesGlobal = (id) => wanted === null || wanted.has(id);
|
|
13822
14153
|
const capturesWorkspace = (id, suffix) => wanted === null || wanted.has(`workspace:${id}`) || wanted.has(`workspace:${id}:${suffix}`);
|
|
14154
|
+
/**
|
|
14155
|
+
* The secrets of one scope the request asked for; `null` means "every one of them".
|
|
14156
|
+
* `workspaceId` is how a bare `workspace:<id>` selection covers all of its leaves.
|
|
14157
|
+
*/
|
|
14158
|
+
const selectedSecretFields = (entryId, path, scope, workspaceId) => {
|
|
14159
|
+
const parts = secretParts(scope, presentSecretKeys(parseSecrets(readText(path) ?? "")));
|
|
14160
|
+
const coversEntry = wanted === null || wanted.has(entryId) || workspaceId !== void 0 && wanted.has(`workspace:${workspaceId}`);
|
|
14161
|
+
const picked = parts.filter((part) => coversEntry || wanted.has(`${entryId}:${part.key}`) || part.fields.some((field) => wanted.has(`${entryId}:${field}`)));
|
|
14162
|
+
return picked.length === parts.length ? null : picked.flatMap((part) => part.fields);
|
|
14163
|
+
};
|
|
13823
14164
|
const dir = this.resolveDir();
|
|
13824
14165
|
const sources = this.options.getSources();
|
|
13825
14166
|
const staging = path.join(dir, `.staging-${Date.now()}`);
|
|
@@ -13832,15 +14173,41 @@ var init_backups = __esmMin((() => {
|
|
|
13832
14173
|
const copy = (relative, source, ignoreGenerated = false) => {
|
|
13833
14174
|
if (this.copyInto(staging, relative, source, ignoreGenerated)) captured++;
|
|
13834
14175
|
};
|
|
14176
|
+
/**
|
|
14177
|
+
* Captures one secrets file, narrowed to the picked secrets. Copying the whole file
|
|
14178
|
+
* would capture the ones that were *not* picked, so a per-secret selection writes a
|
|
14179
|
+
* staged document holding only those — and the restore side merges by field, which
|
|
14180
|
+
* is what keeps a restore from wiping a secret the archive never carried.
|
|
14181
|
+
*/
|
|
14182
|
+
const copySecrets = (relative, source, entryId, scope, workspaceId) => {
|
|
14183
|
+
const text = readText(source);
|
|
14184
|
+
if (text === null) return;
|
|
14185
|
+
const fields = selectedSecretFields(entryId, source, scope, workspaceId);
|
|
14186
|
+
if (fields === null) {
|
|
14187
|
+
copy(relative, source);
|
|
14188
|
+
return;
|
|
14189
|
+
}
|
|
14190
|
+
const chosen = pickSecrets(parseSecrets(text), fields);
|
|
14191
|
+
if (chosen === null) return;
|
|
14192
|
+
const target = path.join(staging, relative);
|
|
14193
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
14194
|
+
fs.writeFileSync(target, `${chosen}\n`);
|
|
14195
|
+
captured++;
|
|
14196
|
+
};
|
|
14197
|
+
/**
|
|
14198
|
+
* True when the request touches a secrets entry at all: the entry itself, one of its
|
|
14199
|
+
* leaves, or — for a workspace — the workspace id, which names every leaf it has.
|
|
14200
|
+
*/
|
|
14201
|
+
const capturesSecrets = (entryId, workspaceId) => wanted === null || wanted.has(entryId) || workspaceId !== void 0 && wanted.has(`workspace:${workspaceId}`) || [...wanted].some((id) => id.startsWith(`${entryId}:`));
|
|
13835
14202
|
if (capturesGlobal("global:settings")) copy("global/settings.json", sources.globalSettingsPath);
|
|
13836
|
-
if (
|
|
14203
|
+
if (capturesSecrets("global:secrets")) copySecrets("global/secrets.json", sources.globalSecretsPath, "global:secrets", "global");
|
|
13837
14204
|
if (capturesGlobal("global:tls")) copy("global/tls", sources.tlsDir);
|
|
13838
14205
|
const data = [];
|
|
13839
14206
|
for (const workspace of sources.workspaces) {
|
|
13840
14207
|
const root = `workspaces/${workspace.id}`;
|
|
13841
14208
|
if (capturesWorkspace(workspace.id, "settings")) copy(`${root}/settings.json`, workspace.settingsPath);
|
|
13842
14209
|
if (capturesWorkspace(workspace.id, "servers")) copy(`${root}/servers.config.json`, workspace.serversPath);
|
|
13843
|
-
if (
|
|
14210
|
+
if (capturesSecrets(`workspace:${workspace.id}:secrets`, workspace.id)) copySecrets(`${root}/secrets.json`, workspace.secretsPath, `workspace:${workspace.id}:secrets`, "workspace", workspace.id);
|
|
13844
14211
|
for (const declared of this.declaredData(workspace.servers, sources.includePaths)) {
|
|
13845
14212
|
if (!declared.included) continue;
|
|
13846
14213
|
if (!capturesWorkspace(workspace.id, `data:${declared.path}`)) continue;
|
|
@@ -13871,6 +14238,8 @@ var init_backups = __esmMin((() => {
|
|
|
13871
14238
|
version: 1,
|
|
13872
14239
|
createdAt,
|
|
13873
14240
|
hostname: os.hostname(),
|
|
14241
|
+
writtenBy: appVersion(),
|
|
14242
|
+
schema: 1,
|
|
13874
14243
|
data
|
|
13875
14244
|
};
|
|
13876
14245
|
fs.writeFileSync(path.join(staging, MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
@@ -13927,7 +14296,9 @@ var init_backups = __esmMin((() => {
|
|
|
13927
14296
|
needsPassword: false,
|
|
13928
14297
|
items: [],
|
|
13929
14298
|
applied: [],
|
|
14299
|
+
appliedIds: [],
|
|
13930
14300
|
skipped: [],
|
|
14301
|
+
skippedItems: [],
|
|
13931
14302
|
restartRequired: false,
|
|
13932
14303
|
reloaded: false
|
|
13933
14304
|
};
|
|
@@ -13970,13 +14341,24 @@ var init_backups = __esmMin((() => {
|
|
|
13970
14341
|
names: entries.map((entry) => entry.name),
|
|
13971
14342
|
...password === null ? {} : { password }
|
|
13972
14343
|
});
|
|
13973
|
-
for (const name of extracted.skipped)
|
|
14344
|
+
for (const name of extracted.skipped) {
|
|
14345
|
+
plan.skipped.push(`${name} (symbolic link, skipped)`);
|
|
14346
|
+
plan.skippedItems.push({
|
|
14347
|
+
label: name,
|
|
14348
|
+
reasonKey: "settings.restore.reasonSymbolicLink"
|
|
14349
|
+
});
|
|
14350
|
+
}
|
|
13974
14351
|
const manifestPath = path.join(staging, MANIFEST);
|
|
13975
14352
|
if (!fs.existsSync(manifestPath)) return {
|
|
13976
14353
|
...plan,
|
|
13977
14354
|
error: "the archive has no manifest"
|
|
13978
14355
|
};
|
|
13979
14356
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
14357
|
+
plan.archive = manifestInfo(manifest);
|
|
14358
|
+
if (manifest.schema !== void 0 && manifest.schema > 1) return {
|
|
14359
|
+
...plan,
|
|
14360
|
+
error: `written by home-hosted ${manifest.writtenBy ?? "a newer release"} (config schema ${manifest.schema}); this release understands schema 1`
|
|
14361
|
+
};
|
|
13980
14362
|
const sources = this.options.getSources();
|
|
13981
14363
|
const selectedIds = options.include === void 0 ? null : new Set(options.include);
|
|
13982
14364
|
const actions = /* @__PURE__ */ new Map();
|
|
@@ -13987,6 +14369,13 @@ var init_backups = __esmMin((() => {
|
|
|
13987
14369
|
selected: false
|
|
13988
14370
|
});
|
|
13989
14371
|
plan.skipped.push(`${item.label}${item.note === null ? "" : ` (${item.note})`}`);
|
|
14372
|
+
plan.skippedItems.push({
|
|
14373
|
+
label: item.label,
|
|
14374
|
+
...item.labelKey === void 0 ? {} : { labelKey: item.labelKey },
|
|
14375
|
+
...item.labelParams === void 0 ? {} : { labelParams: item.labelParams },
|
|
14376
|
+
reasonKey: item.noteKey ?? "settings.restore.reasonUnrestorable",
|
|
14377
|
+
...item.noteParams === void 0 ? {} : { reasonParams: item.noteParams }
|
|
14378
|
+
});
|
|
13990
14379
|
return;
|
|
13991
14380
|
}
|
|
13992
14381
|
const selected = selectedIds === null || selectedIds.has(item.id) || aliases.some((alias) => selectedIds.has(alias));
|
|
@@ -13995,14 +14384,30 @@ var init_backups = __esmMin((() => {
|
|
|
13995
14384
|
selected
|
|
13996
14385
|
});
|
|
13997
14386
|
if (selected) actions.set(item.id, apply);
|
|
13998
|
-
else
|
|
14387
|
+
else {
|
|
14388
|
+
plan.skipped.push(`${item.label} (not selected)`);
|
|
14389
|
+
plan.skippedItems.push({
|
|
14390
|
+
label: item.label,
|
|
14391
|
+
...item.labelKey === void 0 ? {} : { labelKey: item.labelKey },
|
|
14392
|
+
...item.labelParams === void 0 ? {} : { labelParams: item.labelParams },
|
|
14393
|
+
reasonKey: "settings.restore.reasonNotSelected"
|
|
14394
|
+
});
|
|
14395
|
+
}
|
|
13999
14396
|
};
|
|
14000
14397
|
const settingsText = readText(path.join(staging, "global", "settings.json"));
|
|
14001
14398
|
const parsedGlobal = readableGlobalSettings(settingsText) ? parseGlobalSettings(JSON.parse(settingsText)).config : null;
|
|
14002
|
-
if (settingsText !== null && parsedGlobal === null)
|
|
14399
|
+
if (settingsText !== null && parsedGlobal === null) {
|
|
14400
|
+
plan.skipped.push("Global settings (the archive's settings are not valid)");
|
|
14401
|
+
plan.skippedItems.push({
|
|
14402
|
+
label: "Global settings",
|
|
14403
|
+
labelKey: "settings.backup.entryGlobalSettings",
|
|
14404
|
+
reasonKey: "settings.restore.reasonInvalidSettings"
|
|
14405
|
+
});
|
|
14406
|
+
}
|
|
14003
14407
|
if (parsedGlobal !== null && settingsText !== null) addItem({
|
|
14004
14408
|
id: "global:settings",
|
|
14005
14409
|
label: "Global settings",
|
|
14410
|
+
labelKey: "settings.backup.entryGlobalSettings",
|
|
14006
14411
|
kind: "settings",
|
|
14007
14412
|
restorable: true,
|
|
14008
14413
|
note: null
|
|
@@ -14010,21 +14415,39 @@ var init_backups = __esmMin((() => {
|
|
|
14010
14415
|
writeFileAtomic(sources.globalSettingsPath, settingsText);
|
|
14011
14416
|
}, ["config"]);
|
|
14012
14417
|
const globalSecrets = readText(path.join(staging, "global", "secrets.json"));
|
|
14013
|
-
if (globalSecrets !== null)
|
|
14014
|
-
|
|
14015
|
-
|
|
14016
|
-
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14418
|
+
if (globalSecrets !== null) {
|
|
14419
|
+
addItem({
|
|
14420
|
+
id: "global:secrets",
|
|
14421
|
+
label: "Global secrets (password, API token)",
|
|
14422
|
+
labelKey: "settings.backup.entryGlobalSecrets",
|
|
14423
|
+
kind: "secrets",
|
|
14424
|
+
restorable: true,
|
|
14425
|
+
note: null
|
|
14426
|
+
}, () => {
|
|
14427
|
+
writeFileAtomic(sources.globalSecretsPath, globalSecrets, { mode: 384 });
|
|
14428
|
+
}, ["secrets"]);
|
|
14429
|
+
for (const part of secretParts("global", presentSecretKeys(parseSecrets(globalSecrets)))) addItem({
|
|
14430
|
+
id: `global:secrets:${part.key}`,
|
|
14431
|
+
label: `Global secrets: ${SECRET_LABELS[part.key]?.en ?? part.key}`,
|
|
14432
|
+
labelKey: "settings.backup.restoreItemLabel",
|
|
14433
|
+
labelParams: {
|
|
14434
|
+
workspace: "global",
|
|
14435
|
+
item: part.key
|
|
14436
|
+
},
|
|
14437
|
+
kind: "secrets",
|
|
14438
|
+
restorable: true,
|
|
14439
|
+
note: null
|
|
14440
|
+
}, () => {
|
|
14441
|
+
mergeIntoFile(sources.globalSecretsPath, globalSecrets, part.fields);
|
|
14442
|
+
});
|
|
14443
|
+
}
|
|
14022
14444
|
const tlsInArchive = path.join(staging, "global", "tls");
|
|
14023
14445
|
if (fs.existsSync(tlsInArchive)) {
|
|
14024
14446
|
const files = fs.readdirSync(tlsInArchive).filter((file) => fs.statSync(path.join(tlsInArchive, file)).isFile());
|
|
14025
14447
|
addItem({
|
|
14026
14448
|
id: "global:tls",
|
|
14027
14449
|
label: "TLS certificate",
|
|
14450
|
+
labelKey: "settings.backup.entryTlsCertificate",
|
|
14028
14451
|
kind: "tls",
|
|
14029
14452
|
restorable: true,
|
|
14030
14453
|
note: null
|
|
@@ -14070,10 +14493,16 @@ var init_backups = __esmMin((() => {
|
|
|
14070
14493
|
addItem({
|
|
14071
14494
|
id: `workspace:${id}:${suffix}`,
|
|
14072
14495
|
label: `${id}: ${label}`,
|
|
14496
|
+
labelKey: "settings.backup.restoreItemLabel",
|
|
14497
|
+
labelParams: {
|
|
14498
|
+
workspace: id,
|
|
14499
|
+
item: suffix
|
|
14500
|
+
},
|
|
14073
14501
|
kind: suffix,
|
|
14074
14502
|
workspaceId: id,
|
|
14075
14503
|
restorable: false,
|
|
14076
|
-
note: "unknown workspace"
|
|
14504
|
+
note: "unknown workspace",
|
|
14505
|
+
noteKey: "settings.backup.noteUnknownWorkspace"
|
|
14077
14506
|
}, null);
|
|
14078
14507
|
}
|
|
14079
14508
|
continue;
|
|
@@ -14082,6 +14511,11 @@ var init_backups = __esmMin((() => {
|
|
|
14082
14511
|
if (readableWorkspaceSettings(settings)) addItem({
|
|
14083
14512
|
id: `workspace:${id}:settings`,
|
|
14084
14513
|
label: `${workspace.label}: Settings`,
|
|
14514
|
+
labelKey: "settings.backup.restoreItemLabel",
|
|
14515
|
+
labelParams: {
|
|
14516
|
+
workspace: workspace.label,
|
|
14517
|
+
item: "settings"
|
|
14518
|
+
},
|
|
14085
14519
|
kind: "settings",
|
|
14086
14520
|
workspaceId: id,
|
|
14087
14521
|
restorable: true,
|
|
@@ -14089,7 +14523,18 @@ var init_backups = __esmMin((() => {
|
|
|
14089
14523
|
}, () => {
|
|
14090
14524
|
writeFileAtomic(workspace.settingsPath, settings);
|
|
14091
14525
|
});
|
|
14092
|
-
else
|
|
14526
|
+
else {
|
|
14527
|
+
plan.skipped.push(`${workspace.label}: Settings (the archive's settings are not valid)`);
|
|
14528
|
+
plan.skippedItems.push({
|
|
14529
|
+
label: `${workspace.label}: Settings`,
|
|
14530
|
+
labelKey: "settings.backup.restoreItemLabel",
|
|
14531
|
+
labelParams: {
|
|
14532
|
+
workspace: workspace.label,
|
|
14533
|
+
item: "settings"
|
|
14534
|
+
},
|
|
14535
|
+
reasonKey: "settings.restore.reasonInvalidSettings"
|
|
14536
|
+
});
|
|
14537
|
+
}
|
|
14093
14538
|
}
|
|
14094
14539
|
if (servers !== null) {
|
|
14095
14540
|
const parsed = readableServersFile(servers);
|
|
@@ -14098,6 +14543,11 @@ var init_backups = __esmMin((() => {
|
|
|
14098
14543
|
addItem({
|
|
14099
14544
|
id: `workspace:${id}:servers`,
|
|
14100
14545
|
label: `${workspace.label}: Servers`,
|
|
14546
|
+
labelKey: "settings.backup.restoreItemLabel",
|
|
14547
|
+
labelParams: {
|
|
14548
|
+
workspace: workspace.label,
|
|
14549
|
+
item: "servers"
|
|
14550
|
+
},
|
|
14101
14551
|
kind: "servers",
|
|
14102
14552
|
workspaceId: id,
|
|
14103
14553
|
restorable: true,
|
|
@@ -14105,18 +14555,51 @@ var init_backups = __esmMin((() => {
|
|
|
14105
14555
|
}, () => {
|
|
14106
14556
|
writeFileAtomic(workspace.serversPath, servers);
|
|
14107
14557
|
});
|
|
14108
|
-
} else
|
|
14558
|
+
} else {
|
|
14559
|
+
plan.skipped.push(`${workspace.label}: Servers (the archive's servers are not valid)`);
|
|
14560
|
+
plan.skippedItems.push({
|
|
14561
|
+
label: `${workspace.label}: Servers`,
|
|
14562
|
+
labelKey: "settings.backup.restoreItemLabel",
|
|
14563
|
+
labelParams: {
|
|
14564
|
+
workspace: workspace.label,
|
|
14565
|
+
item: "servers"
|
|
14566
|
+
},
|
|
14567
|
+
reasonKey: "settings.restore.reasonInvalidServers"
|
|
14568
|
+
});
|
|
14569
|
+
}
|
|
14570
|
+
}
|
|
14571
|
+
if (secrets !== null) {
|
|
14572
|
+
addItem({
|
|
14573
|
+
id: `workspace:${id}:secrets`,
|
|
14574
|
+
label: `${workspace.label}: Secrets`,
|
|
14575
|
+
labelKey: "settings.backup.restoreItemLabel",
|
|
14576
|
+
labelParams: {
|
|
14577
|
+
workspace: workspace.label,
|
|
14578
|
+
item: "secrets"
|
|
14579
|
+
},
|
|
14580
|
+
kind: "secrets",
|
|
14581
|
+
workspaceId: id,
|
|
14582
|
+
restorable: true,
|
|
14583
|
+
note: null
|
|
14584
|
+
}, () => {
|
|
14585
|
+
writeFileAtomic(workspace.secretsPath, secrets, { mode: 384 });
|
|
14586
|
+
});
|
|
14587
|
+
for (const part of secretParts("workspace", presentSecretKeys(parseSecrets(secrets)))) addItem({
|
|
14588
|
+
id: `workspace:${id}:secrets:${part.key}`,
|
|
14589
|
+
label: `${workspace.label}: ${SECRET_LABELS[part.key]?.en ?? part.key}`,
|
|
14590
|
+
labelKey: "settings.backup.restoreItemLabel",
|
|
14591
|
+
labelParams: {
|
|
14592
|
+
workspace: workspace.label,
|
|
14593
|
+
item: part.key
|
|
14594
|
+
},
|
|
14595
|
+
kind: "secrets",
|
|
14596
|
+
workspaceId: id,
|
|
14597
|
+
restorable: true,
|
|
14598
|
+
note: null
|
|
14599
|
+
}, () => {
|
|
14600
|
+
mergeIntoFile(workspace.secretsPath, secrets, part.fields);
|
|
14601
|
+
});
|
|
14109
14602
|
}
|
|
14110
|
-
if (secrets !== null) addItem({
|
|
14111
|
-
id: `workspace:${id}:secrets`,
|
|
14112
|
-
label: `${workspace.label}: Secrets`,
|
|
14113
|
-
kind: "secrets",
|
|
14114
|
-
workspaceId: id,
|
|
14115
|
-
restorable: true,
|
|
14116
|
-
note: null
|
|
14117
|
-
}, () => {
|
|
14118
|
-
writeFileAtomic(workspace.secretsPath, secrets, { mode: 384 });
|
|
14119
|
-
});
|
|
14120
14603
|
}
|
|
14121
14604
|
const instanceTargets = [];
|
|
14122
14605
|
for (const workspace of sources.workspaces) for (const declared of this.declaredData(workspace.servers, sources.includePaths)) {
|
|
@@ -14179,21 +14662,27 @@ var init_backups = __esmMin((() => {
|
|
|
14179
14662
|
const declaredByBackup = archiveAll.some((candidate) => candidate.origin === origin && (workspace === void 0 || candidate.workspace === workspace));
|
|
14180
14663
|
addItem({
|
|
14181
14664
|
...base,
|
|
14182
|
-
note: declaredByBackup ? "declared by the backup's config, which is not being restored" : "not declared by this config, nor by the backup"
|
|
14665
|
+
note: declaredByBackup ? "declared by the backup's config, which is not being restored" : "not declared by this config, nor by the backup",
|
|
14666
|
+
noteKey: declaredByBackup ? "settings.backup.noteDeclaredByBackup" : "settings.backup.noteNotDeclared"
|
|
14183
14667
|
}, null, aliases);
|
|
14184
14668
|
continue;
|
|
14185
14669
|
}
|
|
14186
14670
|
if (!fs.existsSync(from)) {
|
|
14187
14671
|
addItem({
|
|
14188
14672
|
...base,
|
|
14189
|
-
note: "missing from the archive"
|
|
14673
|
+
note: "missing from the archive",
|
|
14674
|
+
noteKey: "settings.backup.noteMissingFromArchive"
|
|
14190
14675
|
}, null, aliases);
|
|
14191
14676
|
continue;
|
|
14192
14677
|
}
|
|
14193
14678
|
addItem({
|
|
14194
14679
|
...base,
|
|
14195
14680
|
restorable: true,
|
|
14196
|
-
|
|
14681
|
+
...target.path === archivedPath ? { note: null } : {
|
|
14682
|
+
note: `restored from ${archivedPath}`,
|
|
14683
|
+
noteKey: "settings.backup.noteRestoredFrom",
|
|
14684
|
+
noteParams: { path: archivedPath }
|
|
14685
|
+
}
|
|
14197
14686
|
}, () => fs.cpSync(from, target.path, {
|
|
14198
14687
|
recursive: true,
|
|
14199
14688
|
force: true,
|
|
@@ -14207,12 +14696,14 @@ var init_backups = __esmMin((() => {
|
|
|
14207
14696
|
}
|
|
14208
14697
|
if (!options.confirm) {
|
|
14209
14698
|
plan.applied = [...actions.keys()].map((id) => labelOf(plan, id));
|
|
14699
|
+
plan.appliedIds = [...actions.keys()];
|
|
14210
14700
|
return plan;
|
|
14211
14701
|
}
|
|
14212
14702
|
for (const [id, apply] of actions) {
|
|
14213
14703
|
apply();
|
|
14214
14704
|
plan.applied.push(labelOf(plan, id));
|
|
14215
14705
|
}
|
|
14706
|
+
plan.appliedIds = [...actions.keys()];
|
|
14216
14707
|
if (this.options.onRestored !== void 0 && [...actions.keys()].some((id) => RELOADABLE.test(id))) {
|
|
14217
14708
|
plan.reloaded = true;
|
|
14218
14709
|
this.options.onRestored();
|
|
@@ -14239,26 +14730,36 @@ var init_backups = __esmMin((() => {
|
|
|
14239
14730
|
}
|
|
14240
14731
|
/** Selectable entries: the three global slices, then one per workspace. */
|
|
14241
14732
|
entries(sources) {
|
|
14733
|
+
const missing = (path, key) => {
|
|
14734
|
+
const absent = this.missingNote(path);
|
|
14735
|
+
return absent === null ? { note: null } : {
|
|
14736
|
+
note: absent,
|
|
14737
|
+
noteKey: key
|
|
14738
|
+
};
|
|
14739
|
+
};
|
|
14242
14740
|
const entries = [
|
|
14243
14741
|
{
|
|
14244
14742
|
id: "global:settings",
|
|
14245
14743
|
label: "Global settings",
|
|
14744
|
+
labelKey: "settings.backup.entryGlobalSettings",
|
|
14246
14745
|
kind: "settings",
|
|
14247
|
-
|
|
14746
|
+
...missing(sources.globalSettingsPath, "settings.backup.noteMissing"),
|
|
14248
14747
|
items: []
|
|
14249
14748
|
},
|
|
14250
14749
|
{
|
|
14251
14750
|
id: "global:secrets",
|
|
14252
14751
|
label: "Global secrets (password, API token)",
|
|
14752
|
+
labelKey: "settings.backup.entryGlobalSecrets",
|
|
14253
14753
|
kind: "secrets",
|
|
14254
|
-
|
|
14255
|
-
items:
|
|
14754
|
+
...missing(sources.globalSecretsPath, "settings.backup.noteMissing"),
|
|
14755
|
+
items: this.secretItems("global", "global:secrets", sources.globalSecretsPath)
|
|
14256
14756
|
},
|
|
14257
14757
|
{
|
|
14258
14758
|
id: "global:tls",
|
|
14259
14759
|
label: "TLS certificate",
|
|
14760
|
+
labelKey: "settings.backup.entryTlsCertificate",
|
|
14260
14761
|
kind: "tls",
|
|
14261
|
-
|
|
14762
|
+
...missing(sources.tlsDir, "settings.backup.noteMissing"),
|
|
14262
14763
|
items: []
|
|
14263
14764
|
}
|
|
14264
14765
|
];
|
|
@@ -14272,30 +14773,62 @@ var init_backups = __esmMin((() => {
|
|
|
14272
14773
|
});
|
|
14273
14774
|
return entries;
|
|
14274
14775
|
}
|
|
14776
|
+
/**
|
|
14777
|
+
* One leaf per secret the file actually holds, so a restore can bring back the
|
|
14778
|
+
* password without the API token, or the Telegram token without the DDNS credentials.
|
|
14779
|
+
*
|
|
14780
|
+
* The set comes from the file's own top-level keys rather than a list here, so a
|
|
14781
|
+
* secret added later appears on its own — the failure mode of a second list is a
|
|
14782
|
+
* secret that can be captured but never restored individually.
|
|
14783
|
+
*/
|
|
14784
|
+
secretItems(scope, entryId, path) {
|
|
14785
|
+
const present = presentSecretKeys(parseSecrets(readText(path) ?? ""));
|
|
14786
|
+
if (present.length === 0) return [];
|
|
14787
|
+
return secretParts(scope, present).map((part) => ({
|
|
14788
|
+
id: `${entryId}:${part.key}`,
|
|
14789
|
+
label: SECRET_LABELS[part.key]?.en ?? part.key,
|
|
14790
|
+
...SECRET_LABELS[part.key] === void 0 ? {} : { labelKey: SECRET_LABELS[part.key].key },
|
|
14791
|
+
kind: "secrets",
|
|
14792
|
+
secret: part.key,
|
|
14793
|
+
included: true,
|
|
14794
|
+
note: null
|
|
14795
|
+
}));
|
|
14796
|
+
}
|
|
14275
14797
|
workspaceItems(workspace, includePaths) {
|
|
14276
14798
|
const id = workspace.id;
|
|
14799
|
+
const note = (path) => {
|
|
14800
|
+
const absent = this.missingNote(path);
|
|
14801
|
+
return absent === null ? { note: null } : {
|
|
14802
|
+
note: absent,
|
|
14803
|
+
noteKey: "settings.backup.noteMissing"
|
|
14804
|
+
};
|
|
14805
|
+
};
|
|
14277
14806
|
const items = [
|
|
14278
14807
|
{
|
|
14279
14808
|
id: `workspace:${id}:settings`,
|
|
14280
14809
|
label: "Settings",
|
|
14810
|
+
labelKey: "settings.backup.leafSettings",
|
|
14281
14811
|
kind: "settings",
|
|
14282
14812
|
included: true,
|
|
14283
|
-
note
|
|
14813
|
+
...note(workspace.settingsPath)
|
|
14284
14814
|
},
|
|
14285
14815
|
{
|
|
14286
14816
|
id: `workspace:${id}:servers`,
|
|
14287
14817
|
label: "Servers",
|
|
14818
|
+
labelKey: "settings.backup.leafServers",
|
|
14288
14819
|
kind: "servers",
|
|
14289
14820
|
included: true,
|
|
14290
|
-
note
|
|
14821
|
+
...note(workspace.serversPath)
|
|
14291
14822
|
},
|
|
14292
14823
|
{
|
|
14293
14824
|
id: `workspace:${id}:secrets`,
|
|
14294
14825
|
label: "Secrets",
|
|
14826
|
+
labelKey: "settings.backup.leafSecrets",
|
|
14295
14827
|
kind: "secrets",
|
|
14296
14828
|
included: true,
|
|
14297
|
-
note
|
|
14298
|
-
}
|
|
14829
|
+
...note(workspace.secretsPath)
|
|
14830
|
+
},
|
|
14831
|
+
...this.secretItems("workspace", `workspace:${id}:secrets`, workspace.secretsPath)
|
|
14299
14832
|
];
|
|
14300
14833
|
for (const declared of this.declaredData(workspace.servers, includePaths)) items.push({
|
|
14301
14834
|
id: `workspace:${id}:data:${declared.path}`,
|
|
@@ -14305,6 +14838,8 @@ var init_backups = __esmMin((() => {
|
|
|
14305
14838
|
origin: declared.origin,
|
|
14306
14839
|
included: declared.included,
|
|
14307
14840
|
note: declared.note,
|
|
14841
|
+
...declared.noteKey === void 0 ? {} : { noteKey: declared.noteKey },
|
|
14842
|
+
...declared.noteParams === void 0 ? {} : { noteParams: declared.noteParams },
|
|
14308
14843
|
ignoreGenerated: declared.ignoreGenerated
|
|
14309
14844
|
});
|
|
14310
14845
|
return items;
|
|
@@ -14320,7 +14855,8 @@ var init_backups = __esmMin((() => {
|
|
|
14320
14855
|
if (entry.included && isInside(entry.path, archiveDir)) return {
|
|
14321
14856
|
...entry,
|
|
14322
14857
|
included: false,
|
|
14323
|
-
note: "contains the backup directory"
|
|
14858
|
+
note: "contains the backup directory",
|
|
14859
|
+
noteKey: "settings.backup.noteContainsBackupDir"
|
|
14324
14860
|
};
|
|
14325
14861
|
return entry;
|
|
14326
14862
|
});
|