@camstack/server 1.2.592 → 1.2.594
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/dist/addon-root-inventory.js +11 -50
- package/dist/agent/addon-scan.js +6 -44
- package/dist/agent/agent-addon-runtime.js +3 -8
- package/dist/agent/agent-addon-updates.js +12 -52
- package/dist/agent/agent-cap-dispatch-service.js +3 -5
- package/dist/agent/agent-config.js +7 -42
- package/dist/agent/agent-deploy-swap.js +5 -41
- package/dist/agent/agent-group-runner.js +8 -47
- package/dist/agent/agent-http-auth.js +10 -22
- package/dist/agent/agent-http.js +69 -115
- package/dist/agent/agent-hub-addon-update.js +10 -16
- package/dist/agent/agent-hub-registration.js +4 -7
- package/dist/agent/agent-infra-capabilities.js +5 -9
- package/dist/agent/agent-json-file.js +5 -42
- package/dist/agent/agent-mesh-gate.js +5 -8
- package/dist/agent/agent-node-certificates.js +25 -28
- package/dist/agent/agent-pairing-local.js +10 -47
- package/dist/agent/agent-pairing-routes.js +16 -20
- package/dist/agent/agent-pairing-store.js +18 -58
- package/dist/agent/agent-reconnect.js +1 -4
- package/dist/agent/agent-service.js +42 -78
- package/dist/agent/agent-sniffing-listener.js +11 -48
- package/dist/agent/agent-undeploy.js +5 -41
- package/dist/agent/agent-update-routes.js +14 -19
- package/dist/agent/agent-update-service.js +38 -80
- package/dist/agent/apply-model-distribution.js +1 -4
- package/dist/agent/apply-stage-dev-uploads.js +6 -9
- package/dist/agent/derive-hub-url.js +3 -8
- package/dist/agent/fetch-bundle-from-hub.js +6 -9
- package/dist/agent/hubs-found.js +6 -43
- package/dist/agent/infra-boot-guard.js +1 -4
- package/dist/agent/infra-boot-plan.js +2 -6
- package/dist/agent/main.js +158 -186
- package/dist/agent/register-agent-cap-dispatch.js +3 -6
- package/dist/agent/serial-queue.js +2 -7
- package/dist/agent-status-page.js +1 -4
- package/dist/api/addon-upload.js +22 -61
- package/dist/api/agent-addon-delivery.js +6 -11
- package/dist/api/auth-whoami.js +1 -4
- package/dist/api/backup-download-pump.js +5 -9
- package/dist/api/backup-download.js +7 -10
- package/dist/api/core/addon-settings.router.js +16 -19
- package/dist/api/core/agent-pairing.router.js +76 -79
- package/dist/api/core/agents.router.js +21 -24
- package/dist/api/core/auth-admin-tokens.procedures.js +48 -52
- package/dist/api/core/auth-device-cert.procedures.js +62 -67
- package/dist/api/core/auth-limits.js +16 -25
- package/dist/api/core/auth-reauth.procedures.js +32 -37
- package/dist/api/core/auth-revoke-credentials.procedures.js +22 -26
- package/dist/api/core/auth-sessions.procedures.js +52 -56
- package/dist/api/core/auth.router.js +300 -304
- package/dist/api/core/cap-providers.js +74 -121
- package/dist/api/core/capabilities.router.js +34 -37
- package/dist/api/core/capability-preference-writes.js +10 -17
- package/dist/api/core/certificates.router.js +106 -109
- package/dist/api/core/cluster-nodes.router.js +15 -18
- package/dist/api/core/collection-preference.js +2 -6
- package/dist/api/core/event-bus-proxy.router.js +34 -37
- package/dist/api/core/failure-contributions.js +4 -8
- package/dist/api/core/hwaccel.router.js +17 -20
- package/dist/api/core/integration-secrets.js +8 -12
- package/dist/api/core/lifecycle-job-runner.js +13 -49
- package/dist/api/core/live-events.router.js +29 -32
- package/dist/api/core/load-contributions.js +4 -8
- package/dist/api/core/logging-settings.js +30 -43
- package/dist/api/core/logs.router.js +40 -43
- package/dist/api/core/notifications.router.js +15 -18
- package/dist/api/core/public-event-filter.js +4 -8
- package/dist/api/core/repl.router.js +18 -21
- package/dist/api/core/request-census-settings.js +11 -16
- package/dist/api/core/server-management.provider.js +1 -4
- package/dist/api/core/server-name.js +9 -17
- package/dist/api/core/site-location.js +16 -24
- package/dist/api/core/stream-probe.router.js +21 -24
- package/dist/api/core/system-events.router.js +23 -26
- package/dist/api/cors/cors-policy.js +20 -29
- package/dist/api/cors/register-cors-policy.js +9 -14
- package/dist/api/deploy-stage-registry.js +6 -10
- package/dist/api/dev-uploads-bundle.js +12 -50
- package/dist/api/health/adaptive-probe.routes.js +9 -13
- package/dist/api/health/health.routes.js +8 -13
- package/dist/api/hub-cluster-url.js +3 -9
- package/dist/api/model-distributor.js +12 -50
- package/dist/api/oauth2/consent-page.js +10 -13
- package/dist/api/oauth2/oauth2-routes.js +17 -25
- package/dist/api/oauth2/private-host.js +1 -4
- package/dist/api/server-upload-propagate.js +15 -51
- package/dist/api/server-upload.js +13 -50
- package/dist/api/sse-no-compression.js +1 -4
- package/dist/api/static/frame-policy.js +4 -9
- package/dist/api/static/precompressed-asset.js +15 -57
- package/dist/api/static/spa-mount.js +8 -47
- package/dist/api/static/spa-static.js +5 -12
- package/dist/api/tarball-manifest.js +7 -44
- package/dist/api/trpc/cap-mount-helpers.js +7 -12
- package/dist/api/trpc/cap-route-error-formatter.js +10 -13
- package/dist/api/trpc/cap-router-runtime.js +40 -51
- package/dist/api/trpc/client-ip.js +9 -20
- package/dist/api/trpc/core-cap-bridge.js +32 -42
- package/dist/api/trpc/device-collection-dispatch.js +23 -27
- package/dist/api/trpc/device-config-secret-redaction.js +24 -40
- package/dist/api/trpc/generated-cap-mounts.js +219 -221
- package/dist/api/trpc/generated-cap-routers.js +4878 -4726
- package/dist/api/trpc/http-request-census-hook.js +8 -11
- package/dist/api/trpc/http-request-census.js +34 -41
- package/dist/api/trpc/http-request-census.singleton.js +4 -8
- package/dist/api/trpc/localized-schema-paths.js +1 -4
- package/dist/api/trpc/principal-visibility.js +14 -22
- package/dist/api/trpc/remote-addon-update-grant.js +1 -4
- package/dist/api/trpc/scope-access.js +10 -15
- package/dist/api/trpc/server-provided-caps.js +3 -7
- package/dist/api/trpc/share-view-access.js +6 -14
- package/dist/api/trpc/trpc-error-device-tags.js +3 -6
- package/dist/api/trpc/trpc-error-principal.js +2 -6
- package/dist/api/trpc/trpc.context.js +20 -25
- package/dist/api/trpc/trpc.middleware.js +38 -46
- package/dist/api/trpc/trpc.router.js +58 -99
- package/dist/api/trpc/user-error.js +5 -8
- package/dist/api/trpc/ws-request-census.js +17 -24
- package/dist/api/trpc/ws-slow-consumer-guard.js +11 -16
- package/dist/api/upload-auth.js +5 -9
- package/dist/auth/addon-route-jwt-gate.js +4 -7
- package/dist/auth/addon-route-share-gate.js +7 -12
- package/dist/auth/app-confirm-page.js +17 -22
- package/dist/auth/app-confirm-routes.js +26 -31
- package/dist/auth/app-confirmations.js +7 -44
- package/dist/auth/app-page-shell.js +17 -30
- package/dist/auth/app-page-strings.js +11 -17
- package/dist/auth/app-passkey-page.js +16 -21
- package/dist/auth/app-passkey-routes.js +37 -41
- package/dist/auth/app-refused-route.js +13 -18
- package/dist/auth/auth-network-policy.js +11 -15
- package/dist/auth/auth-provider-gate.js +4 -9
- package/dist/auth/auth-rate-limit.js +3 -7
- package/dist/auth/client-address.js +6 -9
- package/dist/auth/device-enroll-proofs.js +6 -43
- package/dist/auth/external-link-intents.js +3 -7
- package/dist/auth/hub-sessions.js +6 -12
- package/dist/auth/native-link-completions.js +7 -11
- package/dist/auth/native-return.js +9 -13
- package/dist/auth/passkey-provider.js +26 -32
- package/dist/auth/second-factors.js +6 -10
- package/dist/auth/session-connections.js +6 -12
- package/dist/auth/session-cookie.js +12 -23
- package/dist/auth/session-lifetimes.js +8 -12
- package/dist/auth/session-mint.js +4 -11
- package/dist/auth/sso-finish-app.js +13 -16
- package/dist/auth/sso-finish-deps.js +21 -27
- package/dist/auth/sso-finish.js +28 -38
- package/dist/auth/sso-routes.js +22 -25
- package/dist/auth/used-bridge-ids.js +1 -5
- package/dist/boot/boot-config.js +16 -55
- package/dist/boot/init-sequence.js +1 -4
- package/dist/boot/integration-id-backfill.js +3 -8
- package/dist/boot/platform-keychain-key.js +5 -42
- package/dist/boot/post-boot.service.js +14 -18
- package/dist/boot/reconcile-lifecycle-jobs.js +3 -6
- package/dist/boot/retired-public-url-env.js +4 -8
- package/dist/bootstrap-packages.js +1 -4
- package/dist/closure-node-modules.js +2 -38
- package/dist/closure-patches/patch-reach.json +6 -2
- package/dist/core/addon/addon-call-gateway.js +1 -5
- package/dist/core/addon/addon-package.service.js +83 -123
- package/dist/core/addon/addon-process-view.js +2 -6
- package/dist/core/addon/addon-realign-plan.js +1 -4
- package/dist/core/addon/addon-registry.service.js +254 -197
- package/dist/core/addon/addon-row-manifest.js +1 -4
- package/dist/core/addon/addon-search.service.js +3 -7
- package/dist/core/addon/addon-settings-provider.js +1 -3
- package/dist/core/addon/addon-uninstall.js +4 -11
- package/dist/core/addon/addon.tokens.js +2 -5
- package/dist/core/addon/device-meta-mirror.js +8 -13
- package/dist/core/addon/forked-custom-actions.js +6 -11
- package/dist/core/addon/integration-visibility.js +3 -6
- package/dist/core/addon/materialize-route-path.js +1 -4
- package/dist/core/addon/package-dir-utils.js +8 -45
- package/dist/core/addon/prune-misplaced-addons.js +5 -43
- package/dist/core/addon/runner-convergence.js +1 -4
- package/dist/core/addon/runner-serialization.js +1 -4
- package/dist/core/addon/runner-spawn-fanout.js +3 -8
- package/dist/core/addon/settings-door-wiring.js +5 -8
- package/dist/core/addon/single-flight-refresh.js +1 -4
- package/dist/core/addon-bridge/addon-bridge.service.js +7 -44
- package/dist/core/addon-pages/addon-pages.service.js +3 -40
- package/dist/core/addon-widgets/addon-widgets.service.js +4 -42
- package/dist/core/agent/agent-addon-backfill.js +9 -17
- package/dist/core/agent/agent-delivery-ledger.js +3 -7
- package/dist/core/agent/agent-liveness-event.js +2 -6
- package/dist/core/agent/agent-pairing-http.js +41 -45
- package/dist/core/agent/agent-registry.service.js +53 -92
- package/dist/core/agent/cluster-node-history-mirror.js +3 -7
- package/dist/core/agent/cluster-node-history-store.js +113 -58
- package/dist/core/agent/hub-agent-pairing.js +1 -5
- package/dist/core/agent/node-certificates-client.js +3 -6
- package/dist/core/agent/node-directory.js +1 -4
- package/dist/core/app-links/well-known-app-links.js +2 -6
- package/dist/core/auth/auth.service.js +3 -8
- package/dist/core/auth/handoff-code.service.js +11 -49
- package/dist/core/auth/record-kinds.js +35 -0
- package/dist/core/auth/share-token.service.js +174 -122
- package/dist/core/capability/capability.service.js +1 -5
- package/dist/core/config/config.schema.js +1 -6
- package/dist/core/config/config.service.js +2 -6
- package/dist/core/events/event-bus.service.js +3 -7
- package/dist/core/feature/feature.service.js +2 -6
- package/dist/core/lifecycle/lifecycle-runner.singleton.js +5 -10
- package/dist/core/lifecycle/lifecycle-state-machine.js +1 -5
- package/dist/core/logging/log-ring-buffer.js +1 -5
- package/dist/core/logging/logging.service.js +5 -9
- package/dist/core/logging/scoped-logger.js +1 -5
- package/dist/core/moleculer/agent-readiness-pull.js +1 -4
- package/dist/core/moleculer/agent-registration-request.js +3 -7
- package/dist/core/moleculer/cap-call-fn.js +39 -5
- package/dist/core/moleculer/cap-route-authority.js +3 -8
- package/dist/core/moleculer/hub-mesh-guard.js +5 -9
- package/dist/core/moleculer/moleculer.service.js +108 -119
- package/dist/core/network/network-quality.service.js +2 -6
- package/dist/core/notification/notification-wrapper.service.js +3 -7
- package/dist/core/notification/toast-wrapper.service.js +3 -7
- package/dist/core/provider/provider.tokens.js +1 -4
- package/dist/core/repl/repl-engine.service.js +4 -8
- package/dist/core/runner-health/runner-boot-check.js +7 -15
- package/dist/core/runner-health/update-preflight.js +9 -14
- package/dist/core/server-update/server-update.service.js +12 -49
- package/dist/core/server-update/system-apply-closure-patches.js +6 -42
- package/dist/core/server-update/system-ensure-prebuilds.js +3 -6
- package/dist/core/server-update/system-exec-npm.js +2 -5
- package/dist/core/storage/fs-storage-backend.js +1 -5
- package/dist/core/storage/storage-location-manager.js +1 -5
- package/dist/core/storage/storage.service.js +2 -6
- package/dist/core/streaming/ffmpeg-stream-info.js +3 -8
- package/dist/core/streaming/stream-probe.service.js +12 -16
- package/dist/core/streaming/ttl-cache.js +1 -5
- package/dist/core/topology/topology-emitter.service.js +19 -23
- package/dist/core/update-availability-emitter.js +3 -7
- package/dist/core/update-availability-store.js +8 -45
- package/dist/core/updates/agent-installed-packages.js +5 -9
- package/dist/core/updates/restart-marker.js +3 -8
- package/dist/core/updates/update-check-scheduler.js +13 -18
- package/dist/core/updates/wrapper-update-checker.js +3 -7
- package/dist/first-boot-addon-plan.js +2 -6
- package/dist/framework-nodepath.js +2 -38
- package/dist/i18n/request-lang.js +7 -11
- package/dist/launcher.js +30 -65
- package/dist/lifecycle-journal-path.js +2 -38
- package/dist/main.js +287 -324
- package/dist/manual-boot.js +120 -157
- package/dist/node-role.js +1 -4
- package/dist/package-inventory.js +7 -48
- package/dist/pending-restore.js +7 -43
- package/dist/single-copy-cleanup-runner.js +9 -45
- package/dist/single-copy-cleanup.js +7 -15
- package/dist/sqlite-restore.js +9 -48
- package/dist/starter.js +4 -39
- package/dist/ui-serving/lazy-ui-serving.js +3 -39
- package/package.json +13 -13
package/dist/main.js
CHANGED
|
@@ -1,47 +1,9 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument -- pre-existing lint debt across this 800+ line bootstrap module. The flagged sites cross typed boundaries (Fastify request typing, AddonRouteRegistry, AuthService inherited methods) where the projectService context can't trace inheritance chains. Tracked separately; do not amend in unrelated edits. */
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
Object.defineProperty(o, k2, desc);
|
|
10
|
-
}) : (function(o, m, k, k2) {
|
|
11
|
-
if (k2 === undefined) k2 = k;
|
|
12
|
-
o[k2] = m[k];
|
|
13
|
-
}));
|
|
14
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
-
}) : function(o, v) {
|
|
17
|
-
o["default"] = v;
|
|
18
|
-
});
|
|
19
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
-
var ownKeys = function(o) {
|
|
21
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
-
var ar = [];
|
|
23
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
-
return ar;
|
|
25
|
-
};
|
|
26
|
-
return ownKeys(o);
|
|
27
|
-
};
|
|
28
|
-
return function (mod) {
|
|
29
|
-
if (mod && mod.__esModule) return mod;
|
|
30
|
-
var result = {};
|
|
31
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
-
__setModuleDefault(result, mod);
|
|
33
|
-
return result;
|
|
34
|
-
};
|
|
35
|
-
})();
|
|
36
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
-
};
|
|
39
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
const lazy_ui_serving_js_1 = require("./ui-serving/lazy-ui-serving.js");
|
|
41
|
-
const node_child_process_1 = require("node:child_process");
|
|
42
|
-
const fs = __importStar(require("node:fs"));
|
|
43
|
-
const path = __importStar(require("node:path"));
|
|
44
|
-
const node_tls_1 = require("node:tls");
|
|
2
|
+
import { createLazyUiServing } from './ui-serving/lazy-ui-serving.js';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { Server as TlsServer } from 'node:tls';
|
|
45
7
|
// Boot-time capability declaration runs over the auto-generated
|
|
46
8
|
// `ALL_CAPABILITY_DEFINITIONS` array — every `*.cap.ts` file that ships
|
|
47
9
|
// with `@camstack/types` is included automatically. Adding a new cap
|
|
@@ -52,88 +14,88 @@ const node_tls_1 = require("node:tls");
|
|
|
52
14
|
// Sourced from `@camstack/system` (not `@camstack/types`) so the hub's
|
|
53
15
|
// capability surface refreshes on `camstack deploy packages/system` with no new
|
|
54
16
|
// server image. `@camstack/types` is never deployed to the hub.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
17
|
+
import { AddonRouteRegistry, ALL_CAPABILITY_DEFINITIONS, allFamiliesListenHost, bindPendingLanHttp, createDeferredHeapWatchSink, createEventPlaneReader, createInspectorArrayBufferCensus, createMeshQueueReader, createSocketPlaneReader, createV8Reclaimer, DataPlaneRegistry, getLoggingGate, getMoleculerEventStats, getUserManagementInternal, HUB_MAIN_HEAP_WATCH_LABEL, hubMainRssBudget, onSessionsRevoked, proxyToUpstream, registerHttpsListener, registerLanHttpHandler, signalAllRunnerGroups, startEventLoopStallMonitor, startHeapWatch, } from '@camstack/system';
|
|
18
|
+
import fastifyCompress from '@fastify/compress';
|
|
19
|
+
import fastifyCookie from '@fastify/cookie';
|
|
20
|
+
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
|
|
21
|
+
import { applyWSSHandler } from '@trpc/server/adapters/ws';
|
|
22
|
+
import { WebSocketServer } from 'ws';
|
|
23
|
+
import { registerAddonUploadRoute } from './api/addon-upload';
|
|
24
|
+
import { registerAuthWhoamiRoute } from './api/auth-whoami';
|
|
25
|
+
import { registerBackupDownloadRoute } from './api/backup-download.js';
|
|
26
|
+
import { buildSettingsStoreGateway } from './api/core/cap-providers';
|
|
27
|
+
import { LoggingSettingsService } from './api/core/logging-settings';
|
|
28
|
+
import { RequestCensusService } from './api/core/request-census-settings';
|
|
29
|
+
import { createCorsPolicy, refuseForeignWsUpgrade, registerCorsPolicy, } from './api/cors/register-cors-policy';
|
|
30
|
+
import { ADAPTIVE_PROBE_PER_IP_PER_MINUTE, registerAdaptiveProbeRoute, } from './api/health/adaptive-probe.routes';
|
|
31
|
+
import { registerHealthRoutes } from './api/health/health.routes';
|
|
32
|
+
import { RunnerBootCheckService } from './core/runner-health/runner-boot-check';
|
|
33
|
+
import { clusterBaseUrlResolver, resolveHubBaseUrl } from './api/hub-cluster-url';
|
|
34
|
+
import { buildModelDistributorProvider } from './api/model-distributor.js';
|
|
35
|
+
import { registerOauth2Routes } from './api/oauth2/oauth2-routes.js';
|
|
36
|
+
import { registerServerUploadRoute } from './api/server-upload';
|
|
37
|
+
import { registerServerUploadPropagateRoute } from './api/server-upload-propagate';
|
|
38
|
+
import { markSseRequestNoCompression } from './api/sse-no-compression.js';
|
|
39
|
+
import { registerFramePolicy } from './api/static/frame-policy';
|
|
40
|
+
import { resolvePrecompressedAsset, warmPrecompressedAssets, } from './api/static/precompressed-asset.js';
|
|
41
|
+
import { FORWARDED_PREFIX_HEADER, parseForwardedPrefix, sendSpaShell } from './api/static/spa-mount';
|
|
42
|
+
import { addonBundleCacheControl, contentTypeForPath, isRetiredPublicPath, spaAssetCacheControl, spaShellCacheControl, } from './api/static/spa-static';
|
|
43
|
+
import { buildCoreCapService, setRemotePrivilegeLogSink } from './api/trpc/core-cap-bridge';
|
|
44
|
+
import { getHttpRequestCensus } from './api/trpc/http-request-census.singleton';
|
|
45
|
+
import { recordTrpcHttpRequest } from './api/trpc/http-request-census-hook';
|
|
46
|
+
import { createTrpcContext, createWsTrpcContext } from './api/trpc/trpc.context';
|
|
47
|
+
import { getSessionConnections, SESSION_ENDED_CLOSE_CODE, SESSION_ENDED_CLOSE_REASON, } from './auth/session-connections.js';
|
|
48
|
+
import { buildAppRouter } from './api/trpc/trpc.router';
|
|
49
|
+
import { trpcErrorDeviceTags } from './api/trpc/trpc-error-device-tags';
|
|
50
|
+
import { describeTrpcPrincipal } from './api/trpc/trpc-error-principal';
|
|
51
|
+
import { attachWsCensusSession, identifyWsCensusSession } from './api/trpc/ws-request-census';
|
|
52
|
+
import { WsSlowConsumerGuard, wsConnectionFacts } from './api/trpc/ws-slow-consumer-guard.js';
|
|
53
|
+
import { gateAddonJwt } from './auth/addon-route-jwt-gate.js';
|
|
54
|
+
import { gateAddonShareToken, SHARE_DEVICES_HEADER, SHARE_KIND_HEADER, } from './auth/addon-route-share-gate.js';
|
|
55
|
+
import { createAuthNetworkPolicy } from './auth/auth-network-policy.js';
|
|
56
|
+
import { createSessionTokenCheck, sessionBackingFrom } from './auth/hub-sessions.js';
|
|
57
|
+
import { readSessionLifetimes } from './auth/session-lifetimes.js';
|
|
58
|
+
import { clientKeyFromRequest, createRateLimiter } from './auth/auth-rate-limit';
|
|
59
|
+
import { ExternalLinkIntents } from './auth/external-link-intents.js';
|
|
60
|
+
import { buildSessionCookie, clearSessionCookie, isEmbedRedirectTarget, isSessionGradeJwtPayload, loginRedirectUrl, SESSION_COOKIE, sessionCookieIsSecure, shouldRedirectToLogin, } from './auth/session-cookie.js';
|
|
61
|
+
import { createSessionMinter } from './auth/session-mint.js';
|
|
62
|
+
import { createSsoFinishDeps } from './auth/sso-finish-deps.js';
|
|
63
|
+
import { AppConfirmations } from './auth/app-confirmations.js';
|
|
64
|
+
import { registerAppConfirmRoutes } from './auth/app-confirm-routes.js';
|
|
65
|
+
import { registerAppPasskeyRoutes } from './auth/app-passkey-routes.js';
|
|
66
|
+
import { registerAppRefusedRoute } from './auth/app-refused-route.js';
|
|
67
|
+
import { resolvePasskeyProvider } from './auth/passkey-provider.js';
|
|
68
|
+
import { collectSecondFactors } from './auth/second-factors.js';
|
|
69
|
+
import { registerSsoRoutes } from './auth/sso-routes.js';
|
|
70
|
+
import { UsedBridgeIds } from './auth/used-bridge-ids.js';
|
|
71
|
+
import { NativeLinkCompletions } from './auth/native-link-completions.js';
|
|
72
|
+
import { loadBootstrapConfig, setupInfra } from './boot/boot-config';
|
|
73
|
+
import { loadPlatformKeychainKey } from './boot/platform-keychain-key';
|
|
74
|
+
import { runIntegrationIdBackfill } from './boot/integration-id-backfill';
|
|
75
|
+
import { PostBootService } from './boot/post-boot.service';
|
|
76
|
+
import { dropRetiredHubAddressEnv } from './boot/retired-public-url-env';
|
|
77
|
+
import { AddonPackageService } from './core/addon/addon-package.service';
|
|
78
|
+
import { AddonRegistryService } from './core/addon/addon-registry.service';
|
|
79
|
+
import { AddonBridgeService } from './core/addon-bridge/addon-bridge.service';
|
|
80
|
+
import { AddonPagesService } from './core/addon-pages/addon-pages.service';
|
|
81
|
+
import { AddonWidgetsService, stripBundleVersionSegment, } from './core/addon-widgets/addon-widgets.service';
|
|
82
|
+
import { AgentRegistryService } from './core/agent/agent-registry.service';
|
|
83
|
+
import { buildAppLinkFiles, parseCertFingerprints } from './core/app-links/well-known-app-links';
|
|
84
|
+
import { AuthService } from './core/auth/auth.service';
|
|
85
|
+
import { HandoffCodeService } from './core/auth/handoff-code.service';
|
|
86
|
+
import { ShareTokenService } from './core/auth/share-token.service';
|
|
87
|
+
import { ConfigService } from './core/config/config.service';
|
|
88
|
+
import { EventBusService } from './core/events/event-bus.service';
|
|
89
|
+
import { FeatureService } from './core/feature/feature.service';
|
|
90
|
+
import { LoggingService } from './core/logging/logging.service';
|
|
91
|
+
import { MoleculerService } from './core/moleculer/moleculer.service';
|
|
92
|
+
import { NetworkQualityService } from './core/network/network-quality.service';
|
|
93
|
+
import { ReplEngineService } from './core/repl/repl-engine.service';
|
|
94
|
+
import { ServerUpdateService } from './core/server-update/server-update.service';
|
|
95
|
+
import { StorageService } from './core/storage/storage.service';
|
|
96
|
+
import { StreamProbeService } from './core/streaming/stream-probe.service';
|
|
97
|
+
import { bootManual } from './manual-boot';
|
|
98
|
+
import { runSingleCopyCleanup } from './single-copy-cleanup-runner.js';
|
|
137
99
|
// ---- Process-level error handlers ----
|
|
138
100
|
process.on('uncaughtException', (err) => {
|
|
139
101
|
console.error('[uncaughtException] Unhandled exception — server will continue:', err);
|
|
@@ -154,7 +116,7 @@ function immediateChildCleanup(signal) {
|
|
|
154
116
|
// Every addon-runner leads its OWN process group (D668), so the hub-group kill
|
|
155
117
|
// below no longer reaches runners or their ffmpeg/llama-server children.
|
|
156
118
|
// Signal each runner group first.
|
|
157
|
-
const runnerGroups =
|
|
119
|
+
const runnerGroups = signalAllRunnerGroups('SIGTERM');
|
|
158
120
|
// Synchronously kill all children of this process via the OS.
|
|
159
121
|
// This is fast and ensures no orphans even if Fastify shutdown is slow.
|
|
160
122
|
try {
|
|
@@ -164,7 +126,7 @@ function immediateChildCleanup(signal) {
|
|
|
164
126
|
const cmd = isMac
|
|
165
127
|
? `pkill -TERM -P ${myPid} 2>/dev/null; true`
|
|
166
128
|
: `kill -- -${myPid} 2>/dev/null; true`;
|
|
167
|
-
|
|
129
|
+
execSync(cmd, { timeout: 3000 });
|
|
168
130
|
console.log(`[shutdown] Child processes signalled (runner groups: ${runnerGroups})`);
|
|
169
131
|
}
|
|
170
132
|
catch {
|
|
@@ -196,7 +158,7 @@ function cleanupOrphanProcesses() {
|
|
|
196
158
|
const cmd = isMac
|
|
197
159
|
? "ps -eo pid,ppid,command | grep -E 'coreml_inference\\.py' | grep -v grep"
|
|
198
160
|
: "ps -eo pid,ppid,cmd | grep -E 'coreml_inference\\.py' | grep -v grep";
|
|
199
|
-
const output =
|
|
161
|
+
const output = execSync(cmd, { encoding: 'utf8', timeout: 5000 }).trim();
|
|
200
162
|
if (!output)
|
|
201
163
|
return;
|
|
202
164
|
for (const line of output.split('\n')) {
|
|
@@ -242,9 +204,9 @@ function cleanupOrphanProcesses() {
|
|
|
242
204
|
*/
|
|
243
205
|
async function sendMaybePrecompressed(reply, absolutePath, acceptEncoding) {
|
|
244
206
|
if (reply.getHeader('content-type') === undefined) {
|
|
245
|
-
reply.type(
|
|
207
|
+
reply.type(contentTypeForPath(absolutePath));
|
|
246
208
|
}
|
|
247
|
-
const resolved = await
|
|
209
|
+
const resolved = await resolvePrecompressedAsset(absolutePath, acceptEncoding, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
|
|
248
210
|
if (resolved.encoding === 'br') {
|
|
249
211
|
reply.header('content-encoding', 'br');
|
|
250
212
|
reply.header('vary', 'accept-encoding');
|
|
@@ -265,10 +227,10 @@ async function bootstrap() {
|
|
|
265
227
|
// retired address variable is dropped here — before any addon runner is
|
|
266
228
|
// forked, so no child can read it — and warned about once a logger exists
|
|
267
229
|
// (below). See `boot/retired-public-url-env.ts`.
|
|
268
|
-
const publicUrlWarning =
|
|
230
|
+
const publicUrlWarning = dropRetiredHubAddressEnv(process.env);
|
|
269
231
|
// A desktop hub's keychain master key arrives on an inherited pipe (D788).
|
|
270
232
|
// Read and closed before anything is forked, so no runner inherits it.
|
|
271
|
-
const platformKey =
|
|
233
|
+
const platformKey = loadPlatformKeychainKey();
|
|
272
234
|
if (platformKey.kind === 'loaded') {
|
|
273
235
|
console.log(`[keychain] master key provided by the ${platformKey.label}`);
|
|
274
236
|
}
|
|
@@ -297,7 +259,7 @@ async function bootstrap() {
|
|
|
297
259
|
// calls `malloc_trim`, and no V8 flavor reaches it. So the pass is a real bound on the V8
|
|
298
260
|
// share and touches none of the share that grew. It is telemetry plus a partial bound, not a
|
|
299
261
|
// fix — do not read a reclaim line as "the leak was handled".
|
|
300
|
-
const heapReclaimer =
|
|
262
|
+
const heapReclaimer = createV8Reclaimer();
|
|
301
263
|
const heapReclaim = heapReclaimer === undefined ? undefined : { reclaim: heapReclaimer };
|
|
302
264
|
// ...and the budget the heartbeat alarms on. hub-main has no manifest to declare one in, and
|
|
303
265
|
// the entrypoint that would be the obvious alternative is the least deployable surface here —
|
|
@@ -310,7 +272,7 @@ async function bootstrap() {
|
|
|
310
272
|
// line for every runner in the cluster and none for the largest process in it. The sink is
|
|
311
273
|
// repointed at `LoggingService` the moment it exists (below), and boot lines keep going to
|
|
312
274
|
// stdout exactly as before. See `createDeferredHeapWatchSink`.
|
|
313
|
-
const heapWatchSink =
|
|
275
|
+
const heapWatchSink = createDeferredHeapWatchSink(HUB_MAIN_HEAP_WATCH_LABEL);
|
|
314
276
|
// ...and the event planes, which is the OTHER half of what this process is.
|
|
315
277
|
// hub-main's `arrayBuffers` is a rate, not a set: every forced compaction
|
|
316
278
|
// drops it to 12-29MB (ten passes out of ten, 2026-08-29), so the number to
|
|
@@ -325,12 +287,12 @@ async function bootstrap() {
|
|
|
325
287
|
// the reader reports `undefined` and the line carries no fan-out fields at all
|
|
326
288
|
// — a plane that does not exist yet must not be reported as a silent one.
|
|
327
289
|
let moleculerForEventPlane = null;
|
|
328
|
-
|
|
329
|
-
eventPlane:
|
|
290
|
+
startHeapWatch(HUB_MAIN_HEAP_WATCH_LABEL, heapWatchSink.sink, undefined, heapReclaim, undefined, undefined, true, hubMainRssBudget(), {
|
|
291
|
+
eventPlane: createEventPlaneReader({
|
|
330
292
|
registry: () => moleculerForEventPlane?.childRegistry ?? null,
|
|
331
293
|
crossNodeStats: () => {
|
|
332
294
|
const broker = moleculerForEventPlane?.broker;
|
|
333
|
-
return broker === undefined ? null :
|
|
295
|
+
return broker === undefined ? null : getMoleculerEventStats(broker);
|
|
334
296
|
},
|
|
335
297
|
}),
|
|
336
298
|
// The fan-out counters above answered their own question and sharpened
|
|
@@ -341,7 +303,7 @@ async function bootstrap() {
|
|
|
341
303
|
// analytics` computed from a runner whose rchar also counts page-cache
|
|
342
304
|
// reads. These counters live on the channel itself, so the attribution is
|
|
343
305
|
// per peer and per frame kind. Same lazy registry, same reason.
|
|
344
|
-
socketPlane:
|
|
306
|
+
socketPlane: createSocketPlaneReader({
|
|
345
307
|
registry: () => moleculerForEventPlane?.childRegistry ?? null,
|
|
346
308
|
}),
|
|
347
309
|
// The write queue to every peer NODE, from Moleculer's own writer
|
|
@@ -349,7 +311,7 @@ async function bootstrap() {
|
|
|
349
311
|
// exclusion of "every socket queue" — a sum over `_getActiveHandles()` —
|
|
350
312
|
// never saw them. With the UDS queues above, every outbound plane this
|
|
351
313
|
// process writes to is now on the line and judged on every probe (D446).
|
|
352
|
-
meshQueue:
|
|
314
|
+
meshQueue: createMeshQueueReader(() => moleculerForEventPlane?.broker ?? null),
|
|
353
315
|
// The ArrayBuffer census, armed by the probe itself. Twice on 2026-09-10
|
|
354
316
|
// this process's `arrayBuffers` climbed linearly for an hour (to 1.3 GB
|
|
355
317
|
// and to 5 GB, both LIVE across a forced compaction) and released in one
|
|
@@ -359,7 +321,7 @@ async function bootstrap() {
|
|
|
359
321
|
// the pause a reclaim pass already costs — walked in memory, nothing
|
|
360
322
|
// written, on a threshold held for a minute, at most twice an hour.
|
|
361
323
|
// It is NOT a heap snapshot; see `array-buffer-census.ts`.
|
|
362
|
-
arrayBufferCensus: { census:
|
|
324
|
+
arrayBufferCensus: { census: createInspectorArrayBufferCensus() },
|
|
363
325
|
});
|
|
364
326
|
// Clean up orphaned processes from previous crashes before starting
|
|
365
327
|
cleanupOrphanProcesses();
|
|
@@ -373,14 +335,14 @@ async function bootstrap() {
|
|
|
373
335
|
// path for local/dev runs where neither env var is set.
|
|
374
336
|
const configPath = process.env.CONFIG_PATH ??
|
|
375
337
|
path.join(process.env.CAMSTACK_DATA ?? path.join(process.cwd(), 'camstack-data'), 'config.yaml');
|
|
376
|
-
const bootstrapConfig =
|
|
377
|
-
const infra = await
|
|
338
|
+
const bootstrapConfig = loadBootstrapConfig(configPath);
|
|
339
|
+
const infra = await setupInfra(configPath, bootstrapConfig);
|
|
378
340
|
const { dataPath, tlsOptions, tlsCert } = infra;
|
|
379
341
|
const port = infra.bootstrapConfig.server.port;
|
|
380
342
|
const host = infra.bootstrapConfig.server.host;
|
|
381
343
|
// --- Phase 3: Create app + tRPC register + listen ---
|
|
382
344
|
const fastifyOpts = tlsOptions ? { https: tlsOptions } : {};
|
|
383
|
-
const app = await
|
|
345
|
+
const app = await bootManual({ infra, fastifyOpts });
|
|
384
346
|
app.enableShutdownHooks();
|
|
385
347
|
const fastify = app.getHttpAdapter().getInstance();
|
|
386
348
|
// Which browser origins may read hub responses, and open the /trpc socket
|
|
@@ -388,19 +350,19 @@ async function bootstrap() {
|
|
|
388
350
|
// `null`, and the operator's `server.corsOrigins`. First hook on the
|
|
389
351
|
// instance so every route — and every OPTIONS — is judged.
|
|
390
352
|
if (publicUrlWarning !== undefined) {
|
|
391
|
-
app.get(
|
|
353
|
+
app.get(LoggingService).createLogger('public-url').warn(publicUrlWarning);
|
|
392
354
|
}
|
|
393
|
-
const corsConfig = app.get(
|
|
394
|
-
const corsPolicy =
|
|
355
|
+
const corsConfig = app.get(ConfigService);
|
|
356
|
+
const corsPolicy = createCorsPolicy({
|
|
395
357
|
configuredOrigins: () => corsConfig.get('server.corsOrigins'),
|
|
396
|
-
logger: app.get(
|
|
358
|
+
logger: app.get(LoggingService).createLogger('cors'),
|
|
397
359
|
});
|
|
398
|
-
|
|
399
|
-
await fastify.register(
|
|
360
|
+
registerCorsPolicy(fastify, corsPolicy);
|
|
361
|
+
await fastify.register(fastifyCookie);
|
|
400
362
|
// Every HTML document the hub serves may be framed by its own origin only
|
|
401
363
|
// (D425). Stamped here, on the one instance, so no shell-send site added
|
|
402
364
|
// later can ship without it.
|
|
403
|
-
|
|
365
|
+
registerFramePolicy(fastify);
|
|
404
366
|
// Gzip/Brotli compression for all responses (including static JS/CSS).
|
|
405
367
|
// Registered before @fastify/static so the compress plugin wraps the
|
|
406
368
|
// static send path — hashed admin-ui chunks go from ~2MB to ~600KB on
|
|
@@ -410,9 +372,9 @@ async function bootstrap() {
|
|
|
410
372
|
// silently in the compressor buffer — see `api/sse-no-compression.ts` for
|
|
411
373
|
// the mechanism and the live measurement.
|
|
412
374
|
fastify.addHook('onRequest', async (request) => {
|
|
413
|
-
|
|
375
|
+
markSseRequestNoCompression(request);
|
|
414
376
|
});
|
|
415
|
-
await fastify.register(
|
|
377
|
+
await fastify.register(fastifyCompress, { global: true, threshold: 1024 });
|
|
416
378
|
// Data-plane POST bodies: the addon reverse-proxy (`proxyToUpstream`) pipes
|
|
417
379
|
// `request.raw` upstream, but Fastify's default application/json parser would
|
|
418
380
|
// drain it first, so a POST body would reach the addon empty. Register a
|
|
@@ -421,53 +383,53 @@ async function bootstrap() {
|
|
|
421
383
|
// the raw stream intact for piping. No effect on JSON API routes.
|
|
422
384
|
fastify.addContentTypeParser('application/octet-stream', (_req, _payload, done) => done(null, undefined));
|
|
423
385
|
// Make LocationManager available to StorageService before lifecycle hooks run
|
|
424
|
-
const storageService = app.get(
|
|
386
|
+
const storageService = app.get(StorageService);
|
|
425
387
|
storageService.setLocationManager(infra.locationManager);
|
|
426
388
|
// ConfigService — SettingsStore is wired later by the settings-store capability consumer
|
|
427
|
-
const config = app.get(
|
|
389
|
+
const config = app.get(ConfigService);
|
|
428
390
|
// The real client address behind a trusted proxy, and the opt-in trusted
|
|
429
391
|
// networks — read from settings per request (auth-network-policy.ts).
|
|
430
|
-
const authNetwork =
|
|
392
|
+
const authNetwork = createAuthNetworkPolicy((key) => config.get(key));
|
|
431
393
|
// Public addon routes that declare `rateLimit: 'auth'` (an IdP start /
|
|
432
394
|
// callback) and the SSO finish: 30 per minute per real client address.
|
|
433
|
-
const authRouteLimiter =
|
|
395
|
+
const authRouteLimiter = createRateLimiter({ windowMs: 60_000, max: 30 });
|
|
434
396
|
// Login sessions (local-auth's table, reached through its hub-internal
|
|
435
397
|
// surface): every session token minted from here on carries a `sid`, and
|
|
436
398
|
// every verify of one asks whether that session is still live.
|
|
437
|
-
const sessionLifetimes = () =>
|
|
399
|
+
const sessionLifetimes = () => readSessionLifetimes((key) => config.get(key));
|
|
438
400
|
// Register addon upload route (multipart — must be registered before tRPC).
|
|
439
401
|
// We pass AddonRegistryService so the handler can resolve the
|
|
440
402
|
// `user-management` cap singleton at request time (the only working
|
|
441
403
|
// path to validate `cst_*` scoped tokens — see addon-upload.ts).
|
|
442
404
|
try {
|
|
443
|
-
const uploadAuthService = app.get(
|
|
444
|
-
const uploadAddonBridge = app.get(
|
|
445
|
-
const uploadMoleculer = app.get(
|
|
446
|
-
const uploadAddonRegistry = app.get(
|
|
447
|
-
const uploadAddonPackage = app.get(
|
|
448
|
-
const uploadLogger = app.get(
|
|
405
|
+
const uploadAuthService = app.get(AuthService);
|
|
406
|
+
const uploadAddonBridge = app.get(AddonBridgeService);
|
|
407
|
+
const uploadMoleculer = app.get(MoleculerService);
|
|
408
|
+
const uploadAddonRegistry = app.get(AddonRegistryService);
|
|
409
|
+
const uploadAddonPackage = app.get(AddonPackageService);
|
|
410
|
+
const uploadLogger = app.get(LoggingService).createLogger('addon-upload');
|
|
449
411
|
// The delivery ledger marks "the hub is writing addons to this node" for
|
|
450
412
|
// the whole deploy+reload, so the addon back-fill cannot read the roster
|
|
451
413
|
// gap a deploy opens and repair the node over the bytes just shipped.
|
|
452
|
-
await
|
|
414
|
+
await registerAddonUploadRoute(fastify, uploadAddonBridge, uploadAuthService, uploadMoleculer, uploadAddonRegistry, uploadAddonPackage, uploadLogger, clusterBaseUrlResolver(() => uploadAddonRegistry
|
|
453
415
|
.getCapabilityRegistry()
|
|
454
|
-
.getSingleton('local-network')), app.get(
|
|
416
|
+
.getSingleton('local-network')), app.get(AgentRegistryService).deliveryLedger);
|
|
455
417
|
console.log('[bootstrap] Addon upload route registered at POST /api/addons/upload');
|
|
456
418
|
// Dev server-deploy channel gateway (`camstack deploy-server`). MUST be
|
|
457
419
|
// registered AFTER registerAddonUploadRoute — it reuses the multipart
|
|
458
420
|
// plugin that route installs on this instance.
|
|
459
|
-
|
|
421
|
+
registerServerUploadRoute(fastify, {
|
|
460
422
|
authService: uploadAuthService,
|
|
461
423
|
addonRegistry: uploadAddonRegistry,
|
|
462
|
-
logger: app.get(
|
|
424
|
+
logger: app.get(LoggingService).createLogger('server-upload'),
|
|
463
425
|
});
|
|
464
426
|
console.log('[bootstrap] Server upload route registered at POST /api/server-upload');
|
|
465
|
-
|
|
427
|
+
registerServerUploadPropagateRoute(fastify, {
|
|
466
428
|
authService: uploadAuthService,
|
|
467
429
|
addonRegistry: uploadAddonRegistry,
|
|
468
|
-
logger: app.get(
|
|
430
|
+
logger: app.get(LoggingService).createLogger('server-upload-propagate'),
|
|
469
431
|
broker: uploadMoleculer.broker,
|
|
470
|
-
resolveClusterBaseUrl:
|
|
432
|
+
resolveClusterBaseUrl: clusterBaseUrlResolver(() => uploadAddonRegistry
|
|
471
433
|
.getCapabilityRegistry()
|
|
472
434
|
.getSingleton('local-network')),
|
|
473
435
|
});
|
|
@@ -476,7 +438,7 @@ async function bootstrap() {
|
|
|
476
438
|
// scoped tokens, returns the resolved identity + scope summary.
|
|
477
439
|
// Mirrors the addon-upload auth chain so the CLI can ping for
|
|
478
440
|
// token-still-valid without consuming a real cap.
|
|
479
|
-
await
|
|
441
|
+
await registerAuthWhoamiRoute(fastify, uploadAuthService, uploadAddonRegistry);
|
|
480
442
|
console.log('[bootstrap] Auth whoami route registered at GET /api/auth/whoami');
|
|
481
443
|
}
|
|
482
444
|
catch (err) {
|
|
@@ -485,7 +447,7 @@ async function bootstrap() {
|
|
|
485
447
|
// Register tRPC plugin on Fastify BEFORE listen.
|
|
486
448
|
// If registration fails, start in degraded mode: serve a health warning endpoint.
|
|
487
449
|
// Instantiate new core services
|
|
488
|
-
const loggingService = app.get(
|
|
450
|
+
const loggingService = app.get(LoggingService);
|
|
489
451
|
// The heap heartbeat's handover: from here on hub-main's `[mem]` lines go through the log
|
|
490
452
|
// manager like every runner's do — buffer, console destination, rotated file and Loki — so
|
|
491
453
|
// the one process that mediates every cross-runner call is finally trendable next to the
|
|
@@ -516,19 +478,19 @@ async function bootstrap() {
|
|
|
516
478
|
// interval is `unref`ed, so it cannot hold the process open, and hub-main's
|
|
517
479
|
// shutdown is process exit.
|
|
518
480
|
const stallLogger = loggingService.createLogger('event-loop');
|
|
519
|
-
|
|
520
|
-
const addonRouteRegistry = new
|
|
521
|
-
const dataPlaneRegistry = new
|
|
481
|
+
startEventLoopStallMonitor(stallLogger);
|
|
482
|
+
const addonRouteRegistry = new AddonRouteRegistry();
|
|
483
|
+
const dataPlaneRegistry = new DataPlaneRegistry();
|
|
522
484
|
// Use Fastify-managed notification/toast wrappers (globally provided by NotificationModule)
|
|
523
|
-
const { NotificationServiceWrapper } = await
|
|
524
|
-
const { ToastServiceWrapper } = await
|
|
485
|
+
const { NotificationServiceWrapper } = await import('./core/notification/notification-wrapper.service');
|
|
486
|
+
const { ToastServiceWrapper } = await import('./core/notification/toast-wrapper.service');
|
|
525
487
|
const notificationWrapper = app.get(NotificationServiceWrapper);
|
|
526
488
|
const toastWrapper = app.get(ToastServiceWrapper);
|
|
527
489
|
// Expose the underlying core services for the tRPC router
|
|
528
490
|
const notificationService = notificationWrapper.service;
|
|
529
491
|
const toastService = toastWrapper.service;
|
|
530
492
|
// Wire AddonRouteRegistry and NotificationService
|
|
531
|
-
const addonRegistry = app.get(
|
|
493
|
+
const addonRegistry = app.get(AddonRegistryService);
|
|
532
494
|
addonRegistry.setAddonRouteRegistry(addonRouteRegistry);
|
|
533
495
|
addonRegistry.setDataPlaneRegistry(dataPlaneRegistry);
|
|
534
496
|
// ── Configure the CapabilityRegistry (created in AddonRegistryService constructor) ──
|
|
@@ -539,7 +501,7 @@ async function bootstrap() {
|
|
|
539
501
|
hubInstalledVersion: (name) => {
|
|
540
502
|
try {
|
|
541
503
|
return (app
|
|
542
|
-
.get(
|
|
504
|
+
.get(AddonPackageService)
|
|
543
505
|
.listInstalled()
|
|
544
506
|
.find((p) => p.name === name)?.version ?? null);
|
|
545
507
|
}
|
|
@@ -549,7 +511,7 @@ async function bootstrap() {
|
|
|
549
511
|
},
|
|
550
512
|
audit: async (event) => {
|
|
551
513
|
try {
|
|
552
|
-
const internal =
|
|
514
|
+
const internal = getUserManagementInternal(capabilityRegistry.getSingleton('user-management'));
|
|
553
515
|
if (internal === null) {
|
|
554
516
|
console.warn('[bootstrap] agent addon update not audited: no user-management internal');
|
|
555
517
|
return;
|
|
@@ -561,17 +523,17 @@ async function bootstrap() {
|
|
|
561
523
|
}
|
|
562
524
|
},
|
|
563
525
|
});
|
|
564
|
-
const sessionBacking =
|
|
526
|
+
const sessionBacking = sessionBackingFrom(capabilityRegistry, sessionLifetimes);
|
|
565
527
|
// From here on every verify of a session-grade token — tRPC, addon routes,
|
|
566
528
|
// the session cookie, OAuth authorize, uploads, health — requires its
|
|
567
529
|
// session to be live (`hub-sessions.ts`).
|
|
568
|
-
app.get(
|
|
530
|
+
app.get(AuthService).setSessionCheck(createSessionTokenCheck(capabilityRegistry));
|
|
569
531
|
// Declare every shipped capability BEFORE app.init() so addon
|
|
570
532
|
// registerProvider() calls (in-process or via the Moleculer bridge
|
|
571
533
|
// from forked workers / agents) find their definition during
|
|
572
534
|
// onModuleInit. The list comes from the codegen — see the import
|
|
573
535
|
// comment above for the rationale.
|
|
574
|
-
for (const capDef of
|
|
536
|
+
for (const capDef of ALL_CAPABILITY_DEFINITIONS) {
|
|
575
537
|
capabilityRegistry.declareCapability(capDef);
|
|
576
538
|
}
|
|
577
539
|
// Hub-internal `sso-bridge` provider — gives auth-provider addons
|
|
@@ -579,7 +541,7 @@ async function bootstrap() {
|
|
|
579
541
|
// bridge token before redirecting to `/api/auth/sso/finish`. Without
|
|
580
542
|
// this, the finish endpoint would have to trust unsigned query params
|
|
581
543
|
// (anyone could craft `?isAdmin=1`). Backed by AuthService.
|
|
582
|
-
const authServiceForBridge = app.get(
|
|
544
|
+
const authServiceForBridge = app.get(AuthService);
|
|
583
545
|
capabilityRegistry.registerProvider('sso-bridge', '$hub', {
|
|
584
546
|
signBridgeToken: async ({ claims, ttlSec, }) => {
|
|
585
547
|
const token = authServiceForBridge.signSsoBridgeToken(claims, ttlSec ?? 300);
|
|
@@ -598,38 +560,39 @@ async function bootstrap() {
|
|
|
598
560
|
// Share-view (`csv_*`) tokens — hub-resident mint/verify/revoke over the
|
|
599
561
|
// settings backend (lazy getter: the backend lands after the sqlite-storage
|
|
600
562
|
// builtin registers; the service resolves it per call).
|
|
601
|
-
const shareTokenService = new
|
|
563
|
+
const shareTokenService = new ShareTokenService(() => addonRegistry.getSettingsBackend(), loggingService.createLogger('share-tokens'));
|
|
564
|
+
addonRegistry.setShareTokenService(shareTokenService);
|
|
602
565
|
// One-time native-app login handoff codes (in-memory, 60s TTL, single-use) —
|
|
603
566
|
// minted by `auth.createHandoffCode`, redeemed by the viewer app via the
|
|
604
567
|
// public `auth.redeemHandoffCode`.
|
|
605
|
-
const handoffCodeService = new
|
|
568
|
+
const handoffCodeService = new HandoffCodeService();
|
|
606
569
|
// Pending "link this sign-in account" intents (D782) — minted by
|
|
607
570
|
// `auth.beginExternalLink`, consumed once by `/api/auth/sso/finish`.
|
|
608
|
-
const externalLinkIntents = new
|
|
571
|
+
const externalLinkIntents = new ExternalLinkIntents();
|
|
609
572
|
// Native-app links parked for the app to claim, and one-time codes that
|
|
610
573
|
// open the account page signed in, in a browser (D801).
|
|
611
|
-
const nativeLinkCompletions = new
|
|
574
|
+
const nativeLinkCompletions = new NativeLinkCompletions();
|
|
612
575
|
// `jti`s of login bridges already finished — a bridge is single-use.
|
|
613
|
-
const usedBridgeIds = new
|
|
614
|
-
const browserHandoffCodes = new
|
|
576
|
+
const usedBridgeIds = new UsedBridgeIds();
|
|
577
|
+
const browserHandoffCodes = new HandoffCodeService();
|
|
615
578
|
// Parked "sign the app in?" confirmations of the two native app flows (D858).
|
|
616
|
-
const appConfirmations = new
|
|
617
|
-
const nativeLinkMinter =
|
|
618
|
-
signer: app.get(
|
|
579
|
+
const appConfirmations = new AppConfirmations();
|
|
580
|
+
const nativeLinkMinter = createSessionMinter({
|
|
581
|
+
signer: app.get(AuthService),
|
|
619
582
|
directory: () => capabilityRegistry.getSingleton('user-management'),
|
|
620
583
|
sessions: sessionBacking,
|
|
621
584
|
});
|
|
622
585
|
const nativeLinkWiring = () => ({
|
|
623
|
-
auth: app.get(
|
|
586
|
+
auth: app.get(AuthService),
|
|
624
587
|
registry: capabilityRegistry,
|
|
625
|
-
moleculer: app.get(
|
|
588
|
+
moleculer: app.get(MoleculerService),
|
|
626
589
|
minter: nativeLinkMinter,
|
|
627
590
|
linkIntents: externalLinkIntents,
|
|
628
591
|
nativeLinks: nativeLinkCompletions,
|
|
629
592
|
usedBridgeIds,
|
|
630
593
|
});
|
|
631
|
-
const buildRouter = () =>
|
|
632
|
-
authService: app.get(
|
|
594
|
+
const buildRouter = () => buildAppRouter({
|
|
595
|
+
authService: app.get(AuthService),
|
|
633
596
|
shareTokenService,
|
|
634
597
|
handoffCodeService,
|
|
635
598
|
externalLinkIntents,
|
|
@@ -638,26 +601,26 @@ async function bootstrap() {
|
|
|
638
601
|
browserHandoffs: browserHandoffCodes,
|
|
639
602
|
// The SSO finish's own link step — the same conflict rule, the same
|
|
640
603
|
// store — run for the app's claim instead of the browser's return.
|
|
641
|
-
link: (input) =>
|
|
604
|
+
link: (input) => createSsoFinishDeps(nativeLinkWiring(), undefined).identities.link(input),
|
|
642
605
|
},
|
|
643
606
|
configService: config,
|
|
644
607
|
authNetworkPolicy: authNetwork,
|
|
645
608
|
sessionBacking,
|
|
646
|
-
featureService: app.get(
|
|
609
|
+
featureService: app.get(FeatureService),
|
|
647
610
|
loggingService,
|
|
648
|
-
eventBus: app.get(
|
|
649
|
-
agentRegistry: app.get(
|
|
650
|
-
moleculer: app.get(
|
|
611
|
+
eventBus: app.get(EventBusService),
|
|
612
|
+
agentRegistry: app.get(AgentRegistryService),
|
|
613
|
+
moleculer: app.get(MoleculerService),
|
|
651
614
|
addonRegistry,
|
|
652
|
-
replEngine: app.get(
|
|
653
|
-
networkQualityService: app.get(
|
|
654
|
-
addonBridge: app.get(
|
|
655
|
-
addonPackageService: app.get(
|
|
615
|
+
replEngine: app.get(ReplEngineService),
|
|
616
|
+
networkQualityService: app.get(NetworkQualityService),
|
|
617
|
+
addonBridge: app.get(AddonBridgeService),
|
|
618
|
+
addonPackageService: app.get(AddonPackageService),
|
|
656
619
|
notificationService,
|
|
657
620
|
toastService,
|
|
658
621
|
capabilityRegistry,
|
|
659
|
-
streamProbe: app.get(
|
|
660
|
-
serverUpdateService: app.get(
|
|
622
|
+
streamProbe: app.get(StreamProbeService),
|
|
623
|
+
serverUpdateService: app.get(ServerUpdateService),
|
|
661
624
|
});
|
|
662
625
|
// ── The core-cap mesh bridge, mounted BEFORE a single addon boots (D171) ──
|
|
663
626
|
//
|
|
@@ -682,9 +645,9 @@ async function bootstrap() {
|
|
|
682
645
|
try {
|
|
683
646
|
appRouter = buildRouter();
|
|
684
647
|
{
|
|
685
|
-
const meshBroker = app.get(
|
|
648
|
+
const meshBroker = app.get(MoleculerService);
|
|
686
649
|
// D802: another node's call runs as `node:<id>`, never the hub's own principal.
|
|
687
|
-
meshBroker.registerCoreCapService(
|
|
650
|
+
meshBroker.registerCoreCapService(buildCoreCapService(appRouter, undefined, {
|
|
688
651
|
ownNodeId: meshBroker.nodeId,
|
|
689
652
|
remoteUpdateGrant: remoteUpdateGrantDeps(),
|
|
690
653
|
}));
|
|
@@ -704,7 +667,7 @@ async function bootstrap() {
|
|
|
704
667
|
capabilityRegistry.ready();
|
|
705
668
|
// Register log-receiver service for agent log forwarding (after app.init
|
|
706
669
|
// so Moleculer re-advertises the service list to the network)
|
|
707
|
-
const moleculer = app.get(
|
|
670
|
+
const moleculer = app.get(MoleculerService);
|
|
708
671
|
// Hand the heartbeat its event-plane sources. The reader holds this by
|
|
709
672
|
// closure and re-reads `childRegistry` / `broker` on every line, so a registry
|
|
710
673
|
// that is rebuilt later is picked up without re-wiring anything.
|
|
@@ -716,26 +679,26 @@ async function bootstrap() {
|
|
|
716
679
|
// router) so forked addons (addon-model-studio) can reach it via `ctx.api`,
|
|
717
680
|
// not only external HTTP clients. Needs the broker + the deploy-stage
|
|
718
681
|
// registry, which an addon can't access.
|
|
719
|
-
capabilityRegistry.registerProvider('model-distributor', '$hub',
|
|
682
|
+
capabilityRegistry.registerProvider('model-distributor', '$hub', buildModelDistributorProvider(moleculer, loggingService, clusterBaseUrlResolver(() => capabilityRegistry.getSingleton('local-network'))));
|
|
720
683
|
// ── Health routes (hub self + agent forwarding) ──────────────────
|
|
721
684
|
// Registered after app.init() so the AgentRegistryService is wired and
|
|
722
685
|
// the Moleculer broker is ready to forward `$agent.health` calls.
|
|
723
686
|
try {
|
|
724
687
|
const hubVersion = readHubVersion();
|
|
725
|
-
|
|
688
|
+
registerHealthRoutes(fastify, {
|
|
726
689
|
moleculer,
|
|
727
|
-
agentRegistry: app.get(
|
|
690
|
+
agentRegistry: app.get(AgentRegistryService),
|
|
728
691
|
hubVersion,
|
|
729
692
|
// Read the live flag at request time — flips to true once the tRPC
|
|
730
693
|
// router finishes registering (see `trpcRegistered = true` below).
|
|
731
694
|
isReady: () => trpcRegistered,
|
|
732
|
-
verifyToken: (token) => app.get(
|
|
733
|
-
runnerBootCheck: () => app.get(
|
|
695
|
+
verifyToken: (token) => app.get(AuthService).verifyToken(token),
|
|
696
|
+
runnerBootCheck: () => app.get(RunnerBootCheckService).current(),
|
|
734
697
|
});
|
|
735
698
|
console.log(`[bootstrap] Health routes registered (hub v${hubVersion})`);
|
|
736
|
-
|
|
737
|
-
verifyToken: (token) => app.get(
|
|
738
|
-
limiter:
|
|
699
|
+
registerAdaptiveProbeRoute(fastify, {
|
|
700
|
+
verifyToken: (token) => app.get(AuthService).verifyToken(token),
|
|
701
|
+
limiter: createRateLimiter({ windowMs: 60_000, max: ADAPTIVE_PROBE_PER_IP_PER_MINUTE }),
|
|
739
702
|
network: authNetwork,
|
|
740
703
|
});
|
|
741
704
|
}
|
|
@@ -768,12 +731,12 @@ async function bootstrap() {
|
|
|
768
731
|
// this boot unless `restore()` (below, just before `listen`) finds a window
|
|
769
732
|
// an operator armed earlier. The logger is attached now so a restored census
|
|
770
733
|
// has somewhere to report before the first request arrives.
|
|
771
|
-
const httpRequestCensus =
|
|
734
|
+
const httpRequestCensus = getHttpRequestCensus();
|
|
772
735
|
const trpcLogger = loggingService.createLogger('tRPC');
|
|
773
736
|
httpRequestCensus.setLogger(trpcLogger);
|
|
774
|
-
|
|
737
|
+
setRemotePrivilegeLogSink(trpcLogger);
|
|
775
738
|
try {
|
|
776
|
-
const authService = app.get(
|
|
739
|
+
const authService = app.get(AuthService);
|
|
777
740
|
// Normally already built and bridged onto the mesh in `beforeAddonBoot`
|
|
778
741
|
// above — this is the fallback for the case where that failed, so a hub
|
|
779
742
|
// whose mesh bridge could not be mounted still serves HTTP.
|
|
@@ -781,9 +744,9 @@ async function bootstrap() {
|
|
|
781
744
|
appRouter = buildRouter();
|
|
782
745
|
try {
|
|
783
746
|
{
|
|
784
|
-
const meshBroker = app.get(
|
|
747
|
+
const meshBroker = app.get(MoleculerService);
|
|
785
748
|
// D802: another node's call runs as `node:<id>`, never the hub's own principal.
|
|
786
|
-
meshBroker.registerCoreCapService(
|
|
749
|
+
meshBroker.registerCoreCapService(buildCoreCapService(appRouter, undefined, {
|
|
787
750
|
ownNodeId: meshBroker.nodeId,
|
|
788
751
|
remoteUpdateGrant: remoteUpdateGrantDeps(),
|
|
789
752
|
}));
|
|
@@ -794,30 +757,30 @@ async function bootstrap() {
|
|
|
794
757
|
console.warn('[bootstrap] Failed to register core-cap mesh bridge:', err);
|
|
795
758
|
}
|
|
796
759
|
}
|
|
797
|
-
await fastify.register(
|
|
760
|
+
await fastify.register(fastifyTRPCPlugin, {
|
|
798
761
|
prefix: '/trpc',
|
|
799
762
|
trpcOptions: {
|
|
800
763
|
router: appRouter,
|
|
801
764
|
createContext: async ({ req }) => {
|
|
802
|
-
const trpcCtx = await
|
|
765
|
+
const trpcCtx = await createTrpcContext(req, authService, addonRegistry, shareTokenService,
|
|
803
766
|
// A bearer refused for NOT being an API credential never reaches a
|
|
804
767
|
// procedure, so `onError` below never sees it — the refusal would
|
|
805
768
|
// be silent without this. Same reason `gateAddonJwt` logs (D103).
|
|
806
|
-
app.get(
|
|
769
|
+
app.get(LoggingService).createLogger('tRPC'));
|
|
807
770
|
// WHO called, counted here because this is the first point at which
|
|
808
771
|
// a principal exists AND the only one tRPC runs exactly once per
|
|
809
772
|
// HTTP request (batched or not). Disarmed this is a boolean read and
|
|
810
773
|
// a return — see `http-request-census-hook.ts`.
|
|
811
|
-
|
|
774
|
+
recordTrpcHttpRequest(httpRequestCensus, req, trpcCtx.user);
|
|
812
775
|
return trpcCtx;
|
|
813
776
|
},
|
|
814
777
|
onError: ({ path: trpcPath, error, ctx, input, }) => {
|
|
815
|
-
const trpcLogger = app.get(
|
|
778
|
+
const trpcLogger = app.get(LoggingService).createLogger('tRPC');
|
|
816
779
|
trpcLogger.warn('tRPC error', {
|
|
817
780
|
// WHICH camera. A refusal on a device-scoped path that names no
|
|
818
781
|
// deviceId cannot answer "one camera or all of them?" — the
|
|
819
782
|
// question always asked first (2026-08-29, `intercom.startSession`).
|
|
820
|
-
tags:
|
|
783
|
+
tags: trpcErrorDeviceTags(trpcPath, input),
|
|
821
784
|
meta: {
|
|
822
785
|
code: error.code,
|
|
823
786
|
path: trpcPath ?? '?',
|
|
@@ -825,7 +788,7 @@ async function bootstrap() {
|
|
|
825
788
|
// to the integration that hit it, and an operator grepping for
|
|
826
789
|
// the integration finds nothing — which reads as "the hub logged
|
|
827
790
|
// nothing" (2026-08-09).
|
|
828
|
-
principal:
|
|
791
|
+
principal: describeTrpcPrincipal(ctx?.user),
|
|
829
792
|
message: error.message,
|
|
830
793
|
},
|
|
831
794
|
});
|
|
@@ -840,7 +803,7 @@ async function bootstrap() {
|
|
|
840
803
|
});
|
|
841
804
|
trpcRegistered = true;
|
|
842
805
|
// Register addon-pages static file endpoint
|
|
843
|
-
const addonPagesService = app.get(
|
|
806
|
+
const addonPagesService = app.get(AddonPagesService);
|
|
844
807
|
fastify.get('/api/addon-pages/:addonId/*', async (request, reply) => {
|
|
845
808
|
const { addonId } = request.params;
|
|
846
809
|
const filePath = request.params['*'];
|
|
@@ -863,7 +826,7 @@ async function bootstrap() {
|
|
|
863
826
|
: 'application/octet-stream';
|
|
864
827
|
const stream = fs.createReadStream(resolved);
|
|
865
828
|
return reply
|
|
866
|
-
.header('cache-control',
|
|
829
|
+
.header('cache-control', addonBundleCacheControl(resolved))
|
|
867
830
|
.type(contentType)
|
|
868
831
|
.send(stream);
|
|
869
832
|
});
|
|
@@ -872,7 +835,7 @@ async function bootstrap() {
|
|
|
872
835
|
// `dist/widgets.mjs` (declared per-widget in `addon-widgets-source`
|
|
873
836
|
// metadata). Path-traversal protection + cap-registration check
|
|
874
837
|
// both live in `AddonWidgetsService.resolveBundle`.
|
|
875
|
-
const addonWidgetsService = app.get(
|
|
838
|
+
const addonWidgetsService = app.get(AddonWidgetsService);
|
|
876
839
|
fastify.get('/api/addon-widgets/:addonId/*', async (request, reply) => {
|
|
877
840
|
const { addonId } = request.params;
|
|
878
841
|
const filePath = request.params['*'];
|
|
@@ -898,10 +861,10 @@ async function bootstrap() {
|
|
|
898
861
|
// fixed-name `_stub.js`/`remoteEntry.js` — is safely immutable (this
|
|
899
862
|
// defeats a CDN edge serving a stale stub). Legacy unversioned requests
|
|
900
863
|
// keep the per-file revalidation policy.
|
|
901
|
-
const { versioned } =
|
|
864
|
+
const { versioned } = stripBundleVersionSegment(filePath);
|
|
902
865
|
const cacheControl = versioned
|
|
903
866
|
? 'public, max-age=31536000, immutable'
|
|
904
|
-
:
|
|
867
|
+
: addonBundleCacheControl(resolved);
|
|
905
868
|
const stream = fs.createReadStream(resolved);
|
|
906
869
|
return reply.header('cache-control', cacheControl).type(contentType).send(stream);
|
|
907
870
|
});
|
|
@@ -942,16 +905,16 @@ async function bootstrap() {
|
|
|
942
905
|
// bridge bound to the browser that started it; the session is minted by
|
|
943
906
|
// THE session mint from the user record as it is now (D712). Rules in
|
|
944
907
|
// `auth/sso-finish.ts`, routes in `auth/sso-routes.ts`.
|
|
945
|
-
const ssoMinter =
|
|
946
|
-
signer: app.get(
|
|
908
|
+
const ssoMinter = createSessionMinter({
|
|
909
|
+
signer: app.get(AuthService),
|
|
947
910
|
directory: () => capabilityRegistry.getSingleton('user-management'),
|
|
948
911
|
sessions: sessionBacking,
|
|
949
912
|
});
|
|
950
|
-
|
|
913
|
+
registerSsoRoutes(fastify, {
|
|
951
914
|
wiring: {
|
|
952
|
-
auth: app.get(
|
|
915
|
+
auth: app.get(AuthService),
|
|
953
916
|
registry: capabilityRegistry,
|
|
954
|
-
moleculer: app.get(
|
|
917
|
+
moleculer: app.get(MoleculerService),
|
|
955
918
|
minter: ssoMinter,
|
|
956
919
|
linkIntents: externalLinkIntents,
|
|
957
920
|
nativeLinks: nativeLinkCompletions,
|
|
@@ -962,46 +925,46 @@ async function bootstrap() {
|
|
|
962
925
|
browserHandoffCodes,
|
|
963
926
|
confirmations: appConfirmations,
|
|
964
927
|
minter: ssoMinter,
|
|
965
|
-
logger: app.get(
|
|
928
|
+
logger: app.get(LoggingService).createLogger('sso-finish'),
|
|
966
929
|
});
|
|
967
930
|
// ── Passkey sign-in for the native app (D858) ───────────────────
|
|
968
931
|
// A standalone page + two same-origin legs; the assertion ends in a handoff
|
|
969
932
|
// code at the app's custom scheme, never in a web session.
|
|
970
|
-
|
|
971
|
-
passkeys: () =>
|
|
933
|
+
registerAppPasskeyRoutes(fastify, {
|
|
934
|
+
passkeys: () => resolvePasskeyProvider(capabilityRegistry, app.get(MoleculerService)),
|
|
972
935
|
secondFactors: (userId) => {
|
|
973
936
|
const um = capabilityRegistry.getSingleton('user-management');
|
|
974
|
-
return
|
|
937
|
+
return collectSecondFactors({
|
|
975
938
|
...(um && typeof um.getTotpStatus === 'function'
|
|
976
939
|
? { getTotpStatus: (input) => um.getTotpStatus(input) }
|
|
977
940
|
: {}),
|
|
978
|
-
passkeyProvider: () =>
|
|
941
|
+
passkeyProvider: () => resolvePasskeyProvider(capabilityRegistry, app.get(MoleculerService)),
|
|
979
942
|
}, userId, 'passkey');
|
|
980
943
|
},
|
|
981
944
|
minter: ssoMinter,
|
|
982
945
|
confirmations: appConfirmations,
|
|
983
946
|
network: authNetwork,
|
|
984
947
|
limiter: authRouteLimiter,
|
|
985
|
-
logger: app.get(
|
|
948
|
+
logger: app.get(LoggingService).createLogger('app-passkey'),
|
|
986
949
|
});
|
|
987
|
-
|
|
950
|
+
registerAppRefusedRoute(fastify);
|
|
988
951
|
// The last step of both app flows: "Sign in the app as <user>?" - the
|
|
989
952
|
// handoff code is minted only when the person presses Continue (D858).
|
|
990
|
-
|
|
953
|
+
registerAppConfirmRoutes(fastify, {
|
|
991
954
|
confirmations: appConfirmations,
|
|
992
955
|
handoffCodes: handoffCodeService,
|
|
993
956
|
network: authNetwork,
|
|
994
957
|
limiter: authRouteLimiter,
|
|
995
|
-
logger: app.get(
|
|
958
|
+
logger: app.get(LoggingService).createLogger('app-confirm'),
|
|
996
959
|
});
|
|
997
960
|
// ── Backup archive download ─────────────────────────────────────
|
|
998
961
|
// Streams an archive through the storage cap's chunked-download
|
|
999
962
|
// protocol at the pace the client reads it (D447). The route, its
|
|
1000
963
|
// auth chain and the drain-respecting pump live in `api/backup-download*`.
|
|
1001
|
-
|
|
1002
|
-
verifier: app.get(
|
|
964
|
+
registerBackupDownloadRoute(fastify, {
|
|
965
|
+
verifier: app.get(AuthService),
|
|
1003
966
|
capabilities: capabilityRegistry,
|
|
1004
|
-
logger: app.get(
|
|
967
|
+
logger: app.get(LoggingService).createLogger('backup-download'),
|
|
1005
968
|
});
|
|
1006
969
|
// POST /api/auth/session — upgrade a tRPC-issued JWT to a browser cookie.
|
|
1007
970
|
fastify.post('/api/auth/session', async (request, reply) => {
|
|
@@ -1015,7 +978,7 @@ async function bootstrap() {
|
|
|
1015
978
|
// e.g. the totp-challenge from login leg 1) verify under the same
|
|
1016
979
|
// secret — accepting one here handed out a cookie that bypassed
|
|
1017
980
|
// the second factor on every cookie-gated surface.
|
|
1018
|
-
if (!
|
|
981
|
+
if (!isSessionGradeJwtPayload(payload)) {
|
|
1019
982
|
return reply.status(401).send({ error: 'invalid token' });
|
|
1020
983
|
}
|
|
1021
984
|
const expSec = typeof payload.exp === 'number' ? payload.exp : 0;
|
|
@@ -1024,16 +987,16 @@ async function bootstrap() {
|
|
|
1024
987
|
catch {
|
|
1025
988
|
return reply.status(401).send({ error: 'invalid token' });
|
|
1026
989
|
}
|
|
1027
|
-
const c =
|
|
1028
|
-
secure:
|
|
990
|
+
const c = buildSessionCookie(token, ttlSec, {
|
|
991
|
+
secure: sessionCookieIsSecure(request.protocol, request.headers, FORWARDED_PREFIX_HEADER),
|
|
1029
992
|
});
|
|
1030
993
|
reply.setCookie(c.name, c.value, c.options);
|
|
1031
994
|
return reply.send({ ok: true });
|
|
1032
995
|
});
|
|
1033
996
|
// DELETE /api/auth/session — clear the cookie on logout.
|
|
1034
997
|
fastify.delete('/api/auth/session', async (request, reply) => {
|
|
1035
|
-
const c =
|
|
1036
|
-
secure:
|
|
998
|
+
const c = clearSessionCookie({
|
|
999
|
+
secure: sessionCookieIsSecure(request.protocol, request.headers, FORWARDED_PREFIX_HEADER),
|
|
1037
1000
|
});
|
|
1038
1001
|
reply.setCookie(c.name, c.value, c.options);
|
|
1039
1002
|
return reply.send({ ok: true });
|
|
@@ -1053,7 +1016,7 @@ async function bootstrap() {
|
|
|
1053
1016
|
if (!token)
|
|
1054
1017
|
return reply.status(401).send({ error: 'Bearer token required' });
|
|
1055
1018
|
const next = request.query?.next ?? '';
|
|
1056
|
-
if (!
|
|
1019
|
+
if (!isEmbedRedirectTarget(next))
|
|
1057
1020
|
return reply.status(400).send({ error: 'invalid next' });
|
|
1058
1021
|
let ttlSec;
|
|
1059
1022
|
try {
|
|
@@ -1064,8 +1027,8 @@ async function bootstrap() {
|
|
|
1064
1027
|
catch {
|
|
1065
1028
|
return reply.status(401).send({ error: 'invalid token' });
|
|
1066
1029
|
}
|
|
1067
|
-
const c =
|
|
1068
|
-
secure:
|
|
1030
|
+
const c = buildSessionCookie(token, ttlSec, {
|
|
1031
|
+
secure: sessionCookieIsSecure(request.protocol, request.headers, FORWARDED_PREFIX_HEADER),
|
|
1069
1032
|
});
|
|
1070
1033
|
reply.setCookie(c.name, c.value, c.options);
|
|
1071
1034
|
return reply.redirect(next);
|
|
@@ -1073,7 +1036,7 @@ async function bootstrap() {
|
|
|
1073
1036
|
// Every refusal on the addon gate is a request an operator will be asking
|
|
1074
1037
|
// about ("why did Home Assistant stop actuating?"). Its own scope so the
|
|
1075
1038
|
// reason is greppable rather than buried in the bootstrap logger.
|
|
1076
|
-
const addonGateLogger = app.get(
|
|
1039
|
+
const addonGateLogger = app.get(LoggingService).createLogger('addon-route-auth');
|
|
1077
1040
|
// Addon HTTP API route catch-all: /addon/:addonId/*
|
|
1078
1041
|
// Only handles non-GET or routes that actually exist in the addon route registry.
|
|
1079
1042
|
// GET requests that don't match an addon route are SPA pages — handled by the /* fallback.
|
|
@@ -1107,7 +1070,7 @@ async function bootstrap() {
|
|
|
1107
1070
|
let shareHeaders = null;
|
|
1108
1071
|
if (access !== 'public') {
|
|
1109
1072
|
const authHeader = request.headers.authorization;
|
|
1110
|
-
const cookieToken = request.cookies?.[
|
|
1073
|
+
const cookieToken = request.cookies?.[SESSION_COOKIE];
|
|
1111
1074
|
if (!authHeader && !cookieToken) {
|
|
1112
1075
|
return reply.status(401).send({ error: 'Unauthorized' });
|
|
1113
1076
|
}
|
|
@@ -1118,7 +1081,7 @@ async function bootstrap() {
|
|
|
1118
1081
|
// HUB sets — whatever the client sent under those names is dropped
|
|
1119
1082
|
// (see `addon-route-share-gate.ts`).
|
|
1120
1083
|
const record = await shareTokenService.validate(token);
|
|
1121
|
-
const gate =
|
|
1084
|
+
const gate = gateAddonShareToken({ record, endpoint: dpMatch.endpoint });
|
|
1122
1085
|
if (!gate.ok)
|
|
1123
1086
|
return reply.status(gate.status).send({ error: gate.error });
|
|
1124
1087
|
shareHeaders = gate.headers;
|
|
@@ -1141,7 +1104,7 @@ async function bootstrap() {
|
|
|
1141
1104
|
catch {
|
|
1142
1105
|
return reply.status(401).send({ error: 'Invalid token' });
|
|
1143
1106
|
}
|
|
1144
|
-
const gate =
|
|
1107
|
+
const gate = gateAddonJwt({ payload, addonId, method, access }, addonGateLogger);
|
|
1145
1108
|
if (!gate.ok) {
|
|
1146
1109
|
return reply.status(gate.status).send({ error: gate.error });
|
|
1147
1110
|
}
|
|
@@ -1156,8 +1119,8 @@ async function bootstrap() {
|
|
|
1156
1119
|
// The share headers are the HUB's to write: drop whatever the client
|
|
1157
1120
|
// sent under those names, then set the validated scope, if any.
|
|
1158
1121
|
for (const name of Object.keys(request.raw.headers)) {
|
|
1159
|
-
if (name.toLowerCase() ===
|
|
1160
|
-
name.toLowerCase() ===
|
|
1122
|
+
if (name.toLowerCase() === SHARE_KIND_HEADER ||
|
|
1123
|
+
name.toLowerCase() === SHARE_DEVICES_HEADER)
|
|
1161
1124
|
delete request.raw.headers[name];
|
|
1162
1125
|
}
|
|
1163
1126
|
if (shareHeaders !== null)
|
|
@@ -1169,7 +1132,7 @@ async function bootstrap() {
|
|
|
1169
1132
|
: Buffer.isBuffer(request.body)
|
|
1170
1133
|
? request.body
|
|
1171
1134
|
: Buffer.from(typeof request.body === 'string' ? request.body : JSON.stringify(request.body));
|
|
1172
|
-
|
|
1135
|
+
proxyToUpstream({
|
|
1173
1136
|
baseUrl: dpMatch.endpoint.baseUrl,
|
|
1174
1137
|
secret: dpMatch.endpoint.secret,
|
|
1175
1138
|
upstreamPath,
|
|
@@ -1181,7 +1144,7 @@ async function bootstrap() {
|
|
|
1181
1144
|
}
|
|
1182
1145
|
const match = addonRouteRegistry.matchRoute(method, fullPath);
|
|
1183
1146
|
if (!match) {
|
|
1184
|
-
if (
|
|
1147
|
+
if (isRetiredPublicPath(fullPath)) {
|
|
1185
1148
|
return reply.status(404).send({ error: 'Not found' });
|
|
1186
1149
|
}
|
|
1187
1150
|
if (method === 'GET' && spaIndexHtml) {
|
|
@@ -1190,14 +1153,14 @@ async function bootstrap() {
|
|
|
1190
1153
|
// send used to carry no cache header at all, so browsers kept the
|
|
1191
1154
|
// index.html of a previous build (and its dead hashed asset names)
|
|
1192
1155
|
// across deploys until a force refresh.
|
|
1193
|
-
return
|
|
1156
|
+
return sendSpaShell(reply, spaIndexHtml, request.headers);
|
|
1194
1157
|
}
|
|
1195
1158
|
return reply.status(404).send({ error: 'Not found' });
|
|
1196
1159
|
}
|
|
1197
1160
|
// A route that asked for it (an IdP start / callback) is limited per
|
|
1198
1161
|
// REAL client address before its handler runs.
|
|
1199
1162
|
if (match.route.rateLimit === 'auth') {
|
|
1200
|
-
const routeClient =
|
|
1163
|
+
const routeClient = clientKeyFromRequest(request, authNetwork);
|
|
1201
1164
|
if (routeClient !== null) {
|
|
1202
1165
|
const verdict = authRouteLimiter.check(`route:${addonId}:${routeClient}`);
|
|
1203
1166
|
if (!verdict.allowed) {
|
|
@@ -1211,11 +1174,11 @@ async function bootstrap() {
|
|
|
1211
1174
|
const authHeader = request.headers.authorization;
|
|
1212
1175
|
// Browser navigation has no Authorization header — fall back to the
|
|
1213
1176
|
// session cookie, and if that is missing bounce to the login page.
|
|
1214
|
-
const cookieToken = request.cookies?.[
|
|
1177
|
+
const cookieToken = request.cookies?.[SESSION_COOKIE];
|
|
1215
1178
|
if (!authHeader && !cookieToken) {
|
|
1216
|
-
if (
|
|
1179
|
+
if (shouldRedirectToLogin(request.method, request.headers.accept)) {
|
|
1217
1180
|
const qs = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
|
|
1218
|
-
return reply.redirect(
|
|
1181
|
+
return reply.redirect(loginRedirectUrl(fullPath + qs, parseForwardedPrefix(request.headers['x-forwarded-prefix'])));
|
|
1219
1182
|
}
|
|
1220
1183
|
return reply.status(401).send({ error: 'Unauthorized' });
|
|
1221
1184
|
}
|
|
@@ -1263,7 +1226,7 @@ async function bootstrap() {
|
|
|
1263
1226
|
}
|
|
1264
1227
|
// NOTE: the handler call used to sit INSIDE this try, so a throw from
|
|
1265
1228
|
// an addon's own route handler was reported as `401 Invalid token`.
|
|
1266
|
-
const gate =
|
|
1229
|
+
const gate = gateAddonJwt({ payload, addonId, method, access: match.route.access }, addonGateLogger);
|
|
1267
1230
|
if (!gate.ok) {
|
|
1268
1231
|
return reply.status(gate.status).send({ error: gate.error });
|
|
1269
1232
|
}
|
|
@@ -1297,17 +1260,17 @@ async function bootstrap() {
|
|
|
1297
1260
|
// namespace already excluded from the SPA catch-all everywhere.
|
|
1298
1261
|
// getRegistry is a closure so it always resolves the live registry at
|
|
1299
1262
|
// request time (safe even if called before ready()).
|
|
1300
|
-
|
|
1263
|
+
registerOauth2Routes(fastify, {
|
|
1301
1264
|
getRegistry: () => capabilityRegistry,
|
|
1302
1265
|
verifyToken: (t) => authService.verifyToken(t),
|
|
1303
1266
|
// The hub's public endpoint from the `local-network` cap (the flagged
|
|
1304
1267
|
// address, else tunnel, then mesh, then LAN). `localhost` only when the
|
|
1305
1268
|
// palette names nothing — the dev case, where it is the truth.
|
|
1306
|
-
publicHubUrl: () =>
|
|
1269
|
+
publicHubUrl: () => resolveHubBaseUrl(() => capabilityRegistry.getSingleton('local-network'), 'publicEndpoint').catch(() => `https://localhost:${port}`),
|
|
1307
1270
|
// Every /authorize refusal is a dropped account-linking attempt. Without
|
|
1308
1271
|
// this scope the drop is invisible in Loki and the only signal left is an
|
|
1309
1272
|
// operator noticing that linking stopped working.
|
|
1310
|
-
logger: app.get(
|
|
1273
|
+
logger: app.get(LoggingService).createLogger('oauth2'),
|
|
1311
1274
|
});
|
|
1312
1275
|
console.log('[bootstrap] OAuth2 routes registered at /api/oauth2/*');
|
|
1313
1276
|
// Attach tRPC WebSocket handler using noServer mode to avoid
|
|
@@ -1327,7 +1290,7 @@ async function bootstrap() {
|
|
|
1327
1290
|
// per viewer is exactly the kind of growth that is invisible until it is
|
|
1328
1291
|
// not. `threshold` leaves small frames (subscription ticks) alone, where
|
|
1329
1292
|
// framing overhead would exceed the saving.
|
|
1330
|
-
const wss = new
|
|
1293
|
+
const wss = new WebSocketServer({
|
|
1331
1294
|
noServer: true,
|
|
1332
1295
|
perMessageDeflate: {
|
|
1333
1296
|
threshold: 1024,
|
|
@@ -1352,19 +1315,19 @@ async function bootstrap() {
|
|
|
1352
1315
|
// in the background, JS suspended, TCP open) queued 88 MB of JSON strings
|
|
1353
1316
|
// in hub-main's old_space with no bound and no line. See
|
|
1354
1317
|
// `ws-slow-consumer-guard.ts` for the policy and the alternatives it beat.
|
|
1355
|
-
const wsSlowConsumerGuard = new
|
|
1356
|
-
logger: app.get(
|
|
1318
|
+
const wsSlowConsumerGuard = new WsSlowConsumerGuard({
|
|
1319
|
+
logger: app.get(LoggingService).createLogger('tRPC:ws'),
|
|
1357
1320
|
});
|
|
1358
1321
|
wsSlowConsumerGuard.start();
|
|
1359
1322
|
wss.on('connection', (client, req) => {
|
|
1360
|
-
|
|
1361
|
-
wsSlowConsumerGuard.attach(client,
|
|
1323
|
+
attachWsCensusSession(httpRequestCensus, client, req, app.get(LoggingService).createLogger('tRPC:ws'));
|
|
1324
|
+
wsSlowConsumerGuard.attach(client, wsConnectionFacts(req));
|
|
1362
1325
|
});
|
|
1363
1326
|
// Revoking a login session ends what it holds: its sockets (close 4401
|
|
1364
1327
|
// `session-ended`) and its live media. See `auth/session-connections.ts`.
|
|
1365
|
-
const sessionConnections =
|
|
1366
|
-
sessionConnections.setLogger(app.get(
|
|
1367
|
-
|
|
1328
|
+
const sessionConnections = getSessionConnections();
|
|
1329
|
+
sessionConnections.setLogger(app.get(LoggingService).createLogger('session-revocation'));
|
|
1330
|
+
onSessionsRevoked((event) => {
|
|
1368
1331
|
sessionConnections.endSessions({
|
|
1369
1332
|
sessionIds: event.sessionIds,
|
|
1370
1333
|
userId: event.userId,
|
|
@@ -1373,7 +1336,7 @@ async function bootstrap() {
|
|
|
1373
1336
|
reason: event.reason,
|
|
1374
1337
|
});
|
|
1375
1338
|
});
|
|
1376
|
-
|
|
1339
|
+
applyWSSHandler({
|
|
1377
1340
|
wss,
|
|
1378
1341
|
router: appRouter,
|
|
1379
1342
|
// The complement of the bound above, for the SUSPENDED client: a PING is
|
|
@@ -1386,22 +1349,22 @@ async function bootstrap() {
|
|
|
1386
1349
|
// A termination here closes with 1006; the guard's close line names it.
|
|
1387
1350
|
keepAlive: { enabled: true, pingMs: 30_000, pongWaitMs: 10_000 },
|
|
1388
1351
|
createContext: async (opts) => {
|
|
1389
|
-
const wsCtx = await
|
|
1352
|
+
const wsCtx = await createWsTrpcContext(opts, authService, addonRegistry, shareTokenService, app.get(LoggingService).createLogger('tRPC:ws'));
|
|
1390
1353
|
// The connection's identity, derived ONCE and reused for every
|
|
1391
1354
|
// operation on this socket. Frames that arrived while the bearer was
|
|
1392
1355
|
// still resolving are held by the session and released here.
|
|
1393
|
-
const principal =
|
|
1394
|
-
|
|
1356
|
+
const principal = describeTrpcPrincipal(wsCtx.user);
|
|
1357
|
+
identifyWsCensusSession(opts.res, principal);
|
|
1395
1358
|
wsSlowConsumerGuard.identify(opts.res, principal);
|
|
1396
|
-
const connectionLogger = app.get(
|
|
1359
|
+
const connectionLogger = app.get(LoggingService).createLogger('tRPC:ws');
|
|
1397
1360
|
if (wsCtx.credentialRejected === true) {
|
|
1398
1361
|
// A bearer that resolves to nobody (revoked, expired): say so with
|
|
1399
1362
|
// the close code the client recognises, instead of leaving an
|
|
1400
1363
|
// anonymous socket open for the client to poll.
|
|
1401
1364
|
connectionLogger.info('Closing a socket whose credential was refused', {
|
|
1402
|
-
meta: { principal, code:
|
|
1365
|
+
meta: { principal, code: SESSION_ENDED_CLOSE_CODE },
|
|
1403
1366
|
});
|
|
1404
|
-
opts.res.close(
|
|
1367
|
+
opts.res.close(SESSION_ENDED_CLOSE_CODE, SESSION_ENDED_CLOSE_REASON);
|
|
1405
1368
|
}
|
|
1406
1369
|
else if (wsCtx.user !== null) {
|
|
1407
1370
|
const untrack = sessionConnections.trackSocket(opts.res, {
|
|
@@ -1413,15 +1376,15 @@ async function bootstrap() {
|
|
|
1413
1376
|
return wsCtx;
|
|
1414
1377
|
},
|
|
1415
1378
|
onError: ({ path: trpcPath, error, ctx, input, }) => {
|
|
1416
|
-
const trpcLogger = app.get(
|
|
1379
|
+
const trpcLogger = app.get(LoggingService).createLogger('tRPC:ws');
|
|
1417
1380
|
trpcLogger.warn('tRPC error', {
|
|
1418
1381
|
// The socket the viewer uses — this is the transport that logged the
|
|
1419
1382
|
// four `intercom.startSession` refusals with no camera on them.
|
|
1420
|
-
tags:
|
|
1383
|
+
tags: trpcErrorDeviceTags(trpcPath, input),
|
|
1421
1384
|
meta: {
|
|
1422
1385
|
code: error.code,
|
|
1423
1386
|
path: trpcPath ?? '?',
|
|
1424
|
-
principal:
|
|
1387
|
+
principal: describeTrpcPrincipal(ctx?.user),
|
|
1425
1388
|
message: error.message,
|
|
1426
1389
|
},
|
|
1427
1390
|
});
|
|
@@ -1443,7 +1406,7 @@ async function bootstrap() {
|
|
|
1443
1406
|
const pathname = (request.url ?? '').split('?')[0];
|
|
1444
1407
|
if (pathname === '/trpc') {
|
|
1445
1408
|
// CORS does not cover a WebSocket: refuse a foreign page here (D707).
|
|
1446
|
-
if (
|
|
1409
|
+
if (refuseForeignWsUpgrade(corsPolicy, request.headers, socket))
|
|
1447
1410
|
return;
|
|
1448
1411
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
1449
1412
|
wss.emit('connection', ws, request);
|
|
@@ -1490,25 +1453,25 @@ async function bootstrap() {
|
|
|
1490
1453
|
// HUB-NODE-SCOPED resolution — never the cluster-global singleton. An agent
|
|
1491
1454
|
// node registers the same cap (agent-only placement) and its `getStaticDir()`
|
|
1492
1455
|
// answers with a path on the agent's filesystem.
|
|
1493
|
-
const capRegistry = app.get(
|
|
1456
|
+
const capRegistry = app.get(AddonRegistryService).getCapabilityRegistry();
|
|
1494
1457
|
return capRegistry?.getSingletonForNode(capName, 'hub') ?? null;
|
|
1495
1458
|
};
|
|
1496
|
-
const adminUi =
|
|
1459
|
+
const adminUi = createLazyUiServing({
|
|
1497
1460
|
label: 'Admin UI',
|
|
1498
1461
|
// CAMSTACK_SKIP_ADMIN_UI_WAIT: e2e runs that do not need the SPA.
|
|
1499
1462
|
lookup: () => process.env['CAMSTACK_SKIP_ADMIN_UI_WAIT'] === '1' ? null : hubUiProvider('admin-ui'),
|
|
1500
1463
|
log: uiLogger,
|
|
1501
1464
|
onServed: ({ staticDir, indexPath }) => {
|
|
1502
1465
|
spaIndexHtml = indexPath;
|
|
1503
|
-
|
|
1466
|
+
warmPrecompressedAssets(staticDir, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
|
|
1504
1467
|
},
|
|
1505
1468
|
});
|
|
1506
|
-
const viewerUi =
|
|
1469
|
+
const viewerUi = createLazyUiServing({
|
|
1507
1470
|
label: 'Viewer UI',
|
|
1508
1471
|
lookup: () => hubUiProvider('viewer-ui'),
|
|
1509
1472
|
log: uiLogger,
|
|
1510
1473
|
onServed: ({ staticDir }) => {
|
|
1511
|
-
|
|
1474
|
+
warmPrecompressedAssets(staticDir, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
|
|
1512
1475
|
},
|
|
1513
1476
|
});
|
|
1514
1477
|
const VIEWER_MOUNT = '/viewer/camstack';
|
|
@@ -1520,7 +1483,7 @@ async function bootstrap() {
|
|
|
1520
1483
|
// Unauthenticated by design — Apple's CDN and Android's verifier fetch these
|
|
1521
1484
|
// with no cookies; content-type must be application/json and no redirect.
|
|
1522
1485
|
{
|
|
1523
|
-
const { appleAppSiteAssociation, assetLinks } =
|
|
1486
|
+
const { appleAppSiteAssociation, assetLinks } = buildAppLinkFiles(parseCertFingerprints(process.env['CAMSTACK_ANDROID_CERT_SHA256']));
|
|
1524
1487
|
const aasa = JSON.stringify(appleAppSiteAssociation);
|
|
1525
1488
|
const assetlinks = JSON.stringify(assetLinks);
|
|
1526
1489
|
fastify.get('/.well-known/apple-app-site-association', async (_request, reply) => reply.type('application/json').send(aasa));
|
|
@@ -1549,18 +1512,18 @@ async function bootstrap() {
|
|
|
1549
1512
|
if (rel && /\.[a-zA-Z0-9]+$/.test(pathOnly)) {
|
|
1550
1513
|
const abs = path.join(staticDir, rel);
|
|
1551
1514
|
if ((abs === staticDir || abs.startsWith(staticDir + path.sep)) && fs.existsSync(abs)) {
|
|
1552
|
-
reply.header('cache-control',
|
|
1553
|
-
reply.type(
|
|
1515
|
+
reply.header('cache-control', spaAssetCacheControl(rel));
|
|
1516
|
+
reply.type(contentTypeForPath(rel));
|
|
1554
1517
|
return sendMaybePrecompressed(reply, abs, request.headers['accept-encoding']);
|
|
1555
1518
|
}
|
|
1556
1519
|
return reply.callNotFound();
|
|
1557
1520
|
}
|
|
1558
|
-
reply.header('cache-control',
|
|
1521
|
+
reply.header('cache-control', spaShellCacheControl());
|
|
1559
1522
|
return reply.type('text/html').send(fs.createReadStream(indexPath));
|
|
1560
1523
|
});
|
|
1561
1524
|
fastify.get('/*', async (request, reply) => {
|
|
1562
1525
|
const url = request.url;
|
|
1563
|
-
if (
|
|
1526
|
+
if (isRetiredPublicPath(url))
|
|
1564
1527
|
return reply.callNotFound();
|
|
1565
1528
|
if (url.startsWith('/trpc') ||
|
|
1566
1529
|
url.startsWith('/api/') ||
|
|
@@ -1582,14 +1545,14 @@ async function bootstrap() {
|
|
|
1582
1545
|
const rel = pathOnly.replace(/^\/+/, '');
|
|
1583
1546
|
const abs = path.join(staticDir, rel);
|
|
1584
1547
|
if ((abs === staticDir || abs.startsWith(staticDir + path.sep)) && fs.existsSync(abs)) {
|
|
1585
|
-
reply.header('cache-control',
|
|
1548
|
+
reply.header('cache-control', spaAssetCacheControl(rel));
|
|
1586
1549
|
return sendMaybePrecompressed(reply, abs, request.headers['accept-encoding']);
|
|
1587
1550
|
}
|
|
1588
1551
|
return reply.callNotFound();
|
|
1589
1552
|
}
|
|
1590
1553
|
// A relayed navigation (`X-Forwarded-Prefix`) gets a shell that says where
|
|
1591
1554
|
// it is mounted; without the header the shell is served as before.
|
|
1592
|
-
return
|
|
1555
|
+
return sendSpaShell(reply, indexPath, request.headers);
|
|
1593
1556
|
});
|
|
1594
1557
|
// Boot-time attempt, after listen. Later attempts are driven by requests.
|
|
1595
1558
|
const resolveAdminUi = async () => {
|
|
@@ -1610,12 +1573,12 @@ async function bootstrap() {
|
|
|
1610
1573
|
// fails to boot, hence the catch — and a restore that fails leaves the
|
|
1611
1574
|
// mirror at its default rather than inventing a level (D49).
|
|
1612
1575
|
try {
|
|
1613
|
-
const settingsStore =
|
|
1576
|
+
const settingsStore = buildSettingsStoreGateway(capabilityRegistry);
|
|
1614
1577
|
const loggingLogger = loggingService.createLogger('logging-settings');
|
|
1615
|
-
await new
|
|
1578
|
+
await new LoggingSettingsService({
|
|
1616
1579
|
store: settingsStore,
|
|
1617
|
-
gate:
|
|
1618
|
-
requestCensus: new
|
|
1580
|
+
gate: getLoggingGate(),
|
|
1581
|
+
requestCensus: new RequestCensusService({
|
|
1619
1582
|
census: httpRequestCensus,
|
|
1620
1583
|
store: settingsStore,
|
|
1621
1584
|
logger: loggingLogger,
|
|
@@ -1627,7 +1590,7 @@ async function bootstrap() {
|
|
|
1627
1590
|
console.warn('[bootstrap] logging settings restore skipped:', censusErr instanceof Error ? censusErr.message : censusErr);
|
|
1628
1591
|
}
|
|
1629
1592
|
try {
|
|
1630
|
-
const listenHost =
|
|
1593
|
+
const listenHost = allFamiliesListenHost(host).host;
|
|
1631
1594
|
try {
|
|
1632
1595
|
await app.listen(port, listenHost);
|
|
1633
1596
|
}
|
|
@@ -1650,15 +1613,15 @@ async function bootstrap() {
|
|
|
1650
1613
|
}
|
|
1651
1614
|
throw listenErr;
|
|
1652
1615
|
}
|
|
1653
|
-
const logger = app.get(
|
|
1616
|
+
const logger = app.get(LoggingService).createLogger('System');
|
|
1654
1617
|
const protocol = tlsOptions ? 'https' : 'http';
|
|
1655
1618
|
logger.info('CamStack server listening', { meta: { protocol, host, port, trpcRegistered } });
|
|
1656
1619
|
// Certificate actions (Network → Local access) swap the served certificate
|
|
1657
1620
|
// in place through this registration — without it an upload sat on disk
|
|
1658
1621
|
// while the listener kept serving the boot-time leaf (D726).
|
|
1659
1622
|
if (tlsOptions) {
|
|
1660
|
-
if (fastify.server instanceof
|
|
1661
|
-
|
|
1623
|
+
if (fastify.server instanceof TlsServer) {
|
|
1624
|
+
registerHttpsListener(fastify.server, tlsOptions);
|
|
1662
1625
|
}
|
|
1663
1626
|
else {
|
|
1664
1627
|
logger.warn('HTTPS listener is not a TLS server — certificate changes need a restart');
|
|
@@ -1667,7 +1630,7 @@ async function bootstrap() {
|
|
|
1667
1630
|
// LAN HTTP sibling — same Fastify request graph, dedicated port. Handler
|
|
1668
1631
|
// is registered as soon as we have the instance; bind happens after HTTPS
|
|
1669
1632
|
// is up so a failed HTTPS listen does not leave HTTP serving alone.
|
|
1670
|
-
|
|
1633
|
+
registerLanHttpHandler({
|
|
1671
1634
|
onRequest: (req, res) => {
|
|
1672
1635
|
fastify.server.emit('request', req, res);
|
|
1673
1636
|
},
|
|
@@ -1676,7 +1639,7 @@ async function bootstrap() {
|
|
|
1676
1639
|
},
|
|
1677
1640
|
});
|
|
1678
1641
|
try {
|
|
1679
|
-
const lanHttp = await
|
|
1642
|
+
const lanHttp = await bindPendingLanHttp();
|
|
1680
1643
|
if (lanHttp.listening) {
|
|
1681
1644
|
logger.info('LAN HTTP listening', { meta: { port: lanHttp.port } });
|
|
1682
1645
|
}
|
|
@@ -1693,17 +1656,17 @@ async function bootstrap() {
|
|
|
1693
1656
|
// state switches from 503 to static serving as soon as each capability is up.
|
|
1694
1657
|
void Promise.all([resolveAdminUi(), resolveViewerUi()]);
|
|
1695
1658
|
// Post-boot: fork workers, register device streams, emit system.boot
|
|
1696
|
-
const postBoot = app.get(
|
|
1659
|
+
const postBoot = app.get(PostBootService);
|
|
1697
1660
|
await postBoot.run({ port, host, dataPath, trpcRegistered, tlsCert });
|
|
1698
1661
|
// D877: ONE check, ~60 s after the boot sequence, of the runners this node
|
|
1699
1662
|
// planned against what its process service holds. Stored in memory and served
|
|
1700
1663
|
// on `getServerPackageStatus` / `/health`; not a timer loop.
|
|
1701
|
-
app.get(
|
|
1664
|
+
app.get(RunnerBootCheckService).scheduleAfterBoot();
|
|
1702
1665
|
// The hub reached ready — when this boot is the PROBATION boot of a freshly
|
|
1703
1666
|
// staged @camstack/server root, promote it (currentVersion ← pending, N-1
|
|
1704
1667
|
// retained for rollback) and disarm the starter's crash-loop auto-rollback.
|
|
1705
1668
|
try {
|
|
1706
|
-
const confirm = app.get(
|
|
1669
|
+
const confirm = app.get(ServerUpdateService).confirmBootHealthy();
|
|
1707
1670
|
if (confirm.promoted !== null) {
|
|
1708
1671
|
logger.info('Server root package update confirmed healthy', {
|
|
1709
1672
|
meta: { version: confirm.promoted },
|
|
@@ -1727,7 +1690,7 @@ async function bootstrap() {
|
|
|
1727
1690
|
// seeds them for exactly the boot that would otherwise fail. The agent runs
|
|
1728
1691
|
// the SAME function at its own confirmation point; neither role owns a
|
|
1729
1692
|
// private idea of what may be deleted.
|
|
1730
|
-
void
|
|
1693
|
+
void runSingleCopyCleanup({
|
|
1731
1694
|
dataDir: dataPath,
|
|
1732
1695
|
addonRoot: process.env['CAMSTACK_ADDONS_DIR'] ?? path.join(dataPath, 'addons'),
|
|
1733
1696
|
log: (line) => logger.info(line),
|
|
@@ -1743,7 +1706,7 @@ async function bootstrap() {
|
|
|
1743
1706
|
const listAll = dmForBackfill.listAll;
|
|
1744
1707
|
const setIntegrationId = dmForBackfill.setIntegrationId;
|
|
1745
1708
|
const backfillLogger = loggingService.createLogger('integration-backfill');
|
|
1746
|
-
await
|
|
1709
|
+
await runIntegrationIdBackfill({
|
|
1747
1710
|
listIntegrations: async () => (await integrationRegistry.listIntegrations()).map((i) => ({
|
|
1748
1711
|
id: i.id,
|
|
1749
1712
|
addonId: i.addonId,
|