@camstack/server 0.2.2 → 1.0.1
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/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
- package/dist/api/addon-upload.js +441 -0
- package/dist/api/addons-custom.router.js +91 -0
- package/dist/api/auth-whoami.js +55 -0
- package/dist/api/bridge-addons.router.js +109 -0
- package/dist/api/capabilities.router.js +229 -0
- package/dist/api/core/addon-settings.router.js +117 -0
- package/dist/api/core/agents.router.js +73 -0
- package/dist/api/core/auth.router.js +286 -0
- package/dist/api/core/bulk-update-coordinator.js +229 -0
- package/dist/api/core/cap-providers.js +1124 -0
- package/dist/api/core/capabilities.router.js +138 -0
- package/dist/api/core/collection-preference.js +17 -0
- package/dist/api/core/event-bus-proxy.router.js +45 -0
- package/dist/api/core/hwaccel.router.js +91 -0
- package/dist/api/core/live-events.router.js +61 -0
- package/dist/api/core/logs.router.js +172 -0
- package/dist/api/core/notifications.router.js +67 -0
- package/dist/api/core/repl.router.js +35 -0
- package/dist/api/core/settings-backend.router.js +121 -0
- package/dist/api/core/stream-probe.router.js +58 -0
- package/dist/api/core/system-events.router.js +100 -0
- package/dist/api/health/health.routes.js +68 -0
- package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
- package/dist/api/oauth2/oauth2-routes.js +219 -0
- package/dist/api/trpc/cap-mount-helpers.js +194 -0
- package/dist/api/trpc/cap-route-error-formatter.js +133 -0
- package/dist/api/trpc/client-ip.js +147 -0
- package/dist/api/trpc/core-cap-bridge.js +115 -0
- package/dist/api/trpc/generated-cap-mounts.js +388 -0
- package/dist/api/trpc/generated-cap-routers.js +7635 -0
- package/dist/api/trpc/scope-access.js +93 -0
- package/dist/api/trpc/trpc.context.js +184 -0
- package/dist/api/trpc/trpc.middleware.js +139 -0
- package/dist/api/trpc/trpc.router.js +188 -0
- package/dist/auth/session-cookie.js +47 -0
- package/dist/boot/boot-config.js +241 -0
- package/dist/boot/integration-id-backfill.js +76 -0
- package/dist/boot/post-boot.service.js +85 -0
- package/dist/core/addon/addon-call-gateway.js +99 -0
- package/dist/core/addon/addon-package.service.js +1560 -0
- package/dist/core/addon/addon-registry.service.js +2739 -0
- package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
- package/dist/core/addon/addon-search.service.js +62 -0
- package/dist/core/addon/addon-settings-provider.js +102 -0
- package/dist/core/addon/addon.tokens.js +5 -0
- package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
- package/dist/core/addon-pages/addon-pages.service.js +107 -0
- package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
- package/dist/core/agent/agent-registry.service.js +477 -0
- package/dist/core/auth/auth.service.js +10 -0
- package/dist/core/capability/capability.service.js +58 -0
- package/dist/core/config/config.schema.js +7 -0
- package/dist/core/config/config.service.js +10 -0
- package/dist/core/events/event-bus.service.js +83 -0
- package/dist/core/feature/feature.service.js +10 -0
- package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
- package/dist/core/logging/log-ring-buffer.js +6 -0
- package/dist/core/logging/logging.service.js +130 -0
- package/dist/core/logging/scoped-logger.js +6 -0
- package/dist/core/moleculer/cap-call-fn.js +50 -0
- package/dist/core/moleculer/cap-route-authority.js +122 -0
- package/dist/core/moleculer/moleculer.service.js +898 -0
- package/dist/core/network/network-quality.service.js +7 -0
- package/dist/core/notification/notification-wrapper.service.js +33 -0
- package/dist/core/notification/toast-wrapper.service.js +25 -0
- package/dist/core/provider/provider.tokens.js +4 -0
- package/dist/core/repl/repl-engine.service.js +140 -0
- package/dist/core/storage/fs-storage-backend.js +6 -0
- package/dist/core/storage/storage-location-manager.js +6 -0
- package/dist/core/storage/storage.service.js +7 -0
- package/dist/core/streaming/stream-probe.service.js +209 -0
- package/dist/core/topology/topology-emitter.service.js +106 -0
- package/dist/launcher.js +325 -0
- package/dist/main.js +1098 -0
- package/dist/manual-boot.js +227 -0
- package/package.json +5 -1
- package/src/__tests__/addon-install-e2e.test.ts +0 -74
- package/src/__tests__/addon-pages-e2e.test.ts +0 -200
- package/src/__tests__/addon-route-session.test.ts +0 -17
- package/src/__tests__/addon-settings-router.spec.ts +0 -67
- package/src/__tests__/addon-upload.spec.ts +0 -475
- package/src/__tests__/agent-registry.spec.ts +0 -179
- package/src/__tests__/agent-status-page.spec.ts +0 -82
- package/src/__tests__/auth-session-cookie.test.ts +0 -48
- package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
- package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
- package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
- package/src/__tests__/cap-route-adapter.spec.ts +0 -302
- package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
- package/src/__tests__/cap-routers/harness.ts +0 -163
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
- package/src/__tests__/capability-e2e.test.ts +0 -384
- package/src/__tests__/cli-e2e.test.ts +0 -150
- package/src/__tests__/core-cap-bridge.spec.ts +0 -91
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
- package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
- package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
- package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
- package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
- package/src/__tests__/framework-allowlist.spec.ts +0 -96
- package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
- package/src/__tests__/https-e2e.test.ts +0 -124
- package/src/__tests__/lifecycle-e2e.test.ts +0 -189
- package/src/__tests__/live-events-subscription.spec.ts +0 -149
- package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
- package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
- package/src/__tests__/native-cap-route.spec.ts +0 -427
- package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
- package/src/__tests__/post-boot-restart.spec.ts +0 -161
- package/src/__tests__/singleton-contention.test.ts +0 -499
- package/src/__tests__/streaming-diagnostic.test.ts +0 -615
- package/src/__tests__/streaming-scale.test.ts +0 -314
- package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
- package/src/__tests__/uds-log-ingest.spec.ts +0 -183
- package/src/api/__tests__/addons-custom.spec.ts +0 -148
- package/src/api/__tests__/capabilities.router.test.ts +0 -56
- package/src/api/addon-upload.ts +0 -529
- package/src/api/addons-custom.router.ts +0 -101
- package/src/api/auth-whoami.ts +0 -101
- package/src/api/bridge-addons.router.ts +0 -122
- package/src/api/capabilities.router.ts +0 -265
- package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
- package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
- package/src/api/core/addon-settings.router.ts +0 -127
- package/src/api/core/agents.router.ts +0 -86
- package/src/api/core/auth.router.ts +0 -322
- package/src/api/core/bulk-update-coordinator.ts +0 -305
- package/src/api/core/cap-providers.ts +0 -1339
- package/src/api/core/capabilities.router.ts +0 -149
- package/src/api/core/collection-preference.ts +0 -40
- package/src/api/core/event-bus-proxy.router.ts +0 -45
- package/src/api/core/hwaccel.router.ts +0 -108
- package/src/api/core/live-events.router.ts +0 -67
- package/src/api/core/logs.router.ts +0 -195
- package/src/api/core/notifications.router.ts +0 -66
- package/src/api/core/repl.router.ts +0 -39
- package/src/api/core/settings-backend.router.ts +0 -140
- package/src/api/core/stream-probe.router.ts +0 -57
- package/src/api/core/system-events.router.ts +0 -125
- package/src/api/health/health.routes.ts +0 -117
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
- package/src/api/oauth2/oauth2-routes.ts +0 -281
- package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
- package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
- package/src/api/trpc/cap-mount-helpers.ts +0 -245
- package/src/api/trpc/cap-route-error-formatter.ts +0 -171
- package/src/api/trpc/client-ip.ts +0 -147
- package/src/api/trpc/core-cap-bridge.ts +0 -154
- package/src/api/trpc/generated-cap-mounts.ts +0 -1240
- package/src/api/trpc/generated-cap-routers.ts +0 -11523
- package/src/api/trpc/scope-access.ts +0 -110
- package/src/api/trpc/trpc.context.ts +0 -258
- package/src/api/trpc/trpc.middleware.ts +0 -146
- package/src/api/trpc/trpc.router.ts +0 -389
- package/src/auth/session-cookie.ts +0 -54
- package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
- package/src/boot/boot-config.ts +0 -259
- package/src/boot/integration-id-backfill.ts +0 -109
- package/src/boot/post-boot.service.ts +0 -105
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
- package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
- package/src/core/addon/addon-call-gateway.ts +0 -171
- package/src/core/addon/addon-package.service.ts +0 -1787
- package/src/core/addon/addon-registry.service.ts +0 -3130
- package/src/core/addon/addon-search.service.ts +0 -91
- package/src/core/addon/addon-settings-provider.ts +0 -220
- package/src/core/addon/addon.tokens.ts +0 -2
- package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
- package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
- package/src/core/addon-pages/addon-pages.service.ts +0 -82
- package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
- package/src/core/agent/agent-registry.service.ts +0 -529
- package/src/core/auth/auth.service.spec.ts +0 -86
- package/src/core/auth/auth.service.ts +0 -8
- package/src/core/capability/capability.service.ts +0 -66
- package/src/core/config/config.schema.ts +0 -3
- package/src/core/config/config.service.spec.ts +0 -175
- package/src/core/config/config.service.ts +0 -7
- package/src/core/events/event-bus.service.spec.ts +0 -235
- package/src/core/events/event-bus.service.ts +0 -89
- package/src/core/feature/feature.service.spec.ts +0 -99
- package/src/core/feature/feature.service.ts +0 -8
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
- package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
- package/src/core/logging/log-ring-buffer.ts +0 -3
- package/src/core/logging/logging.service.spec.ts +0 -287
- package/src/core/logging/logging.service.ts +0 -143
- package/src/core/logging/scoped-logger.ts +0 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
- package/src/core/moleculer/cap-call-fn.ts +0 -107
- package/src/core/moleculer/cap-route-authority.ts +0 -194
- package/src/core/moleculer/moleculer.service.ts +0 -1072
- package/src/core/network/network-quality.service.spec.ts +0 -53
- package/src/core/network/network-quality.service.ts +0 -5
- package/src/core/notification/notification-wrapper.service.ts +0 -34
- package/src/core/notification/toast-wrapper.service.ts +0 -27
- package/src/core/provider/provider.tokens.ts +0 -1
- package/src/core/repl/repl-engine.service.spec.ts +0 -444
- package/src/core/repl/repl-engine.service.ts +0 -155
- package/src/core/storage/fs-storage-backend.spec.ts +0 -70
- package/src/core/storage/fs-storage-backend.ts +0 -3
- package/src/core/storage/storage-location-manager.spec.ts +0 -130
- package/src/core/storage/storage-location-manager.ts +0 -3
- package/src/core/storage/storage.service.spec.ts +0 -73
- package/src/core/storage/storage.service.ts +0 -3
- package/src/core/streaming/stream-probe.service.ts +0 -221
- package/src/core/topology/topology-emitter.service.ts +0 -105
- package/src/launcher.ts +0 -314
- package/src/main.ts +0 -1245
- package/src/manual-boot.ts +0 -301
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -33
- package/vitest.config.ts +0 -26
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSettingsBackendRouter = createSettingsBackendRouter;
|
|
4
|
+
/**
|
|
5
|
+
* Settings backend router — tRPC proxy for ISettingsBackend operations.
|
|
6
|
+
*
|
|
7
|
+
* Exposes the core collection-based operations (get, set, query, insert,
|
|
8
|
+
* update, delete, count, isEmpty) so forked worker addons can use
|
|
9
|
+
* context.settingsBackend via tRPC instead of requiring in-process access
|
|
10
|
+
* to the SQLite database.
|
|
11
|
+
*
|
|
12
|
+
* Introduced for Task 11 — TrpcSettingsBackend for forked workers.
|
|
13
|
+
*/
|
|
14
|
+
const zod_1 = require("zod");
|
|
15
|
+
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Zod schemas
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const CollectionKeySchema = zod_1.z.object({
|
|
20
|
+
collection: zod_1.z.string(),
|
|
21
|
+
key: zod_1.z.string(),
|
|
22
|
+
});
|
|
23
|
+
const SetValueSchema = zod_1.z.object({
|
|
24
|
+
collection: zod_1.z.string(),
|
|
25
|
+
key: zod_1.z.string(),
|
|
26
|
+
value: zod_1.z.unknown(),
|
|
27
|
+
});
|
|
28
|
+
const QueryFilterSchema = zod_1.z
|
|
29
|
+
.object({
|
|
30
|
+
where: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
31
|
+
whereIn: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.unknown())).optional(),
|
|
32
|
+
whereBetween: zod_1.z.record(zod_1.z.string(), zod_1.z.tuple([zod_1.z.unknown(), zod_1.z.unknown()])).optional(),
|
|
33
|
+
orderBy: zod_1.z
|
|
34
|
+
.object({
|
|
35
|
+
field: zod_1.z.string(),
|
|
36
|
+
direction: zod_1.z.enum(['asc', 'desc']),
|
|
37
|
+
})
|
|
38
|
+
.optional(),
|
|
39
|
+
limit: zod_1.z.number().optional(),
|
|
40
|
+
offset: zod_1.z.number().optional(),
|
|
41
|
+
})
|
|
42
|
+
.optional();
|
|
43
|
+
const QueryInputSchema = zod_1.z.object({
|
|
44
|
+
collection: zod_1.z.string(),
|
|
45
|
+
filter: QueryFilterSchema,
|
|
46
|
+
});
|
|
47
|
+
const InsertInputSchema = zod_1.z.object({
|
|
48
|
+
collection: zod_1.z.string(),
|
|
49
|
+
record: zod_1.z.object({
|
|
50
|
+
id: zod_1.z.string(),
|
|
51
|
+
data: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
const UpdateInputSchema = zod_1.z.object({
|
|
55
|
+
collection: zod_1.z.string(),
|
|
56
|
+
id: zod_1.z.string(),
|
|
57
|
+
data: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
|
|
58
|
+
});
|
|
59
|
+
const CountInputSchema = zod_1.z.object({
|
|
60
|
+
collection: zod_1.z.string(),
|
|
61
|
+
filter: QueryFilterSchema,
|
|
62
|
+
});
|
|
63
|
+
const IsEmptyInputSchema = zod_1.z.object({
|
|
64
|
+
collection: zod_1.z.string(),
|
|
65
|
+
});
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Router factory
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
function createSettingsBackendRouter(getBackend) {
|
|
70
|
+
const requireBackend = () => {
|
|
71
|
+
const backend = getBackend();
|
|
72
|
+
if (!backend) {
|
|
73
|
+
throw new Error('Settings backend not available — settings-store addon may not be initialized yet');
|
|
74
|
+
}
|
|
75
|
+
return backend;
|
|
76
|
+
};
|
|
77
|
+
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
78
|
+
get: trpc_middleware_js_1.protectedProcedure.input(CollectionKeySchema).query(async ({ input }) => {
|
|
79
|
+
const result = await requireBackend().get(input);
|
|
80
|
+
return { value: result };
|
|
81
|
+
}),
|
|
82
|
+
set: trpc_middleware_js_1.protectedProcedure.input(SetValueSchema).mutation(async ({ input }) => {
|
|
83
|
+
await requireBackend().set({
|
|
84
|
+
collection: input.collection,
|
|
85
|
+
key: input.key,
|
|
86
|
+
value: input.value,
|
|
87
|
+
});
|
|
88
|
+
return { success: true };
|
|
89
|
+
}),
|
|
90
|
+
query: trpc_middleware_js_1.protectedProcedure.input(QueryInputSchema).query(async ({ input }) => {
|
|
91
|
+
const records = await requireBackend().query({
|
|
92
|
+
collection: input.collection,
|
|
93
|
+
filter: input.filter ?? undefined,
|
|
94
|
+
});
|
|
95
|
+
return { records: records.map((r) => ({ id: r.id, data: r.data })) };
|
|
96
|
+
}),
|
|
97
|
+
insert: trpc_middleware_js_1.protectedProcedure.input(InsertInputSchema).mutation(async ({ input }) => {
|
|
98
|
+
await requireBackend().insert(input);
|
|
99
|
+
return { success: true };
|
|
100
|
+
}),
|
|
101
|
+
update: trpc_middleware_js_1.protectedProcedure.input(UpdateInputSchema).mutation(async ({ input }) => {
|
|
102
|
+
await requireBackend().update(input);
|
|
103
|
+
return { success: true };
|
|
104
|
+
}),
|
|
105
|
+
delete: trpc_middleware_js_1.protectedProcedure.input(CollectionKeySchema).mutation(async ({ input }) => {
|
|
106
|
+
await requireBackend().delete(input);
|
|
107
|
+
return { success: true };
|
|
108
|
+
}),
|
|
109
|
+
count: trpc_middleware_js_1.protectedProcedure.input(CountInputSchema).query(async ({ input }) => {
|
|
110
|
+
const result = await requireBackend().count({
|
|
111
|
+
collection: input.collection,
|
|
112
|
+
filter: input.filter ?? undefined,
|
|
113
|
+
});
|
|
114
|
+
return { count: result };
|
|
115
|
+
}),
|
|
116
|
+
isEmpty: trpc_middleware_js_1.protectedProcedure.input(IsEmptyInputSchema).query(async ({ input }) => {
|
|
117
|
+
const result = await requireBackend().isEmpty(input);
|
|
118
|
+
return { empty: result };
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStreamProbeRouter = createStreamProbeRouter;
|
|
4
|
+
/**
|
|
5
|
+
* Stream probe router — fixed core API (not a capability).
|
|
6
|
+
*
|
|
7
|
+
* Thin wrapper over `StreamProbeService` (backend REAL_LOGIC: ffprobe +
|
|
8
|
+
* HTTP probes + 1h cache). Not pluggable: there is exactly one stream
|
|
9
|
+
* probe implementation shipping with the server.
|
|
10
|
+
*
|
|
11
|
+
* Previously these endpoints lived under `streaming.probeStream` — moved
|
|
12
|
+
* here as part of eliminating the `streaming` aggregator capability.
|
|
13
|
+
*/
|
|
14
|
+
const zod_1 = require("zod");
|
|
15
|
+
const types_1 = require("@camstack/types");
|
|
16
|
+
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
17
|
+
const ProbedStreamSchema = zod_1.z.object({
|
|
18
|
+
width: zod_1.z.number().optional(),
|
|
19
|
+
height: zod_1.z.number().optional(),
|
|
20
|
+
codec: zod_1.z.string().optional(),
|
|
21
|
+
fps: zod_1.z.number().optional(),
|
|
22
|
+
bitrateKbps: zod_1.z.number().optional(),
|
|
23
|
+
quality: zod_1.z.enum(['high', 'mid', 'low']),
|
|
24
|
+
});
|
|
25
|
+
const FieldProbeResultSchema = zod_1.z.object({
|
|
26
|
+
status: zod_1.z.enum(['ok', 'error']),
|
|
27
|
+
labels: zod_1.z.array(zod_1.z.string()).optional(),
|
|
28
|
+
error: zod_1.z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
function createStreamProbeRouter(sp) {
|
|
31
|
+
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
32
|
+
probe: trpc_middleware_js_1.adminProcedure
|
|
33
|
+
.input(zod_1.z.object({ url: zod_1.z.string(), force: zod_1.z.boolean().optional() }))
|
|
34
|
+
.output(ProbedStreamSchema)
|
|
35
|
+
.mutation(async ({ input }) => {
|
|
36
|
+
if (!sp)
|
|
37
|
+
throw new Error('StreamProbeService not available');
|
|
38
|
+
const metadata = await sp.probe(input.url, { force: input.force });
|
|
39
|
+
return { ...metadata, quality: (0, types_1.classifyStream)(metadata) };
|
|
40
|
+
}),
|
|
41
|
+
/**
|
|
42
|
+
* Generic field probe — decides stream vs HTTP reachability based on
|
|
43
|
+
* the field key (keys starting with `stream` trigger ffprobe;
|
|
44
|
+
* everything else falls back to an HTTP GET that aborts at headers).
|
|
45
|
+
* Wraps `StreamProbeService.probeField` — used by device providers
|
|
46
|
+
* that need the kernel's probe without re-implementing ffprobe /
|
|
47
|
+
* HTTP fetch in every addon.
|
|
48
|
+
*/
|
|
49
|
+
probeField: trpc_middleware_js_1.adminProcedure
|
|
50
|
+
.input(zod_1.z.object({ key: zod_1.z.string(), value: zod_1.z.unknown() }))
|
|
51
|
+
.output(FieldProbeResultSchema)
|
|
52
|
+
.mutation(async ({ input }) => {
|
|
53
|
+
if (!sp)
|
|
54
|
+
throw new Error('StreamProbeService not available');
|
|
55
|
+
return sp.probeField(input.key, input.value);
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSystemEventsRouter = createSystemEventsRouter;
|
|
4
|
+
/**
|
|
5
|
+
* System events router — fixed core API (not a capability).
|
|
6
|
+
*
|
|
7
|
+
* Exposes recent-events query and live subscribe over the `EventBusService`.
|
|
8
|
+
* Supports hierarchical filtering via EventFilter: agentId → addonId → deviceId.
|
|
9
|
+
* All filters are applied server-side by the SystemEventBus — the client
|
|
10
|
+
* only receives matching events.
|
|
11
|
+
*/
|
|
12
|
+
const zod_1 = require("zod");
|
|
13
|
+
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
14
|
+
function serialize(e) {
|
|
15
|
+
return {
|
|
16
|
+
id: e.id,
|
|
17
|
+
timestamp: new Date(e.timestamp).toISOString(),
|
|
18
|
+
source: {
|
|
19
|
+
type: e.source.type,
|
|
20
|
+
id: e.source.id,
|
|
21
|
+
...(e.source.nodeId ? { nodeId: e.source.nodeId } : {}),
|
|
22
|
+
...(e.source.addonId ? { addonId: e.source.addonId } : {}),
|
|
23
|
+
...(e.source.deviceId !== undefined ? { deviceId: e.source.deviceId } : {}),
|
|
24
|
+
},
|
|
25
|
+
category: e.category,
|
|
26
|
+
data: e.data,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Source / agent / addon / device fields shared by query + subscribe.
|
|
31
|
+
* Category handling is split (asymmetric): see the per-method schemas
|
|
32
|
+
* below.
|
|
33
|
+
*/
|
|
34
|
+
const ScopeFieldsSchema = zod_1.z.object({
|
|
35
|
+
/** Legacy source filter (exact type+id match). */
|
|
36
|
+
source: zod_1.z
|
|
37
|
+
.object({
|
|
38
|
+
type: zod_1.z.string(),
|
|
39
|
+
id: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]),
|
|
40
|
+
})
|
|
41
|
+
.optional(),
|
|
42
|
+
/** Agent/node filter (prefix match: 'hub' matches 'hub/pipeline'). */
|
|
43
|
+
agentId: zod_1.z.string().optional(),
|
|
44
|
+
/** Addon filter. Matches source.addonId or source.id when type='addon'. */
|
|
45
|
+
addonId: zod_1.z.string().optional(),
|
|
46
|
+
/** Device filter. Matches source.deviceId or source.id when type='device'. */
|
|
47
|
+
deviceId: zod_1.z.number().optional(),
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* `getRecent` accepts a category array so the UI can drive a
|
|
51
|
+
* server-side whitelist when reading the historical buffer. Without
|
|
52
|
+
* this, getRecent returns the last `limit` events of any category and
|
|
53
|
+
* a noisy category (per-frame metrics) can displace relevant events
|
|
54
|
+
* (provider.motion, detection.result) out of the window — which was
|
|
55
|
+
* the symptom of "no events visible after page refresh".
|
|
56
|
+
*
|
|
57
|
+
* The underlying ring-buffer filter
|
|
58
|
+
* (`addon-context-factory.ts:getRecent`) already iterates the
|
|
59
|
+
* `category` field as `string | string[]`.
|
|
60
|
+
*/
|
|
61
|
+
const GetRecentInputSchema = ScopeFieldsSchema.extend({
|
|
62
|
+
category: zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional(),
|
|
63
|
+
limit: zod_1.z.number().int().min(1).max(500).optional(),
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* `subscribe` keeps category as a single string. The bus's
|
|
67
|
+
* `extractCategoryPattern` keys handlers by a single pattern; passing
|
|
68
|
+
* an array would silently route only the first category. UIs that
|
|
69
|
+
* need multi-category live filtering must subscribe by deviceId
|
|
70
|
+
* (single-category-per-call or wildcard) and narrow client-side.
|
|
71
|
+
*/
|
|
72
|
+
const SubscribeInputSchema = ScopeFieldsSchema.extend({
|
|
73
|
+
category: zod_1.z.string().optional(),
|
|
74
|
+
});
|
|
75
|
+
function createSystemEventsRouter(eb) {
|
|
76
|
+
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
77
|
+
getRecent: trpc_middleware_js_1.protectedProcedure.input(GetRecentInputSchema).query(({ input }) => {
|
|
78
|
+
return eb
|
|
79
|
+
.getRecent({
|
|
80
|
+
...(input.source ? { source: input.source } : {}),
|
|
81
|
+
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
82
|
+
...(input.addonId ? { addonId: input.addonId } : {}),
|
|
83
|
+
...(input.deviceId !== undefined ? { deviceId: input.deviceId } : {}),
|
|
84
|
+
...(input.category ? { category: input.category } : {}),
|
|
85
|
+
}, input.limit)
|
|
86
|
+
.map(serialize);
|
|
87
|
+
}),
|
|
88
|
+
subscribe: trpc_middleware_js_1.protectedProcedure.input(SubscribeInputSchema).subscription(({ input }) => {
|
|
89
|
+
return (0, trpc_middleware_js_1.iterableSubscription)((push) => {
|
|
90
|
+
return eb.subscribe({
|
|
91
|
+
...(input.source ? { source: input.source } : {}),
|
|
92
|
+
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
93
|
+
...(input.addonId ? { addonId: input.addonId } : {}),
|
|
94
|
+
...(input.deviceId !== undefined ? { deviceId: input.deviceId } : {}),
|
|
95
|
+
...(input.category ? { category: input.category } : {}),
|
|
96
|
+
}, (event) => push(serialize(event)));
|
|
97
|
+
});
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerHealthRoutes = registerHealthRoutes;
|
|
4
|
+
const AGENT_HEALTH_TIMEOUT_MS = 3_000;
|
|
5
|
+
function nowIso() {
|
|
6
|
+
return new Date().toISOString();
|
|
7
|
+
}
|
|
8
|
+
async function buildHubHealth(deps, proc = process) {
|
|
9
|
+
const nodes = await deps.agentRegistry.listNodes();
|
|
10
|
+
const remote = nodes.filter((n) => !n.isHub);
|
|
11
|
+
const online = remote.filter((n) => n.isOnline !== false).length;
|
|
12
|
+
const total = remote.length;
|
|
13
|
+
const memUsage = proc.memoryUsage();
|
|
14
|
+
const totalMem = memUsage.heapTotal + memUsage.external + memUsage.arrayBuffers;
|
|
15
|
+
const memoryPercent = totalMem > 0 ? Math.round((memUsage.heapUsed / totalMem) * 100) : 0;
|
|
16
|
+
return {
|
|
17
|
+
ok: true,
|
|
18
|
+
nodeId: 'hub',
|
|
19
|
+
version: deps.hubVersion,
|
|
20
|
+
uptimeSeconds: Math.round(proc.uptime()),
|
|
21
|
+
pid: proc.pid,
|
|
22
|
+
agents: { total, online, offline: total - online },
|
|
23
|
+
cpuPercent: 0,
|
|
24
|
+
memoryPercent,
|
|
25
|
+
checkedAt: nowIso(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function fetchAgentHealth(deps, nodeId) {
|
|
29
|
+
try {
|
|
30
|
+
const result = (await deps.moleculer.broker.call('$agent.health', {}, { nodeID: nodeId, timeout: AGENT_HEALTH_TIMEOUT_MS }));
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
nodeId,
|
|
37
|
+
error: err instanceof Error ? err.message : String(err),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function registerHealthRoutes(fastify, deps) {
|
|
42
|
+
fastify.get('/health', async () => buildHubHealth(deps));
|
|
43
|
+
fastify.get('/health/agents', async () => {
|
|
44
|
+
const nodes = await deps.agentRegistry.listNodes();
|
|
45
|
+
return {
|
|
46
|
+
agents: nodes.filter((n) => !n.isHub && n.isOnline !== false).map((n) => n.info.id),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
fastify.get('/health/agents/:nodeId', async (req, reply) => {
|
|
50
|
+
const { nodeId } = req.params;
|
|
51
|
+
if (!nodeId) {
|
|
52
|
+
return reply.status(400).send({ ok: false, error: 'nodeId required' });
|
|
53
|
+
}
|
|
54
|
+
const result = await fetchAgentHealth(deps, nodeId);
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
return reply.status(503).send(result);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
});
|
|
60
|
+
fastify.get('/health/cluster', async () => {
|
|
61
|
+
const hub = await buildHubHealth(deps);
|
|
62
|
+
const nodes = await deps.agentRegistry.listNodes();
|
|
63
|
+
const remote = nodes.filter((n) => !n.isHub && n.isOnline !== false);
|
|
64
|
+
const agents = await Promise.all(remote.map((n) => fetchAgentHealth(deps, n.info.id)));
|
|
65
|
+
const ok = hub.ok && agents.every((a) => a.ok);
|
|
66
|
+
return { ok, hub, agents, checkedAt: nowIso() };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderConsentPage = renderConsentPage;
|
|
4
|
+
function escapeHtml(s) {
|
|
5
|
+
return s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
interface ConsentPageInput {
|
|
9
|
-
displayName: string
|
|
10
|
-
username: string
|
|
11
|
-
scopeSummary: string
|
|
12
|
-
/** Hidden form fields replayed on POST. */
|
|
13
|
-
hidden: Record<string, string>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
7
|
/** Allow/Deny consent screen. Submitting POSTs to the same path with the
|
|
17
8
|
* hidden query fields + `consent=allow|deny`. */
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
function renderConsentPage(input) {
|
|
10
|
+
const hidden = Object.entries(input.hidden)
|
|
11
|
+
.map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`)
|
|
12
|
+
.join('\n ');
|
|
13
|
+
return `<!doctype html>
|
|
23
14
|
<html lang="en"><head><meta charset="utf-8">
|
|
24
15
|
<title>CamStack · Authorize ${escapeHtml(input.displayName)}</title>
|
|
25
16
|
<style>
|
|
@@ -39,5 +30,5 @@ export function renderConsentPage(input: ConsentPageInput): string {
|
|
|
39
30
|
<button class="deny" name="consent" value="deny" type="submit">Deny</button>
|
|
40
31
|
</form>
|
|
41
32
|
</div>
|
|
42
|
-
</body></html
|
|
33
|
+
</body></html>`;
|
|
43
34
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateAuthorizeQuery = validateAuthorizeQuery;
|
|
4
|
+
exports.isRedirectUriAllowed = isRedirectUriAllowed;
|
|
5
|
+
exports.summariseScopes = summariseScopes;
|
|
6
|
+
exports.registerOauth2Routes = registerOauth2Routes;
|
|
7
|
+
const consent_page_js_1 = require("./consent-page.js");
|
|
8
|
+
const session_cookie_js_1 = require("../../auth/session-cookie.js");
|
|
9
|
+
/** Validate the inbound authorize query. `client_id` is intentionally
|
|
10
|
+
* NOT checked — that pair is verified only at the Lambda boundary. */
|
|
11
|
+
function validateAuthorizeQuery(q, knownIntegrations) {
|
|
12
|
+
if (q.response_type !== 'code')
|
|
13
|
+
return { ok: false, status: 400, error: 'unsupported_response_type' };
|
|
14
|
+
if (!q.integration || !knownIntegrations.has(q.integration))
|
|
15
|
+
return { ok: false, status: 400, error: 'invalid_request — unknown integration' };
|
|
16
|
+
if (!q.redirect_uri)
|
|
17
|
+
return { ok: false, status: 400, error: 'invalid_request — redirect_uri required' };
|
|
18
|
+
if (!q.state)
|
|
19
|
+
return { ok: false, status: 400, error: 'invalid_request — state required' };
|
|
20
|
+
return { ok: true, integration: q.integration, redirectUri: q.redirect_uri, state: q.state };
|
|
21
|
+
}
|
|
22
|
+
/** True if `redirectUri` starts with one of the integration's allowed prefixes. */
|
|
23
|
+
function isRedirectUriAllowed(redirectUri, allowedPrefixes) {
|
|
24
|
+
return allowedPrefixes.some((p) => redirectUri.startsWith(p));
|
|
25
|
+
}
|
|
26
|
+
/** One-line human summary of a scope list for the consent screen. */
|
|
27
|
+
function summariseScopes(scopes) {
|
|
28
|
+
if (scopes.some((s) => s.type === 'category' && s.target === 'device')) {
|
|
29
|
+
return 'view and control all your cameras and devices';
|
|
30
|
+
}
|
|
31
|
+
return scopes.map((s) => s.type).join(', ') || 'no permissions';
|
|
32
|
+
}
|
|
33
|
+
/** Build a map of integrationId → descriptor from all registered oauth-integration providers. */
|
|
34
|
+
async function buildIntegrationMap(registry) {
|
|
35
|
+
const entries = registry.getCollectionEntries('oauth-integration');
|
|
36
|
+
const descriptorMap = new Map();
|
|
37
|
+
for (const [, provider] of entries) {
|
|
38
|
+
const descriptor = await provider.getDescriptor();
|
|
39
|
+
descriptorMap.set(descriptor.integrationId, descriptor);
|
|
40
|
+
}
|
|
41
|
+
const knownSet = new Set(descriptorMap.keys());
|
|
42
|
+
return { descriptorMap, knownSet };
|
|
43
|
+
}
|
|
44
|
+
/** Parse an application/x-www-form-urlencoded body string into a plain object. */
|
|
45
|
+
function parseFormBody(raw) {
|
|
46
|
+
const params = new URLSearchParams(raw);
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const [key, value] of params.entries()) {
|
|
49
|
+
result[key] = value;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
function registerOauth2Routes(fastify, deps) {
|
|
54
|
+
// Register a content-type parser for application/x-www-form-urlencoded so that
|
|
55
|
+
// consent POST and token POST can read form bodies. @fastify/formbody is not in
|
|
56
|
+
// the project's dependencies; we use URLSearchParams (Node built-in) instead.
|
|
57
|
+
fastify.addContentTypeParser('application/x-www-form-urlencoded', { parseAs: 'string' }, (_req, body, done) => {
|
|
58
|
+
try {
|
|
59
|
+
done(null, parseFormBody(body));
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
done(err, undefined);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// ─── GET /api/oauth2/authorize ────────────────────────────────────────────
|
|
66
|
+
// Mounted under /api/* so it is naturally excluded from the SPA catch-all,
|
|
67
|
+
// the PWA service-worker navigate fallback, and the Vite dev proxy — no
|
|
68
|
+
// per-path special-casing anywhere.
|
|
69
|
+
fastify.get('/api/oauth2/authorize', async (request, reply) => {
|
|
70
|
+
const cookie = request.cookies[session_cookie_js_1.SESSION_COOKIE];
|
|
71
|
+
if (!cookie) {
|
|
72
|
+
if ((0, session_cookie_js_1.shouldRedirectToLogin)(request.method, request.headers.accept)) {
|
|
73
|
+
return reply.redirect((0, session_cookie_js_1.loginRedirectUrl)(request.url));
|
|
74
|
+
}
|
|
75
|
+
return reply.status(401).send({ error: 'unauthorized' });
|
|
76
|
+
}
|
|
77
|
+
let tokenInfo;
|
|
78
|
+
try {
|
|
79
|
+
tokenInfo = deps.verifyToken(cookie);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
if ((0, session_cookie_js_1.shouldRedirectToLogin)(request.method, request.headers.accept)) {
|
|
83
|
+
return reply.redirect((0, session_cookie_js_1.loginRedirectUrl)(request.url));
|
|
84
|
+
}
|
|
85
|
+
return reply.status(401).send({ error: 'unauthorized' });
|
|
86
|
+
}
|
|
87
|
+
const registry = deps.getRegistry();
|
|
88
|
+
if (!registry) {
|
|
89
|
+
return reply.status(503).send({ error: 'service_unavailable' });
|
|
90
|
+
}
|
|
91
|
+
const { descriptorMap, knownSet } = await buildIntegrationMap(registry);
|
|
92
|
+
const query = request.query;
|
|
93
|
+
const v = validateAuthorizeQuery(query, knownSet);
|
|
94
|
+
if (!v.ok) {
|
|
95
|
+
return reply.status(v.status).send({ error: v.error });
|
|
96
|
+
}
|
|
97
|
+
const descriptor = descriptorMap.get(v.integration);
|
|
98
|
+
if (!isRedirectUriAllowed(v.redirectUri, descriptor.allowedRedirectPrefixes)) {
|
|
99
|
+
return reply
|
|
100
|
+
.status(400)
|
|
101
|
+
.send({ error: 'invalid_request — redirect_uri not allowed for this integration' });
|
|
102
|
+
}
|
|
103
|
+
const html = (0, consent_page_js_1.renderConsentPage)({
|
|
104
|
+
displayName: descriptor.displayName,
|
|
105
|
+
username: tokenInfo.username ?? '',
|
|
106
|
+
scopeSummary: summariseScopes(descriptor.requestedScopes),
|
|
107
|
+
hidden: {
|
|
108
|
+
integration: v.integration,
|
|
109
|
+
redirect_uri: v.redirectUri,
|
|
110
|
+
state: v.state,
|
|
111
|
+
response_type: 'code',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return reply.type('text/html').send(html);
|
|
115
|
+
});
|
|
116
|
+
// ─── POST /api/oauth2/authorize ───────────────────────────────────────────
|
|
117
|
+
fastify.post('/api/oauth2/authorize', async (request, reply) => {
|
|
118
|
+
const cookie = request.cookies[session_cookie_js_1.SESSION_COOKIE];
|
|
119
|
+
if (!cookie) {
|
|
120
|
+
if ((0, session_cookie_js_1.shouldRedirectToLogin)(request.method, request.headers.accept)) {
|
|
121
|
+
return reply.redirect((0, session_cookie_js_1.loginRedirectUrl)(request.url));
|
|
122
|
+
}
|
|
123
|
+
return reply.status(401).send({ error: 'unauthorized' });
|
|
124
|
+
}
|
|
125
|
+
let tokenInfo;
|
|
126
|
+
try {
|
|
127
|
+
tokenInfo = deps.verifyToken(cookie);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
if ((0, session_cookie_js_1.shouldRedirectToLogin)(request.method, request.headers.accept)) {
|
|
131
|
+
return reply.redirect((0, session_cookie_js_1.loginRedirectUrl)(request.url));
|
|
132
|
+
}
|
|
133
|
+
return reply.status(401).send({ error: 'unauthorized' });
|
|
134
|
+
}
|
|
135
|
+
const registry = deps.getRegistry();
|
|
136
|
+
if (!registry) {
|
|
137
|
+
return reply.status(503).send({ error: 'service_unavailable' });
|
|
138
|
+
}
|
|
139
|
+
const { descriptorMap, knownSet } = await buildIntegrationMap(registry);
|
|
140
|
+
const body = request.body;
|
|
141
|
+
const formQuery = {
|
|
142
|
+
response_type: body.response_type,
|
|
143
|
+
integration: body.integration,
|
|
144
|
+
redirect_uri: body.redirect_uri,
|
|
145
|
+
state: body.state,
|
|
146
|
+
};
|
|
147
|
+
const v = validateAuthorizeQuery(formQuery, knownSet);
|
|
148
|
+
if (!v.ok) {
|
|
149
|
+
return reply.status(v.status).send({ error: v.error });
|
|
150
|
+
}
|
|
151
|
+
const descriptor = descriptorMap.get(v.integration);
|
|
152
|
+
if (!isRedirectUriAllowed(v.redirectUri, descriptor.allowedRedirectPrefixes)) {
|
|
153
|
+
return reply
|
|
154
|
+
.status(400)
|
|
155
|
+
.send({ error: 'invalid_request — redirect_uri not allowed for this integration' });
|
|
156
|
+
}
|
|
157
|
+
if (body.consent !== 'allow') {
|
|
158
|
+
return reply.redirect(`${v.redirectUri}?error=access_denied&state=${encodeURIComponent(v.state)}`);
|
|
159
|
+
}
|
|
160
|
+
const userMgmt = registry.getSingleton('user-management');
|
|
161
|
+
if (!userMgmt) {
|
|
162
|
+
return reply.status(503).send({ error: 'service_unavailable' });
|
|
163
|
+
}
|
|
164
|
+
const { code } = await userMgmt.oauthIssueCode({
|
|
165
|
+
integrationId: v.integration,
|
|
166
|
+
userId: tokenInfo.userId ?? '',
|
|
167
|
+
username: tokenInfo.username ?? '',
|
|
168
|
+
scopes: descriptor.requestedScopes,
|
|
169
|
+
redirectUri: v.redirectUri,
|
|
170
|
+
// Prefer the integration's own public origin (e.g. the operator-selected
|
|
171
|
+
// external-access endpoint surfaced by a forked exporter addon) so the
|
|
172
|
+
// claim the cloud Lambda routes back on is the reachable public URL, not
|
|
173
|
+
// the hub-global fallback (which defaults to localhost in dev).
|
|
174
|
+
hubUrl: descriptor.hubUrl ?? deps.publicHubUrl(),
|
|
175
|
+
});
|
|
176
|
+
return reply.redirect(`${v.redirectUri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(v.state)}`);
|
|
177
|
+
});
|
|
178
|
+
// ─── POST /api/oauth2/token ───────────────────────────────────────────────
|
|
179
|
+
// No session gate — called by Alexa/Lambda directly.
|
|
180
|
+
fastify.post('/api/oauth2/token', async (request, reply) => {
|
|
181
|
+
const registry = deps.getRegistry();
|
|
182
|
+
if (!registry) {
|
|
183
|
+
return reply.status(503).send({ error: 'service_unavailable' });
|
|
184
|
+
}
|
|
185
|
+
const userMgmt = registry.getSingleton('user-management');
|
|
186
|
+
if (!userMgmt) {
|
|
187
|
+
return reply.status(503).send({ error: 'service_unavailable' });
|
|
188
|
+
}
|
|
189
|
+
const body = request.body;
|
|
190
|
+
let tokenResult;
|
|
191
|
+
if (body.grant_type === 'authorization_code') {
|
|
192
|
+
if (!body.code || !body.redirect_uri) {
|
|
193
|
+
return reply.status(400).send({ error: 'invalid_request' });
|
|
194
|
+
}
|
|
195
|
+
tokenResult = await userMgmt.oauthExchangeCode({
|
|
196
|
+
code: body.code,
|
|
197
|
+
redirectUri: body.redirect_uri,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else if (body.grant_type === 'refresh_token') {
|
|
201
|
+
if (!body.refresh_token) {
|
|
202
|
+
return reply.status(400).send({ error: 'invalid_request' });
|
|
203
|
+
}
|
|
204
|
+
tokenResult = await userMgmt.oauthRefresh({ refreshToken: body.refresh_token });
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
return reply.status(400).send({ error: 'unsupported_grant_type' });
|
|
208
|
+
}
|
|
209
|
+
if (tokenResult === null) {
|
|
210
|
+
return reply.status(400).send({ error: 'invalid_grant' });
|
|
211
|
+
}
|
|
212
|
+
return reply.send({
|
|
213
|
+
access_token: tokenResult.accessToken,
|
|
214
|
+
refresh_token: tokenResult.refreshToken,
|
|
215
|
+
expires_in: tokenResult.expiresIn,
|
|
216
|
+
token_type: 'Bearer',
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|