@camstack/server 1.2.480 → 1.2.482
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/agent/main.js +15 -4
- package/dist/api/core/auth.router.js +188 -192
- package/dist/api/core/cap-providers.js +49 -5
- package/dist/api/core/integration-secrets.js +60 -0
- package/dist/api/trpc/device-config-secret-redaction.js +111 -13
- package/dist/api/trpc/generated-cap-mounts.js +3 -1
- package/dist/api/trpc/generated-cap-routers.js +1710 -1345
- package/dist/api/trpc/trpc.router.js +1 -1
- package/dist/auth/auth-provider-gate.js +19 -0
- package/dist/auth/external-link-intents.js +63 -0
- package/dist/auth/passkey-provider.js +96 -0
- package/dist/auth/second-factors.js +21 -0
- package/dist/auth/session-mint.js +0 -8
- package/dist/auth/sso-finish-deps.js +71 -0
- package/dist/auth/sso-finish.js +164 -39
- package/dist/boot/platform-keychain-key.js +94 -0
- package/dist/core/addon/addon-registry.service.js +59 -0
- package/dist/core/addon/addon-settings-provider.js +7 -1
- package/dist/core/moleculer/moleculer.service.js +31 -1
- package/dist/main.js +31 -4
- package/package.json +10 -10
|
@@ -70,6 +70,7 @@ exports.buildNodesProvider = buildNodesProvider;
|
|
|
70
70
|
exports.buildIntegrationsProvider = buildIntegrationsProvider;
|
|
71
71
|
exports.dispatchCustomAction = dispatchCustomAction;
|
|
72
72
|
exports.buildAddonsProvider = buildAddonsProvider;
|
|
73
|
+
const integration_secrets_1 = require("./integration-secrets");
|
|
73
74
|
const node_crypto_1 = require("node:crypto");
|
|
74
75
|
const os = __importStar(require("node:os"));
|
|
75
76
|
const system_1 = require("@camstack/system");
|
|
@@ -685,6 +686,14 @@ function buildNodesProvider(agentRegistry, moleculer, addonRegistry, getNodeRoot
|
|
|
685
686
|
},
|
|
686
687
|
};
|
|
687
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* The keychain handle a hub door uses for an integration's addon (D787), or
|
|
691
|
+
* `null` when this registry predates the keychain (a test double, an older
|
|
692
|
+
* kernel) — the door then stores the settings as it always did.
|
|
693
|
+
*/
|
|
694
|
+
function ownerSecrets(ar, owner) {
|
|
695
|
+
return typeof ar.secretsForOwner === 'function' ? ar.secretsForOwner(owner) : null;
|
|
696
|
+
}
|
|
688
697
|
function requireIntegrationRegistry(ar) {
|
|
689
698
|
const reg = ar.getIntegrationRegistry();
|
|
690
699
|
if (!reg)
|
|
@@ -845,7 +854,10 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
845
854
|
},
|
|
846
855
|
getByAddonId: async (input) => requireIntegrationRegistry(ar).getIntegrationByAddonId(input.addonId),
|
|
847
856
|
create: async (input) => {
|
|
848
|
-
const { skipRestart, ...
|
|
857
|
+
const { skipRestart, secretKeys, settings, ...rest } = input;
|
|
858
|
+
// Settings are written AFTER the row exists: a secret's ref is scoped
|
|
859
|
+
// to the integration id (D787).
|
|
860
|
+
const payload = rest;
|
|
849
861
|
const reg = requireIntegrationRegistry(ar);
|
|
850
862
|
logger.info('request', {
|
|
851
863
|
tags: { addonId: input.addonId },
|
|
@@ -872,7 +884,7 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
872
884
|
// those would make every integration lacking the hook uncreatable, and
|
|
873
885
|
// would fail a good create on a flaky network. They pass, and they SAY SO
|
|
874
886
|
// in the log: silence here would read as "validated".
|
|
875
|
-
const verdict = await runConnectionTest(ar, input.addonId,
|
|
887
|
+
const verdict = await runConnectionTest(ar, input.addonId, settings ?? {}, logger);
|
|
876
888
|
if (verdict.status === 'rejected') {
|
|
877
889
|
logger.warn('rejected — connection test refused the settings', {
|
|
878
890
|
tags: { addonId: input.addonId },
|
|
@@ -896,11 +908,25 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
896
908
|
});
|
|
897
909
|
}
|
|
898
910
|
const integration = await reg.createIntegration(payload);
|
|
911
|
+
if (settings !== undefined && Object.keys(settings).length > 0) {
|
|
912
|
+
await (0, integration_secrets_1.writeIntegrationSettings)({
|
|
913
|
+
registry: reg,
|
|
914
|
+
secrets: ownerSecrets(ar, integration.addonId),
|
|
915
|
+
onOrphaned: (refs) => logger.warn('dropped keychain refs could not be deleted', {
|
|
916
|
+
meta: { refs: [...refs] },
|
|
917
|
+
}),
|
|
918
|
+
}, {
|
|
919
|
+
integrationId: integration.id,
|
|
920
|
+
settings,
|
|
921
|
+
secretKeys: secretKeys ?? [],
|
|
922
|
+
operatorWrite: true,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
899
925
|
logger.info('persisted', {
|
|
900
926
|
tags: { integrationId: integration.id, addonId: integration.addonId },
|
|
901
927
|
meta: { phase: 'create' },
|
|
902
928
|
});
|
|
903
|
-
const hasSettings =
|
|
929
|
+
const hasSettings = settings != null && Object.keys(settings).length > 0;
|
|
904
930
|
if (!skipRestart && hasSettings) {
|
|
905
931
|
logger.info('settings present — restarting addon', {
|
|
906
932
|
tags: { addonId: input.addonId },
|
|
@@ -1086,7 +1112,13 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
1086
1112
|
const integration = await reg.getIntegration(input.id);
|
|
1087
1113
|
if (!integration)
|
|
1088
1114
|
throw new Error(`Integration "${input.id}" not found`);
|
|
1089
|
-
|
|
1115
|
+
// The integration's own addon (the caller the transport stamped) reads
|
|
1116
|
+
// its secrets resolved; anyone else — the admin form — reads "set"
|
|
1117
|
+
// views, never a value (D787, D789).
|
|
1118
|
+
const caller = Reflect.get(input, 'callerAddonId');
|
|
1119
|
+
const forOwner = typeof caller === 'string' &&
|
|
1120
|
+
(caller === integration.addonId || ar.runnerIdFor(integration.addonId) === caller);
|
|
1121
|
+
return (0, integration_secrets_1.readIntegrationSettings)({ registry: reg, secrets: forOwner ? ownerSecrets(ar, integration.addonId) : null }, { integrationId: input.id, forOwner });
|
|
1090
1122
|
},
|
|
1091
1123
|
setSettings: async (input) => {
|
|
1092
1124
|
const reg = requireIntegrationRegistry(ar);
|
|
@@ -1103,7 +1135,19 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
1103
1135
|
tags: { integrationId: input.id, addonId: integration.addonId },
|
|
1104
1136
|
meta: { phase: 'setSettings', keys: settingsKeys },
|
|
1105
1137
|
});
|
|
1106
|
-
await
|
|
1138
|
+
await (0, integration_secrets_1.writeIntegrationSettings)({
|
|
1139
|
+
registry: reg,
|
|
1140
|
+
secrets: ownerSecrets(ar, integration.addonId),
|
|
1141
|
+
onOrphaned: (refs) => logger.warn('dropped keychain refs could not be deleted', {
|
|
1142
|
+
meta: { refs: [...refs] },
|
|
1143
|
+
}),
|
|
1144
|
+
}, {
|
|
1145
|
+
integrationId: input.id,
|
|
1146
|
+
settings: input.settings,
|
|
1147
|
+
secretKeys: input.secretKeys ?? [],
|
|
1148
|
+
// The integration's own addon writing back is not the operator.
|
|
1149
|
+
operatorWrite: typeof Reflect.get(input, 'callerAddonId') !== 'string',
|
|
1150
|
+
});
|
|
1107
1151
|
logger.info('persisted — restarting addon', {
|
|
1108
1152
|
tags: { addonId: integration.addonId },
|
|
1109
1153
|
meta: { phase: 'setSettings' },
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.writeIntegrationSettings = writeIntegrationSettings;
|
|
4
|
+
exports.readIntegrationSettings = readIntegrationSettings;
|
|
5
|
+
/**
|
|
6
|
+
* The integration-settings door of the keychain (D787). An integration's
|
|
7
|
+
* settings (a Wyze password, a PetKit account password) live in the
|
|
8
|
+
* `integration_settings` table, written by `integrations.create` /
|
|
9
|
+
* `setSettings` and read back by the integration's own addon through
|
|
10
|
+
* `integrations.getSettings` — and by the admin form through the same method.
|
|
11
|
+
*
|
|
12
|
+
* - WRITE: the keys the form's schema declares secret (`secretKeys`, by type)
|
|
13
|
+
* and every key that already holds a marker go through the keychain save
|
|
14
|
+
* path, OWNED by the integration's addon (`integration/<id>` scope). A value
|
|
15
|
+
* equal to an inline pre-keychain one stays inline; `''` keeps.
|
|
16
|
+
* - READ, by the owning addon (the caller the transport stamped): markers
|
|
17
|
+
* resolve to values. READ, by anyone else (the admin form): markers become
|
|
18
|
+
* their "set" views — the value never leaves the hub.
|
|
19
|
+
*/
|
|
20
|
+
const types_1 = require("@camstack/types");
|
|
21
|
+
async function writeIntegrationSettings(deps, input) {
|
|
22
|
+
if (deps.secrets === null) {
|
|
23
|
+
return deps.registry.setIntegrationSettings(input.integrationId, input.settings);
|
|
24
|
+
}
|
|
25
|
+
const stored = (await deps.registry.getIntegrationSettings(input.integrationId)) ?? {};
|
|
26
|
+
const markerKeys = Object.entries(stored)
|
|
27
|
+
.filter(([, v]) => (0, types_1.isSecretMarker)(v))
|
|
28
|
+
.map(([k]) => k);
|
|
29
|
+
const tree = (0, types_1.secretTreeFromPaths)([...new Set([...input.secretKeys, ...markerKeys])]);
|
|
30
|
+
if (tree.size === 0) {
|
|
31
|
+
return deps.registry.setIntegrationSettings(input.integrationId, input.settings);
|
|
32
|
+
}
|
|
33
|
+
const result = await (0, types_1.transformSecretWrite)({
|
|
34
|
+
tree,
|
|
35
|
+
secrets: deps.secrets,
|
|
36
|
+
patch: input.settings,
|
|
37
|
+
stored,
|
|
38
|
+
scope: { kind: 'integration', integrationId: input.integrationId },
|
|
39
|
+
operatorWrite: input.operatorWrite === true,
|
|
40
|
+
});
|
|
41
|
+
if (Object.keys(result.patch).length > 0) {
|
|
42
|
+
await deps.registry.setIntegrationSettings(input.integrationId, result.patch);
|
|
43
|
+
}
|
|
44
|
+
const { orphaned } = await result.afterWrite();
|
|
45
|
+
if (orphaned.length > 0)
|
|
46
|
+
deps.onOrphaned?.(orphaned);
|
|
47
|
+
}
|
|
48
|
+
async function readIntegrationSettings(deps, input) {
|
|
49
|
+
const stored = await deps.registry.getIntegrationSettings(input.integrationId);
|
|
50
|
+
if (input.forOwner) {
|
|
51
|
+
if (deps.secrets === null)
|
|
52
|
+
return stored;
|
|
53
|
+
return (await (0, types_1.resolveSecretMarkers)(stored, deps.secrets)).value;
|
|
54
|
+
}
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
57
|
+
out[key] = (0, types_1.isSecretMarker)(value) ? (0, types_1.toSecretFieldView)(value) : value;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
@@ -53,6 +53,7 @@ exports.redactStatusAggregateSetup = redactStatusAggregateSetup;
|
|
|
53
53
|
exports.redactStatusAggregateBatchSetup = redactStatusAggregateBatchSetup;
|
|
54
54
|
exports.redactConfigEntries = redactConfigEntries;
|
|
55
55
|
exports.redactStreamUrlSecrets = redactStreamUrlSecrets;
|
|
56
|
+
exports.maskUrlCredentialsDeep = maskUrlCredentialsDeep;
|
|
56
57
|
/** What a non-admin sees instead of the value. Matches the `***` convention
|
|
57
58
|
* `maskUrlCredentials` already uses for credentials in log output, so one
|
|
58
59
|
* redaction marker means one thing across the system. */
|
|
@@ -86,24 +87,88 @@ function isSecretConfigKey(key) {
|
|
|
86
87
|
function isRecord(x) {
|
|
87
88
|
return x !== null && typeof x === 'object' && !Array.isArray(x);
|
|
88
89
|
}
|
|
90
|
+
function parsePath(path) {
|
|
91
|
+
return path
|
|
92
|
+
.split('.')
|
|
93
|
+
.filter((part) => part.length > 0)
|
|
94
|
+
.map((part) => part.endsWith('[]') ? { key: part.slice(0, -2), each: true } : { key: part, each: false });
|
|
95
|
+
}
|
|
96
|
+
/** The declared secret paths a device row advertises, parsed. Absent ⇒ none. */
|
|
97
|
+
function declaredPathsOf(row) {
|
|
98
|
+
const raw = row['secretConfigPaths'];
|
|
99
|
+
if (!Array.isArray(raw))
|
|
100
|
+
return [];
|
|
101
|
+
return raw.filter((p) => typeof p === 'string').map(parsePath);
|
|
102
|
+
}
|
|
103
|
+
/** Withhold every non-empty string at or below `value` (a declared secret node). */
|
|
104
|
+
function withholdAll(value) {
|
|
105
|
+
if (typeof value === 'string')
|
|
106
|
+
return value.length > 0 ? exports.REDACTED_SECRET : value;
|
|
107
|
+
if (Array.isArray(value))
|
|
108
|
+
return value.map(withholdAll);
|
|
109
|
+
if (!isRecord(value))
|
|
110
|
+
return value;
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const [key, child] of Object.entries(value))
|
|
113
|
+
out[key] = withholdAll(child);
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
/** Apply ONE declared path to `value`, returning a copy; a path that does not resolve is a no-op. */
|
|
117
|
+
function withholdPath(value, segments) {
|
|
118
|
+
const [head, ...rest] = segments;
|
|
119
|
+
if (head === undefined)
|
|
120
|
+
return withholdAll(value);
|
|
121
|
+
if (!isRecord(value) || !(head.key in value))
|
|
122
|
+
return value;
|
|
123
|
+
const child = value[head.key];
|
|
124
|
+
const next = head.each
|
|
125
|
+
? Array.isArray(child)
|
|
126
|
+
? child.map((el) => withholdPath(el, rest))
|
|
127
|
+
: child
|
|
128
|
+
: withholdPath(child, rest);
|
|
129
|
+
return { ...value, [head.key]: next };
|
|
130
|
+
}
|
|
89
131
|
/**
|
|
90
|
-
*
|
|
132
|
+
* The keyword BACKSTOP (D634), now at any depth: a credential-NAMED key's
|
|
133
|
+
* string value is withheld, and a non-secret string that is a URL loses its
|
|
134
|
+
* userinfo / credential params. Only strings are touched — a numeric
|
|
135
|
+
* `maxTokens` or an array called `eventTypeTokens` is not a credential.
|
|
91
136
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* the only credentials this repo persists are strings.
|
|
137
|
+
* This is no longer the declaration (D783): it catches what a driver has not
|
|
138
|
+
* yet declared, and `scripts/check-secret-fields-declared.ts` is what keeps
|
|
139
|
+
* that set from growing.
|
|
96
140
|
*/
|
|
97
|
-
function
|
|
141
|
+
function backstop(value) {
|
|
142
|
+
if (typeof value === 'string') {
|
|
143
|
+
return value.includes('://') ? (0, types_1.maskUrlCredentials)(value) : value;
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(value))
|
|
146
|
+
return value.map(backstop);
|
|
147
|
+
if (!isRecord(value))
|
|
148
|
+
return value;
|
|
98
149
|
const out = {};
|
|
99
|
-
for (const [key,
|
|
150
|
+
for (const [key, child] of Object.entries(value)) {
|
|
100
151
|
out[key] =
|
|
101
|
-
isSecretConfigKey(key) && typeof
|
|
152
|
+
isSecretConfigKey(key) && typeof child === 'string' && child.length > 0
|
|
102
153
|
? exports.REDACTED_SECRET
|
|
103
|
-
:
|
|
154
|
+
: backstop(child);
|
|
104
155
|
}
|
|
105
156
|
return out;
|
|
106
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Withhold a device `config` blob's secrets: first every DECLARED path
|
|
160
|
+
* (`secretConfigPaths`, at any depth), then the keyword backstop over what is
|
|
161
|
+
* left.
|
|
162
|
+
*
|
|
163
|
+
* An EMPTY value stays empty: "not configured" and "configured, withheld" are
|
|
164
|
+
* different facts, and flattening them would make a broken camera look set up.
|
|
165
|
+
*/
|
|
166
|
+
function redactConfigBlob(config, declared) {
|
|
167
|
+
let out = config;
|
|
168
|
+
for (const segments of declared)
|
|
169
|
+
out = withholdPath(out, segments);
|
|
170
|
+
return backstop(out);
|
|
171
|
+
}
|
|
107
172
|
/**
|
|
108
173
|
* Project a `DeviceInfo` (or an array of them) with its `config` redacted.
|
|
109
174
|
* Anything that is not a device row passes through untouched — a route that
|
|
@@ -117,7 +182,7 @@ function redactDeviceInfoSecrets(data) {
|
|
|
117
182
|
const config = data['config'];
|
|
118
183
|
if (!isRecord(config))
|
|
119
184
|
return data;
|
|
120
|
-
return { ...data, config:
|
|
185
|
+
return { ...data, config: redactConfigBlob(config, declaredPathsOf(data)) };
|
|
121
186
|
}
|
|
122
187
|
/**
|
|
123
188
|
* Project a `Record<parentId, DeviceInfo[]>` — the shape `getChildrenBatch`
|
|
@@ -349,7 +414,7 @@ function redactConfigEntries(data) {
|
|
|
349
414
|
* `streamBroker.getRtspEntry`, `deviceOps.getStreamSources`, …) hand a
|
|
350
415
|
* non-admin caller `rtsp://admin:hunter2@192.168.10.139/…` verbatim: a
|
|
351
416
|
* read-only token can open every camera by copy-pasting the string. These
|
|
352
|
-
* are NOT config blobs, so {@link
|
|
417
|
+
* are NOT config blobs, so {@link redactDeviceInfoSecrets} never sees them — the leak
|
|
353
418
|
* lives in a plain `url` / `mutedUrl` string field.
|
|
354
419
|
*/
|
|
355
420
|
const URL_FIELD_KEYS = ['url', 'mutedUrl'];
|
|
@@ -377,6 +442,27 @@ function redactStreamUrlSecrets(data) {
|
|
|
377
442
|
}
|
|
378
443
|
return out;
|
|
379
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Mask URL credentials in EVERY string of an arbitrary payload — the runtime
|
|
447
|
+
* state reads (`deviceState.getSnapshot` / `getCapSlice` / `getAllSnapshots`),
|
|
448
|
+
* the status aggregates and `streamCatalog.getCatalog`. Reolink persists its
|
|
449
|
+
* stream catalog (`rtsp://user:pass@…`, FLV `…&password=…`) in the
|
|
450
|
+
* `stream-catalog` slice, so a view-level read handed the camera password to
|
|
451
|
+
* any non-admin (D783). Only strings holding `://` are touched; no key is
|
|
452
|
+
* judged by its name here, so `eventTypeTokens` & co. keep their values.
|
|
453
|
+
*/
|
|
454
|
+
function maskUrlCredentialsDeep(data) {
|
|
455
|
+
if (typeof data === 'string')
|
|
456
|
+
return data.includes('://') ? (0, types_1.maskUrlCredentials)(data) : data;
|
|
457
|
+
if (Array.isArray(data))
|
|
458
|
+
return data.map(maskUrlCredentialsDeep);
|
|
459
|
+
if (!isRecord(data))
|
|
460
|
+
return data;
|
|
461
|
+
const out = {};
|
|
462
|
+
for (const [key, value] of Object.entries(data))
|
|
463
|
+
out[key] = maskUrlCredentialsDeep(value);
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
380
466
|
/**
|
|
381
467
|
* Every device-manager read whose response carries a device `config` blob, and
|
|
382
468
|
* the projection that cuts it.
|
|
@@ -408,8 +494,14 @@ exports.NON_ADMIN_CONFIG_REDACTED_METHODS = new Map([
|
|
|
408
494
|
['integrations.list', redactDeepSecrets],
|
|
409
495
|
['integrations.get', redactDeepSecrets],
|
|
410
496
|
['integrations.getByAddonId', redactDeepSecrets],
|
|
411
|
-
[
|
|
412
|
-
|
|
497
|
+
[
|
|
498
|
+
'deviceManager.getDeviceStatusAggregate',
|
|
499
|
+
(d) => maskUrlCredentialsDeep(redactStatusAggregateSetup(d)),
|
|
500
|
+
],
|
|
501
|
+
[
|
|
502
|
+
'deviceManager.getDeviceStatusAggregateBatch',
|
|
503
|
+
(d) => maskUrlCredentialsDeep(redactStatusAggregateBatchSetup(d)),
|
|
504
|
+
],
|
|
413
505
|
['deviceManager.getDevice', redactDeviceInfoSecrets],
|
|
414
506
|
['deviceManager.listAll', redactDeviceInfoSecrets],
|
|
415
507
|
['deviceManager.getChildren', redactDeviceInfoSecrets],
|
|
@@ -428,4 +520,10 @@ exports.NON_ADMIN_CONFIG_REDACTED_METHODS = new Map([
|
|
|
428
520
|
['deviceManager.getStreamSources', redactStreamUrlSecrets],
|
|
429
521
|
['streamBroker.getRtspEntry', redactStreamUrlSecrets],
|
|
430
522
|
['streamBroker.getAllRtspEntries', redactStreamUrlSecrets],
|
|
523
|
+
// ── Stream catalog + runtime state (D783) — Reolink persists its catalog,
|
|
524
|
+
// credentials inline, in the `stream-catalog` slice. Every URL is masked.
|
|
525
|
+
['streamCatalog.getCatalog', maskUrlCredentialsDeep],
|
|
526
|
+
['deviceState.getSnapshot', maskUrlCredentialsDeep],
|
|
527
|
+
['deviceState.getCapSlice', maskUrlCredentialsDeep],
|
|
528
|
+
['deviceState.getAllSnapshots', maskUrlCredentialsDeep],
|
|
431
529
|
]);
|
|
@@ -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: 164 Skipped (legacy): 3
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.LEGACY_SHAPE_SKIP = void 0;
|
|
8
8
|
exports.mountAllCaps = mountAllCaps;
|
|
@@ -269,6 +269,8 @@ function mountAllCaps(services) {
|
|
|
269
269
|
imageSettings: (0, generated_cap_routers_js_1.createCapRouter_imageSettings)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'image-settings'), remoteCapProxy),
|
|
270
270
|
integrations: (0, generated_cap_routers_js_1.createCapRouter_integrations)((_ctx) => reg?.getSingleton('integrations') ?? null, remoteCapProxy),
|
|
271
271
|
intercom: (0, generated_cap_routers_js_1.createCapRouter_intercom)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'intercom'), remoteCapProxy),
|
|
272
|
+
keychain: (0, generated_cap_routers_js_1.createCapRouter_keychain)((_ctx) => reg?.getSingleton('keychain') ?? null, remoteCapProxy),
|
|
273
|
+
keychainAdmin: (0, generated_cap_routers_js_1.createCapRouter_keychainAdmin)((_ctx) => reg?.getSingleton('keychain-admin') ?? null, remoteCapProxy),
|
|
272
274
|
lawnMowerControl: (0, generated_cap_routers_js_1.createCapRouter_lawnMowerControl)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'lawn-mower-control'), remoteCapProxy),
|
|
273
275
|
llm: (0, generated_cap_routers_js_1.createCapRouter_llm)((_ctx, addonId) => {
|
|
274
276
|
if (!reg)
|