@camstack/server 1.2.80 → 1.2.82
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.
|
@@ -68,10 +68,8 @@ exports.buildNodesProvider = buildNodesProvider;
|
|
|
68
68
|
exports.buildIntegrationsProvider = buildIntegrationsProvider;
|
|
69
69
|
exports.dispatchCustomAction = dispatchCustomAction;
|
|
70
70
|
exports.buildAddonsProvider = buildAddonsProvider;
|
|
71
|
-
const node_child_process_1 = require("node:child_process");
|
|
72
71
|
const node_crypto_1 = require("node:crypto");
|
|
73
72
|
const os = __importStar(require("node:os"));
|
|
74
|
-
const node_util_1 = require("node:util");
|
|
75
73
|
const system_1 = require("@camstack/system");
|
|
76
74
|
const types_1 = require("@camstack/types");
|
|
77
75
|
const server_1 = require("@trpc/server");
|
|
@@ -79,7 +77,6 @@ const integration_id_backfill_1 = require("../../boot/integration-id-backfill");
|
|
|
79
77
|
const addon_package_service_js_1 = require("../../core/addon/addon-package.service.js");
|
|
80
78
|
const lifecycle_runner_singleton_js_1 = require("../../core/lifecycle/lifecycle-runner.singleton.js");
|
|
81
79
|
const collection_preference_js_1 = require("./collection-preference.js");
|
|
82
|
-
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
83
80
|
// ── system ──────────────────────────────────────────────────────────
|
|
84
81
|
function getRetention(registry) {
|
|
85
82
|
return (registry?.getSingleton('analysis-data-persistence')?.retention ?? null);
|
|
@@ -569,6 +566,117 @@ function getDeviceProvider(ar, addonId) {
|
|
|
569
566
|
const provider = ar.getCapabilityRegistry().getProviderByAddon('device-provider', addonId);
|
|
570
567
|
return isDeviceProvider(provider) ? provider : null;
|
|
571
568
|
}
|
|
569
|
+
function isConnectionTestProvider(value) {
|
|
570
|
+
return (value !== null &&
|
|
571
|
+
typeof value === 'object' &&
|
|
572
|
+
typeof Reflect.get(value, 'testSettings') === 'function');
|
|
573
|
+
}
|
|
574
|
+
function getConnectionTestProvider(ar, addonId) {
|
|
575
|
+
const provider = ar.getCapabilityRegistry().getProviderByAddon('connection-test', addonId);
|
|
576
|
+
return isConnectionTestProvider(provider) ? provider : null;
|
|
577
|
+
}
|
|
578
|
+
function isBrokerProvider(value) {
|
|
579
|
+
return (value !== null &&
|
|
580
|
+
typeof value === 'object' &&
|
|
581
|
+
typeof Reflect.get(value, 'testConnection') === 'function');
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Does this addon declare a pre-creation test at all? Drives
|
|
585
|
+
* `getAvailableTypes().canTest`, so the wizard can say "this one cannot be
|
|
586
|
+
* tested" instead of offering a button that always answers the same nonsense.
|
|
587
|
+
*
|
|
588
|
+
* Broker-mode addons count: their connection identity is the broker, and the
|
|
589
|
+
* broker cap owns a real `testConnection`.
|
|
590
|
+
*/
|
|
591
|
+
function declaresConnectionTest(ar, addonId, mode) {
|
|
592
|
+
if (getConnectionTestProvider(ar, addonId) !== null)
|
|
593
|
+
return true;
|
|
594
|
+
if (mode !== 'broker')
|
|
595
|
+
return false;
|
|
596
|
+
return isBrokerProvider(ar.getCapabilityRegistry().getProviderByAddon('broker', addonId));
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Run the DECLARED pre-creation test for `(addonId, settings)`.
|
|
600
|
+
*
|
|
601
|
+
* Never throws: a test that blows up is `inconclusive`, which is emphatically
|
|
602
|
+
* NOT `rejected`. A timeout must not read as a refusal — it would block a
|
|
603
|
+
* perfectly good create on a flaky network, and the operator would have no way
|
|
604
|
+
* to tell the two apart.
|
|
605
|
+
*/
|
|
606
|
+
async function runConnectionTest(ar, addonId, settings, logger) {
|
|
607
|
+
// 1. The provider's own declared test — the primary path.
|
|
608
|
+
const tester = getConnectionTestProvider(ar, addonId);
|
|
609
|
+
if (tester) {
|
|
610
|
+
try {
|
|
611
|
+
const outcome = await tester.testSettings({ settings });
|
|
612
|
+
if (outcome.outcome === 'validated') {
|
|
613
|
+
return {
|
|
614
|
+
status: 'validated',
|
|
615
|
+
testedBy: addonId,
|
|
616
|
+
...(outcome.latencyMs !== undefined ? { latencyMs: outcome.latencyMs } : {}),
|
|
617
|
+
...(outcome.detail !== undefined ? { detail: outcome.detail } : {}),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
if (outcome.outcome === 'rejected') {
|
|
621
|
+
return { status: 'rejected', error: outcome.error, testedBy: addonId };
|
|
622
|
+
}
|
|
623
|
+
return { status: 'inconclusive', error: outcome.error, testedBy: addonId };
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
logger.warn('connection test could not run', {
|
|
627
|
+
tags: { addonId },
|
|
628
|
+
meta: { phase: 'test', error: (0, types_1.errMsg)(err) },
|
|
629
|
+
});
|
|
630
|
+
return {
|
|
631
|
+
status: 'inconclusive',
|
|
632
|
+
error: `Connection test could not run: ${(0, types_1.errMsg)(err)}`,
|
|
633
|
+
testedBy: addonId,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// 2. Broker-backed integrations carry their connection identity as a
|
|
638
|
+
// `brokerId`; the broker already owns the real semantic check (HA opens a
|
|
639
|
+
// temporary WS handshake, MQTT pings the bridge). Reached because the
|
|
640
|
+
// settings NAME a broker, not because nothing else matched.
|
|
641
|
+
const brokerId = settings['brokerId'];
|
|
642
|
+
if (typeof brokerId === 'string' && brokerId.length > 0) {
|
|
643
|
+
const brokerProvider = ar
|
|
644
|
+
.getCapabilityRegistry()
|
|
645
|
+
.getProviderByAddonId('broker', addonId);
|
|
646
|
+
if (!brokerProvider) {
|
|
647
|
+
return {
|
|
648
|
+
status: 'inconclusive',
|
|
649
|
+
error: `Broker provider for addon '${addonId}' is not available`,
|
|
650
|
+
testedBy: null,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
const result = await brokerProvider.testConnection({ id: brokerId });
|
|
655
|
+
return result.ok
|
|
656
|
+
? { status: 'validated', latencyMs: result.latencyMs, testedBy: addonId }
|
|
657
|
+
: { status: 'rejected', error: result.error, testedBy: addonId };
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
logger.warn('broker connection test could not run', {
|
|
661
|
+
tags: { addonId },
|
|
662
|
+
meta: { phase: 'test', brokerId, error: (0, types_1.errMsg)(err) },
|
|
663
|
+
});
|
|
664
|
+
return {
|
|
665
|
+
status: 'inconclusive',
|
|
666
|
+
error: `Broker test could not run: ${(0, types_1.errMsg)(err)}`,
|
|
667
|
+
testedBy: addonId,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// 3. Nothing declared. Say so plainly — this must never render as a pass,
|
|
672
|
+
// and must never render as a failure we did not observe.
|
|
673
|
+
return {
|
|
674
|
+
status: 'unsupported',
|
|
675
|
+
error: `Integration '${addonId}' declares no connection test, ` +
|
|
676
|
+
`so these settings cannot be validated before it is created.`,
|
|
677
|
+
testedBy: null,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
572
680
|
/**
|
|
573
681
|
* Marker caps that flag an addon as a creatable integration type:
|
|
574
682
|
* - `device-provider` — classic providers (Reolink/ONVIF/Frigate)
|
|
@@ -621,6 +729,38 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
621
729
|
throw new Error(`Addon "${input.addonId}" is unique-instance and already has an integration (${existing[0].id})`);
|
|
622
730
|
}
|
|
623
731
|
}
|
|
732
|
+
// ── the gate ────────────────────────────────────────────────────
|
|
733
|
+
//
|
|
734
|
+
// "Next" must validate and create ONLY on success. `rejected` is the one
|
|
735
|
+
// status that blocks: it means the remote was asked and said no. An
|
|
736
|
+
// `unsupported` (no test declared) or `inconclusive` (declared test could
|
|
737
|
+
// not complete) observed NOTHING about the credentials — refusing on
|
|
738
|
+
// those would make every integration lacking the hook uncreatable, and
|
|
739
|
+
// would fail a good create on a flaky network. They pass, and they SAY SO
|
|
740
|
+
// in the log: silence here would read as "validated".
|
|
741
|
+
const verdict = await runConnectionTest(ar, input.addonId, input.settings ?? {}, logger);
|
|
742
|
+
if (verdict.status === 'rejected') {
|
|
743
|
+
logger.warn('rejected — connection test refused the settings', {
|
|
744
|
+
tags: { addonId: input.addonId },
|
|
745
|
+
meta: { phase: 'create', status: verdict.status, testedBy: verdict.testedBy },
|
|
746
|
+
});
|
|
747
|
+
throw new server_1.TRPCError({
|
|
748
|
+
code: 'BAD_REQUEST',
|
|
749
|
+
message: verdict.error ?? `Connection test failed for '${input.addonId}'`,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
if (verdict.status === 'validated') {
|
|
753
|
+
logger.info('connection test validated — proceeding', {
|
|
754
|
+
tags: { addonId: input.addonId },
|
|
755
|
+
meta: { phase: 'create', latencyMs: verdict.latencyMs },
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
logger.warn('creating without validation — the settings were not validated', {
|
|
760
|
+
tags: { addonId: input.addonId },
|
|
761
|
+
meta: { phase: 'create', status: verdict.status, reason: verdict.error },
|
|
762
|
+
});
|
|
763
|
+
}
|
|
624
764
|
const integration = await reg.createIntegration(payload);
|
|
625
765
|
logger.info('persisted', {
|
|
626
766
|
tags: { integrationId: integration.id, addonId: integration.addonId },
|
|
@@ -911,6 +1051,7 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
911
1051
|
kind,
|
|
912
1052
|
brokerKind,
|
|
913
1053
|
supportsLocationImport,
|
|
1054
|
+
canTest: declaresConnectionTest(ar, m.id, mode),
|
|
914
1055
|
existingInstances: existing.map((i) => ({
|
|
915
1056
|
id: i.id,
|
|
916
1057
|
name: i.name,
|
|
@@ -920,63 +1061,24 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
920
1061
|
});
|
|
921
1062
|
},
|
|
922
1063
|
testConnection: async (input) => {
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
942
|
-
try {
|
|
943
|
-
const result = await brokerProvider.testConnection({ id: brokerId });
|
|
944
|
-
return result.ok ? { success: true } : { success: false, error: result.error };
|
|
945
|
-
}
|
|
946
|
-
catch (err) {
|
|
947
|
-
return { success: false, error: (0, types_1.errMsg)(err) };
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
// Default — RTSP/Frigate/ONVIF legacy path: probe the stream URL.
|
|
951
|
-
const url = String(input.settings['main_stream_url'] ?? input.settings['url'] ?? '').trim();
|
|
952
|
-
if (!url)
|
|
953
|
-
return { success: false, error: 'No stream URL provided' };
|
|
954
|
-
try {
|
|
955
|
-
const { stdout } = await execFileAsync('ffprobe', [
|
|
956
|
-
'-v',
|
|
957
|
-
'error',
|
|
958
|
-
'-rtsp_transport',
|
|
959
|
-
'tcp',
|
|
960
|
-
'-timeout',
|
|
961
|
-
'3000000',
|
|
962
|
-
'-show_entries',
|
|
963
|
-
'stream=codec_name,width,height',
|
|
964
|
-
'-of',
|
|
965
|
-
'json',
|
|
966
|
-
url,
|
|
967
|
-
], { timeout: 5000 });
|
|
968
|
-
const parsed = (0, types_1.asJsonObject)(JSON.parse(stdout));
|
|
969
|
-
const streams = (0, types_1.asJsonArray)(parsed?.streams);
|
|
970
|
-
return streams.length > 0
|
|
971
|
-
? { success: true }
|
|
972
|
-
: { success: false, error: 'No streams found at URL' };
|
|
973
|
-
}
|
|
974
|
-
catch (err) {
|
|
975
|
-
return {
|
|
976
|
-
success: false,
|
|
977
|
-
error: `Connection failed: ${(0, types_1.errMsg)(err)}`,
|
|
978
|
-
};
|
|
979
|
-
}
|
|
1064
|
+
// The Test button. Routes to the DECLARED test (see `runConnectionTest`)
|
|
1065
|
+
// and reports the tri-state verbatim: `success` is true ONLY for
|
|
1066
|
+
// `validated`, so a test that never ran can never render as a tick — and
|
|
1067
|
+
// `status` lets the UI show "could not be validated" in its own colour
|
|
1068
|
+
// instead of a red failure nobody observed.
|
|
1069
|
+
const verdict = await runConnectionTest(ar, input.addonId, input.settings, logger);
|
|
1070
|
+
logger.info('connection test', {
|
|
1071
|
+
tags: { addonId: input.addonId },
|
|
1072
|
+
meta: { phase: 'test', status: verdict.status, testedBy: verdict.testedBy },
|
|
1073
|
+
});
|
|
1074
|
+
return {
|
|
1075
|
+
success: verdict.status === 'validated',
|
|
1076
|
+
status: verdict.status,
|
|
1077
|
+
testedBy: verdict.testedBy,
|
|
1078
|
+
...(verdict.error !== undefined ? { error: verdict.error } : {}),
|
|
1079
|
+
...(verdict.latencyMs !== undefined ? { latencyMs: verdict.latencyMs } : {}),
|
|
1080
|
+
...(verdict.detail !== undefined ? { detail: verdict.detail } : {}),
|
|
1081
|
+
};
|
|
980
1082
|
},
|
|
981
1083
|
};
|
|
982
1084
|
}
|
|
@@ -7,6 +7,43 @@ exports.registerOauth2Routes = registerOauth2Routes;
|
|
|
7
7
|
const session_cookie_js_1 = require("../../auth/session-cookie.js");
|
|
8
8
|
const consent_page_js_1 = require("./consent-page.js");
|
|
9
9
|
const private_host_js_1 = require("./private-host.js");
|
|
10
|
+
/** Longest caller-supplied `integration` value echoed back in an error. */
|
|
11
|
+
const MAX_ECHOED_INTEGRATION_LEN = 100;
|
|
12
|
+
/** Render the received `integration` for a human, bounded. */
|
|
13
|
+
function describeReceivedIntegration(raw) {
|
|
14
|
+
if (raw === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
const joined = typeof raw === 'string' ? raw : raw.join(',');
|
|
17
|
+
if (joined === '')
|
|
18
|
+
return null;
|
|
19
|
+
return joined.length > MAX_ECHOED_INTEGRATION_LEN
|
|
20
|
+
? `${joined.slice(0, MAX_ECHOED_INTEGRATION_LEN)}…`
|
|
21
|
+
: joined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the `integration` parameter to a single id.
|
|
25
|
+
*
|
|
26
|
+
* A repeat carrying ONE distinct value is collapsed rather than refused:
|
|
27
|
+
* every layer that parses the query — proxy, framework, app — necessarily
|
|
28
|
+
* agrees on the value, so there is no parameter-pollution ambiguity to
|
|
29
|
+
* protect against, and refusing would strand a client whose request is
|
|
30
|
+
* merely redundant. A repeat whose values DISAGREE is exactly that
|
|
31
|
+
* ambiguity, and is refused.
|
|
32
|
+
*/
|
|
33
|
+
function resolveIntegrationParam(raw) {
|
|
34
|
+
if (raw === undefined)
|
|
35
|
+
return { ok: false, reason: 'missing' };
|
|
36
|
+
if (typeof raw === 'string') {
|
|
37
|
+
return raw === '' ? { ok: false, reason: 'missing' } : { ok: true, value: raw };
|
|
38
|
+
}
|
|
39
|
+
const distinct = [...new Set(raw.filter((v) => v !== ''))];
|
|
40
|
+
const [first] = distinct;
|
|
41
|
+
if (first === undefined)
|
|
42
|
+
return { ok: false, reason: 'missing' };
|
|
43
|
+
if (distinct.length > 1)
|
|
44
|
+
return { ok: false, reason: 'conflicting-repeat' };
|
|
45
|
+
return { ok: true, value: first };
|
|
46
|
+
}
|
|
10
47
|
/** Validate the inbound authorize query. `client_id` is intentionally
|
|
11
48
|
* NOT checked — that pair is verified only at the Lambda boundary, and a
|
|
12
49
|
* PUBLIC client (`requiresPkce`) has no secret to check it against at all;
|
|
@@ -14,9 +51,24 @@ const private_host_js_1 = require("./private-host.js");
|
|
|
14
51
|
function validateAuthorizeQuery(q, knownIntegrations) {
|
|
15
52
|
if (q.response_type !== 'code')
|
|
16
53
|
return { ok: false, status: 400, error: 'unsupported_response_type' };
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
54
|
+
// Three distinct client faults used to collapse into one opaque string.
|
|
55
|
+
// Alexa linking was down for a day because the message named neither what
|
|
56
|
+
// arrived nor what would have been accepted.
|
|
57
|
+
const detail = {
|
|
58
|
+
received: describeReceivedIntegration(q.integration),
|
|
59
|
+
known_integrations: [...knownIntegrations.keys()],
|
|
60
|
+
};
|
|
61
|
+
const param = resolveIntegrationParam(q.integration);
|
|
62
|
+
if (!param.ok) {
|
|
63
|
+
const error = param.reason === 'missing'
|
|
64
|
+
? 'invalid_request — integration parameter missing'
|
|
65
|
+
: 'invalid_request — integration parameter repeated with conflicting values';
|
|
66
|
+
return { ok: false, status: 400, error, detail };
|
|
67
|
+
}
|
|
68
|
+
const policy = knownIntegrations.get(param.value);
|
|
69
|
+
if (!policy) {
|
|
70
|
+
return { ok: false, status: 400, error: 'invalid_request — unknown integration', detail };
|
|
71
|
+
}
|
|
20
72
|
if (!q.redirect_uri)
|
|
21
73
|
return { ok: false, status: 400, error: 'invalid_request — redirect_uri required' };
|
|
22
74
|
if (!q.state)
|
|
@@ -32,7 +84,7 @@ function validateAuthorizeQuery(q, knownIntegrations) {
|
|
|
32
84
|
}
|
|
33
85
|
return {
|
|
34
86
|
ok: true,
|
|
35
|
-
integration:
|
|
87
|
+
integration: param.value,
|
|
36
88
|
redirectUri: q.redirect_uri,
|
|
37
89
|
state: q.state,
|
|
38
90
|
codeChallenge: challenge,
|
|
@@ -76,6 +128,20 @@ function summariseScopes(scopes) {
|
|
|
76
128
|
}
|
|
77
129
|
return scopes.map((s) => s.type).join(', ') || 'no permissions';
|
|
78
130
|
}
|
|
131
|
+
/** A refused `/authorize`. Logged at `warn` and echoed to the caller: both
|
|
132
|
+
* halves name the value that ARRIVED and the ids that would have worked, so
|
|
133
|
+
* neither the operator nor the log reader has to guess which of them is
|
|
134
|
+
* wrong. */
|
|
135
|
+
function refuseAuthorize(reply, logger, method, refusal) {
|
|
136
|
+
logger.warn(`oauth2 authorize refused: ${refusal.error}`, {
|
|
137
|
+
meta: {
|
|
138
|
+
method,
|
|
139
|
+
received: refusal.detail?.received ?? null,
|
|
140
|
+
knownIntegrations: refusal.detail?.known_integrations ?? [],
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return reply.status(refusal.status).send({ error: refusal.error, ...refusal.detail });
|
|
144
|
+
}
|
|
79
145
|
/** Build a map of integrationId → descriptor from all registered oauth-integration providers. */
|
|
80
146
|
async function buildIntegrationMap(registry) {
|
|
81
147
|
const entries = registry.getCollectionEntries('oauth-integration');
|
|
@@ -137,10 +203,19 @@ function registerOauth2Routes(fastify, deps) {
|
|
|
137
203
|
const query = request.query;
|
|
138
204
|
const v = validateAuthorizeQuery(query, descriptorMap);
|
|
139
205
|
if (!v.ok) {
|
|
140
|
-
return reply.
|
|
206
|
+
return refuseAuthorize(reply, deps.logger, 'GET', v);
|
|
141
207
|
}
|
|
142
208
|
const descriptor = descriptorMap.get(v.integration);
|
|
143
209
|
if (!isRedirectUriAllowed(v.redirectUri, descriptor.allowedRedirectPrefixes, descriptor.allowedPrivateHostPaths ?? [])) {
|
|
210
|
+
deps.logger.warn('oauth2 authorize refused: redirect_uri not allowed', {
|
|
211
|
+
meta: {
|
|
212
|
+
method: 'GET',
|
|
213
|
+
integration: v.integration,
|
|
214
|
+
redirectUri: v.redirectUri,
|
|
215
|
+
allowedPrefixes: descriptor.allowedRedirectPrefixes,
|
|
216
|
+
allowedPrivateHostPaths: descriptor.allowedPrivateHostPaths ?? [],
|
|
217
|
+
},
|
|
218
|
+
});
|
|
144
219
|
return reply
|
|
145
220
|
.status(400)
|
|
146
221
|
.send({ error: 'invalid_request — redirect_uri not allowed for this integration' });
|
|
@@ -223,10 +298,19 @@ function registerOauth2Routes(fastify, deps) {
|
|
|
223
298
|
// and every one of them is attacker-editable.
|
|
224
299
|
const v = validateAuthorizeQuery(formQuery, descriptorMap);
|
|
225
300
|
if (!v.ok) {
|
|
226
|
-
return reply.
|
|
301
|
+
return refuseAuthorize(reply, deps.logger, 'POST', v);
|
|
227
302
|
}
|
|
228
303
|
const descriptor = descriptorMap.get(v.integration);
|
|
229
304
|
if (!isRedirectUriAllowed(v.redirectUri, descriptor.allowedRedirectPrefixes, descriptor.allowedPrivateHostPaths ?? [])) {
|
|
305
|
+
deps.logger.warn('oauth2 authorize refused: redirect_uri not allowed', {
|
|
306
|
+
meta: {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
integration: v.integration,
|
|
309
|
+
redirectUri: v.redirectUri,
|
|
310
|
+
allowedPrefixes: descriptor.allowedRedirectPrefixes,
|
|
311
|
+
allowedPrivateHostPaths: descriptor.allowedPrivateHostPaths ?? [],
|
|
312
|
+
},
|
|
313
|
+
});
|
|
230
314
|
return reply
|
|
231
315
|
.status(400)
|
|
232
316
|
.send({ error: 'invalid_request — redirect_uri not allowed for this integration' });
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
|
|
3
3
|
// Re-run: npx tsx scripts/generate-cap-mounts.ts
|
|
4
4
|
//
|
|
5
|
-
// Mounted:
|
|
5
|
+
// Mounted: 145 Skipped (legacy): 3
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.LEGACY_SHAPE_SKIP = void 0;
|
|
8
8
|
exports.mountAllCaps = mountAllCaps;
|
|
@@ -111,6 +111,15 @@ function mountAllCaps(services) {
|
|
|
111
111
|
carbonMonoxide: (0, generated_cap_routers_js_1.createCapRouter_carbonMonoxide)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'carbon-monoxide'), remoteCapProxy),
|
|
112
112
|
climateControl: (0, generated_cap_routers_js_1.createCapRouter_climateControl)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'climate-control'), remoteCapProxy),
|
|
113
113
|
color: (0, generated_cap_routers_js_1.createCapRouter_color)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'color'), remoteCapProxy),
|
|
114
|
+
connectionTest: (0, generated_cap_routers_js_1.createCapRouter_connectionTest)((_ctx, addonId) => {
|
|
115
|
+
if (!reg)
|
|
116
|
+
return null;
|
|
117
|
+
if (addonId !== undefined) {
|
|
118
|
+
return reg.getProviderByAddonId('connection-test', addonId);
|
|
119
|
+
}
|
|
120
|
+
const entries = reg.getCollectionEntries('connection-test');
|
|
121
|
+
return entries[0]?.[1] ?? null;
|
|
122
|
+
}, remoteCapProxy),
|
|
114
123
|
connectivity: (0, generated_cap_routers_js_1.createCapRouter_connectivity)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'connectivity'), remoteCapProxy),
|
|
115
124
|
consumables: (0, generated_cap_routers_js_1.createCapRouter_consumables)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'consumables'), remoteCapProxy),
|
|
116
125
|
contact: (0, generated_cap_routers_js_1.createCapRouter_contact)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'contact'), remoteCapProxy),
|