@camstack/types 1.2.42 → 1.2.43
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.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/cap-call-context.d.ts +26 -0
- package/dist/capabilities/index.d.ts +3 -3
- package/dist/capabilities/pipeline-orchestrator.cap.d.ts +26 -0
- package/dist/capabilities/privacy-mask.cap.d.ts +69 -9
- package/dist/capabilities/recording.cap.d.ts +27 -0
- package/dist/capabilities/snapshot.cap.d.ts +1 -1
- package/dist/capabilities/stream-broker.cap.d.ts +4 -0
- package/dist/capabilities/stream-params.cap.d.ts +8 -4
- package/dist/device/device-profile.d.ts +12 -4
- package/dist/device/system-mirror.d.ts +11 -0
- package/dist/ffmpeg/encode-defaults.d.ts +18 -0
- package/dist/ffmpeg/invocation.d.ts +100 -2
- package/dist/ffmpeg/sharing-key.d.ts +54 -2
- package/dist/generated/addon-api.d.ts +32 -0
- package/dist/generated/device-proxy.d.ts +1 -1
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +749 -56
- package/dist/index.mjs +725 -57
- package/dist/interfaces/camera-switches.d.ts +163 -5
- package/dist/interfaces/inference-engine.d.ts +24 -3
- package/dist/pipeline/native-lease.d.ts +150 -0
- package/dist/{sleep-Cvi1JxZp.js → sleep-Bx9IIoT0.js} +38 -1
- package/dist/{sleep-BmNKsY7v.mjs → sleep-DtstvzWm.mjs} +33 -2
- package/dist/utils/addon-id.d.ts +30 -0
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* ## This file adds no state
|
|
6
6
|
*
|
|
7
7
|
* Every switch here is a VIEW onto an authority that already existed
|
|
8
|
-
* ([
|
|
8
|
+
* ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
|
|
9
9
|
* group is that there is exactly one place each function is turned off, and
|
|
10
10
|
* the group routes to it:
|
|
11
11
|
*
|
|
@@ -16,6 +16,40 @@
|
|
|
16
16
|
* | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
|
|
17
17
|
* | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
|
|
18
18
|
* | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
|
|
19
|
+
* | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
|
|
20
|
+
* | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
|
|
21
|
+
*
|
|
22
|
+
* ## The two switches whose authority is not on this server
|
|
23
|
+
*
|
|
24
|
+
* `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
|
|
25
|
+
* in "the group stores nothing" — it is the purest form of it: the camera
|
|
26
|
+
* holds the fact, every read is a read-through, and there is no server-side
|
|
27
|
+
* copy that could drift. Their availability therefore cannot come from
|
|
28
|
+
* `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
|
|
29
|
+
* is filtered out there); it comes from the cap's own camera-probed
|
|
30
|
+
* `privacyMask.getOptions()`, which is strictly more honest — it answers for
|
|
31
|
+
* THIS camera rather than for the device type
|
|
32
|
+
* ([D74](../../../../docs/decisions/adr-0074.md)).
|
|
33
|
+
*
|
|
34
|
+
* ## `privacy-mask` is the one row whose ON is not "the function is working"
|
|
35
|
+
*
|
|
36
|
+
* Every other switch means *this camera's function is doing its job*, so
|
|
37
|
+
* `enabled: false` is a thing an operator took away. `privacy-mask` means **the
|
|
38
|
+
* MASK is active** — `enabled: true` is video deliberately obscured. The
|
|
39
|
+
* polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
|
|
40
|
+
* (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
|
|
41
|
+
* a HomeKit toggle that disagreed with the app's toggle for the same camera is
|
|
42
|
+
* worse than either surface not having one.
|
|
43
|
+
*
|
|
44
|
+
* Two consequences follow and both are load-bearing:
|
|
45
|
+
*
|
|
46
|
+
* - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
|
|
47
|
+
* exactly this row. With the polarity above, every camera that has NOT drawn
|
|
48
|
+
* a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
|
|
49
|
+
* normal, healthy state of most cameras rendered as an operator disablement.
|
|
50
|
+
* - **Its cost line names BOTH directions.** `costWhenOff` is rendered
|
|
51
|
+
* unconditionally by both clients, so for this row it has to read correctly
|
|
52
|
+
* whichever way the switch is sitting.
|
|
19
53
|
*
|
|
20
54
|
* The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
|
|
21
55
|
* migrated the legacy `audioEnabled` / `pipelineEnabled` /
|
|
@@ -35,20 +69,34 @@
|
|
|
35
69
|
*/
|
|
36
70
|
import { z } from 'zod';
|
|
37
71
|
/**
|
|
38
|
-
* The
|
|
39
|
-
*
|
|
72
|
+
* The functions the operator named — five on 2026-08-05, plus the camera's own
|
|
73
|
+
* microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
|
|
74
|
+
* recognition and plate/LPR are per-step toggles on
|
|
40
75
|
* `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
|
|
41
|
-
* editor, not in a
|
|
76
|
+
* editor, not in a safety group.
|
|
42
77
|
*/
|
|
43
78
|
export declare const CameraSwitchIdSchema: z.ZodEnum<{
|
|
79
|
+
"privacy-mask": "privacy-mask";
|
|
44
80
|
"audio-analysis": "audio-analysis";
|
|
45
81
|
recording: "recording";
|
|
46
82
|
"stream-broker": "stream-broker";
|
|
47
83
|
notifications: "notifications";
|
|
48
84
|
"object-detection": "object-detection";
|
|
85
|
+
"device-audio": "device-audio";
|
|
49
86
|
}>;
|
|
50
87
|
export type CameraSwitchId = z.infer<typeof CameraSwitchIdSchema>;
|
|
51
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Stable render order — broadest blast radius first, and a source before the
|
|
90
|
+
* thing that consumes it. `device-audio` sits ABOVE `audio-analysis` because
|
|
91
|
+
* turning the microphone off leaves the analyzer with nothing to analyse; the
|
|
92
|
+
* reverse is not true.
|
|
93
|
+
*
|
|
94
|
+
* `privacy-mask` sits directly ABOVE `device-audio` because they are literal
|
|
95
|
+
* siblings — one cap, one device plane, video then audio — and NOT above
|
|
96
|
+
* `object-detection` despite feeding it: a mask blanks REGIONS, so its blast
|
|
97
|
+
* radius is partial, and the "broadest first" rule does not rank a partial
|
|
98
|
+
* control above a whole-function one.
|
|
99
|
+
*/
|
|
52
100
|
export declare const CAMERA_SWITCH_ORDER: readonly CameraSwitchId[];
|
|
53
101
|
/**
|
|
54
102
|
* WHERE the switch's state actually lives. A discriminated union rather than a
|
|
@@ -65,6 +113,12 @@ export declare const CameraSwitchAuthoritySchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
65
113
|
kind: z.ZodLiteral<"recording-config">;
|
|
66
114
|
}, z.core.$strip>, z.ZodObject<{
|
|
67
115
|
kind: z.ZodLiteral<"notification-mute">;
|
|
116
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
117
|
+
kind: z.ZodLiteral<"camera-audio">;
|
|
118
|
+
capName: z.ZodString;
|
|
119
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
120
|
+
kind: z.ZodLiteral<"camera-mask">;
|
|
121
|
+
capName: z.ZodString;
|
|
68
122
|
}, z.core.$strip>], "kind">;
|
|
69
123
|
export type CameraSwitchAuthority = z.infer<typeof CameraSwitchAuthoritySchema>;
|
|
70
124
|
/**
|
|
@@ -75,6 +129,7 @@ export type CameraSwitchAuthority = z.infer<typeof CameraSwitchAuthoritySchema>;
|
|
|
75
129
|
export declare const CameraSwitchUnavailableReasonSchema: z.ZodEnum<{
|
|
76
130
|
"no-provider": "no-provider";
|
|
77
131
|
"source-unreachable": "source-unreachable";
|
|
132
|
+
"not-configured": "not-configured";
|
|
78
133
|
}>;
|
|
79
134
|
export type CameraSwitchUnavailableReason = z.infer<typeof CameraSwitchUnavailableReasonSchema>;
|
|
80
135
|
/**
|
|
@@ -87,11 +142,13 @@ export type CameraSwitchUnavailableReason = z.infer<typeof CameraSwitchUnavailab
|
|
|
87
142
|
*/
|
|
88
143
|
export declare const CameraSwitchSchema: z.ZodObject<{
|
|
89
144
|
id: z.ZodEnum<{
|
|
145
|
+
"privacy-mask": "privacy-mask";
|
|
90
146
|
"audio-analysis": "audio-analysis";
|
|
91
147
|
recording: "recording";
|
|
92
148
|
"stream-broker": "stream-broker";
|
|
93
149
|
notifications: "notifications";
|
|
94
150
|
"object-detection": "object-detection";
|
|
151
|
+
"device-audio": "device-audio";
|
|
95
152
|
}>;
|
|
96
153
|
label: z.ZodString;
|
|
97
154
|
costWhenOff: z.ZodString;
|
|
@@ -99,6 +156,7 @@ export declare const CameraSwitchSchema: z.ZodObject<{
|
|
|
99
156
|
unavailableReason: z.ZodOptional<z.ZodEnum<{
|
|
100
157
|
"no-provider": "no-provider";
|
|
101
158
|
"source-unreachable": "source-unreachable";
|
|
159
|
+
"not-configured": "not-configured";
|
|
102
160
|
}>>;
|
|
103
161
|
enabled: z.ZodBoolean;
|
|
104
162
|
authority: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -110,6 +168,12 @@ export declare const CameraSwitchSchema: z.ZodObject<{
|
|
|
110
168
|
kind: z.ZodLiteral<"recording-config">;
|
|
111
169
|
}, z.core.$strip>, z.ZodObject<{
|
|
112
170
|
kind: z.ZodLiteral<"notification-mute">;
|
|
171
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
172
|
+
kind: z.ZodLiteral<"camera-audio">;
|
|
173
|
+
capName: z.ZodString;
|
|
174
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
175
|
+
kind: z.ZodLiteral<"camera-mask">;
|
|
176
|
+
capName: z.ZodString;
|
|
113
177
|
}, z.core.$strip>], "kind">;
|
|
114
178
|
}, z.core.$strip>;
|
|
115
179
|
export type CameraSwitch = z.infer<typeof CameraSwitchSchema>;
|
|
@@ -118,11 +182,13 @@ export declare const CameraSwitchGroupSchema: z.ZodObject<{
|
|
|
118
182
|
deviceId: z.ZodNumber;
|
|
119
183
|
switches: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
120
184
|
id: z.ZodEnum<{
|
|
185
|
+
"privacy-mask": "privacy-mask";
|
|
121
186
|
"audio-analysis": "audio-analysis";
|
|
122
187
|
recording: "recording";
|
|
123
188
|
"stream-broker": "stream-broker";
|
|
124
189
|
notifications: "notifications";
|
|
125
190
|
"object-detection": "object-detection";
|
|
191
|
+
"device-audio": "device-audio";
|
|
126
192
|
}>;
|
|
127
193
|
label: z.ZodString;
|
|
128
194
|
costWhenOff: z.ZodString;
|
|
@@ -130,6 +196,7 @@ export declare const CameraSwitchGroupSchema: z.ZodObject<{
|
|
|
130
196
|
unavailableReason: z.ZodOptional<z.ZodEnum<{
|
|
131
197
|
"no-provider": "no-provider";
|
|
132
198
|
"source-unreachable": "source-unreachable";
|
|
199
|
+
"not-configured": "not-configured";
|
|
133
200
|
}>>;
|
|
134
201
|
enabled: z.ZodBoolean;
|
|
135
202
|
authority: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -141,6 +208,12 @@ export declare const CameraSwitchGroupSchema: z.ZodObject<{
|
|
|
141
208
|
kind: z.ZodLiteral<"recording-config">;
|
|
142
209
|
}, z.core.$strip>, z.ZodObject<{
|
|
143
210
|
kind: z.ZodLiteral<"notification-mute">;
|
|
211
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
212
|
+
kind: z.ZodLiteral<"camera-audio">;
|
|
213
|
+
capName: z.ZodString;
|
|
214
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
215
|
+
kind: z.ZodLiteral<"camera-mask">;
|
|
216
|
+
capName: z.ZodString;
|
|
144
217
|
}, z.core.$strip>], "kind">;
|
|
145
218
|
}, z.core.$strip>>>;
|
|
146
219
|
fetchedAt: z.ZodNumber;
|
|
@@ -154,12 +227,32 @@ export type CameraSwitchGroup = z.infer<typeof CameraSwitchGroupSchema>;
|
|
|
154
227
|
*/
|
|
155
228
|
export declare const DETECTION_PIPELINE_CAP_NAME = "detection-pipeline";
|
|
156
229
|
export declare const AUDIO_ANALYSIS_CAP_NAME = "audio-analysis";
|
|
230
|
+
/**
|
|
231
|
+
* The device-NATIVE cap that owns what the camera does not capture — masked
|
|
232
|
+
* video regions and, since 2026-08-07, the microphone. Named here because the
|
|
233
|
+
* `camera-audio` authority, the orchestrator's gather and the fake harness all
|
|
234
|
+
* have to agree on it.
|
|
235
|
+
*/
|
|
236
|
+
export declare const PRIVACY_MASK_CAP_NAME = "privacy-mask";
|
|
157
237
|
/** Static half of a switch: everything that does not depend on a device. */
|
|
158
238
|
export interface CameraSwitchDescriptor {
|
|
159
239
|
readonly id: CameraSwitchId;
|
|
160
240
|
readonly label: string;
|
|
161
241
|
readonly costWhenOff: string;
|
|
162
242
|
readonly authority: CameraSwitchAuthority;
|
|
243
|
+
/**
|
|
244
|
+
* May `enabled: false` on this row appear in {@link switchedOffIds}?
|
|
245
|
+
*
|
|
246
|
+
* `true` for every switch that means "this camera's function is doing its
|
|
247
|
+
* job", where OFF is something a person took away and a status surface must
|
|
248
|
+
* render as disabled rather than broken.
|
|
249
|
+
*
|
|
250
|
+
* `false` for `privacy-mask` alone, and only because its polarity is
|
|
251
|
+
* inverted: OFF there means the mask is not obscuring anything, which is the
|
|
252
|
+
* ordinary state of nearly every camera. Reporting it would put a
|
|
253
|
+
* `switchedOff` badge on healthy cameras and drown the badge that matters.
|
|
254
|
+
*/
|
|
255
|
+
readonly countsAsSwitchedOff: boolean;
|
|
163
256
|
}
|
|
164
257
|
/**
|
|
165
258
|
* THE catalog. One entry per switch; the cost lines are the operator-facing
|
|
@@ -195,6 +288,65 @@ export interface CameraSwitchDerivationInput {
|
|
|
195
288
|
readonly recordingEnabled: boolean | null;
|
|
196
289
|
/** `notificationRules` per-camera mute; `null` when the cap did not answer. */
|
|
197
290
|
readonly notificationsMuted: boolean | null;
|
|
291
|
+
/**
|
|
292
|
+
* The camera's own microphone, read through the `privacy-mask` native cap.
|
|
293
|
+
* `null` = the cap is bound to this camera but did not answer.
|
|
294
|
+
*
|
|
295
|
+
* A camera with no `privacy-mask` provider at all is `{ supported: false }`,
|
|
296
|
+
* not `null` — "this camera has no such control" and "we could not read it"
|
|
297
|
+
* are different answers and the operator is shown different copy for each.
|
|
298
|
+
*/
|
|
299
|
+
readonly deviceAudio: DeviceAudioSwitchState | null;
|
|
300
|
+
/**
|
|
301
|
+
* The camera's own VIDEO privacy mask, read through the same `privacy-mask`
|
|
302
|
+
* native cap and the same pair of round trips as {@link deviceAudio} — one
|
|
303
|
+
* `getOptions` + one `getStatus` answer both, so the second switch is free.
|
|
304
|
+
*
|
|
305
|
+
* `null` = the cap is bound to this camera but did not answer.
|
|
306
|
+
*/
|
|
307
|
+
readonly privacyMask: PrivacyMaskSwitchState | null;
|
|
308
|
+
}
|
|
309
|
+
/** What the camera-probed audio switch reported for one camera. */
|
|
310
|
+
export interface DeviceAudioSwitchState {
|
|
311
|
+
/**
|
|
312
|
+
* `privacyMask.getOptions().supportsAudioMute` — camera-probed, never a
|
|
313
|
+
* device-type guess. A camera that does not expose the control renders no
|
|
314
|
+
* switch rather than a toggle that silently does nothing.
|
|
315
|
+
*/
|
|
316
|
+
readonly supported: boolean;
|
|
317
|
+
/**
|
|
318
|
+
* `privacyMask.getStatus().audioEnabled` — the camera's current answer.
|
|
319
|
+
* `null` when unsupported, or when supported but unreadable; the second
|
|
320
|
+
* case yields `source-unreachable`, never a control defaulted to "on".
|
|
321
|
+
*/
|
|
322
|
+
readonly enabled: boolean | null;
|
|
323
|
+
}
|
|
324
|
+
/** What the camera-probed VIDEO privacy mask reported for one camera. */
|
|
325
|
+
export interface PrivacyMaskSwitchState {
|
|
326
|
+
/**
|
|
327
|
+
* Can this camera store privacy zones at all? Derived from the probed
|
|
328
|
+
* `getOptions()`: `maxRegions > 0` AND at least one supported shape kind.
|
|
329
|
+
* Reolink answers `maxNum: 0` / `supportedShapes: []` honestly on models
|
|
330
|
+
* that cannot, and that must render no control rather than a toggle whose
|
|
331
|
+
* write the camera discards.
|
|
332
|
+
*/
|
|
333
|
+
readonly supported: boolean;
|
|
334
|
+
/**
|
|
335
|
+
* How many zones are actually drawn on the camera right now
|
|
336
|
+
* (`getStatus().regions.length`). `null` = supported, but the status could
|
|
337
|
+
* not be read.
|
|
338
|
+
*
|
|
339
|
+
* **Zero is not "off", it is "nothing to switch".** Toggling the master
|
|
340
|
+
* enable with no zones changes not one pixel — the dead control D62 exists
|
|
341
|
+
* to remove — so it resolves to `not-configured`, which tells the operator
|
|
342
|
+
* to go and draw one instead of leaving them pressing a button.
|
|
343
|
+
*/
|
|
344
|
+
readonly configuredRegions: number | null;
|
|
345
|
+
/**
|
|
346
|
+
* `getStatus().enabled` — the camera's master mask flag, `true` when the
|
|
347
|
+
* zones are being blanked. `null` when unreadable; never defaulted.
|
|
348
|
+
*/
|
|
349
|
+
readonly enabled: boolean | null;
|
|
198
350
|
}
|
|
199
351
|
/**
|
|
200
352
|
* Pure derivation of the whole group. No I/O — the orchestrator gathers, this
|
|
@@ -213,5 +365,11 @@ export declare function deriveCameraSwitches(input: CameraSwitchDerivationInput)
|
|
|
213
365
|
* `switchedOff: ['object-detection']` was turned off; the same camera with an
|
|
214
366
|
* empty list is broken. Unavailable switches never appear — a function nobody
|
|
215
367
|
* provides was not switched off by anyone.
|
|
368
|
+
*
|
|
369
|
+
* `privacy-mask` never appears either, whichever way it is sitting, because its
|
|
370
|
+
* ON means "the mask is active" rather than "the function works"
|
|
371
|
+
* ({@link CameraSwitchDescriptor.countsAsSwitchedOff}). Without that filter the
|
|
372
|
+
* ordinary state of every camera nobody has masked would carry a
|
|
373
|
+
* "switched off" badge, and the badge that matters would be lost in it.
|
|
216
374
|
*/
|
|
217
375
|
export declare function switchedOffIds(switches: readonly CameraSwitch[]): readonly CameraSwitchId[];
|
|
@@ -16,16 +16,36 @@ import type { RawFrameFormat } from '../types/io.js';
|
|
|
16
16
|
* raw buffer to JPEG internally rather than throwing — `infer()` must
|
|
17
17
|
* always succeed for either kind.
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Fields every `InferenceInput` variant carries regardless of pixel transport.
|
|
21
|
+
*/
|
|
22
|
+
interface InferenceInputCommon {
|
|
23
|
+
/**
|
|
24
|
+
* The camera this frame belongs to — **diagnostic only**. It changes no
|
|
25
|
+
* dispatch decision, no deadline and no shed rule; it exists so an inference
|
|
26
|
+
* failure inside a SHARED pool can name the camera that paid for it.
|
|
27
|
+
*
|
|
28
|
+
* A `SharedInferencePool` serves every camera on its
|
|
29
|
+
* `(node, runtime, device)`, so its `inference request timed out` and
|
|
30
|
+
* `worker saturated` lines could describe the pool and nothing else — and
|
|
31
|
+
* "why is 617 worse than 615?" is the question those lines always get asked.
|
|
32
|
+
*
|
|
33
|
+
* Omitted by callers with no camera (commands, model loads, the benchmark);
|
|
34
|
+
* the failure line then carries no `deviceId` tag rather than a fabricated
|
|
35
|
+
* one.
|
|
36
|
+
*/
|
|
37
|
+
readonly deviceId?: number;
|
|
38
|
+
}
|
|
39
|
+
export type InferenceInput = ({
|
|
20
40
|
readonly kind: 'jpeg';
|
|
21
41
|
readonly data: Buffer;
|
|
22
|
-
} | {
|
|
42
|
+
} & InferenceInputCommon) | ({
|
|
23
43
|
readonly kind: 'raw';
|
|
24
44
|
readonly data: Buffer;
|
|
25
45
|
readonly width: number;
|
|
26
46
|
readonly height: number;
|
|
27
47
|
readonly format: RawFrameFormat;
|
|
28
|
-
};
|
|
48
|
+
} & InferenceInputCommon);
|
|
29
49
|
export interface IInferenceEngine {
|
|
30
50
|
readonly runtime: DetectionRuntime;
|
|
31
51
|
readonly device: DetectionDevice;
|
|
@@ -34,3 +54,4 @@ export interface IInferenceEngine {
|
|
|
34
54
|
run?(input: Float32Array, inputShape: readonly number[]): Promise<Float32Array>;
|
|
35
55
|
dispose(): Promise<void>;
|
|
36
56
|
}
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE native-frame **lease** knobs — TTL, RAM budget and demand window for the
|
|
3
|
+
* decode worker's native-resolution frame retention.
|
|
4
|
+
*
|
|
5
|
+
* ## Why they live here and not in the addon that reads them
|
|
6
|
+
*
|
|
7
|
+
* The WRITER is `pipeline-orchestrator` (the cluster-wide settings authority);
|
|
8
|
+
* the READER is a private child process of `addon-pipeline`'s pipeline-runner.
|
|
9
|
+
* Addons never import each other, so a key owned by either side would have to
|
|
10
|
+
* be hand-copied by the other — and a hand-copied key is how a setting silently
|
|
11
|
+
* stops arriving while both sides still look correct. Same reasoning, same
|
|
12
|
+
* placement as `detail-crop.ts` (D52's "one cluster-wide orchestrator setting").
|
|
13
|
+
*
|
|
14
|
+
* ## Why cluster-wide and not per-node
|
|
15
|
+
*
|
|
16
|
+
* The lease is a per-decode-worker RAM window. Its purpose — the late
|
|
17
|
+
* cross-process native crop landing on a full-resolution frame rather than the
|
|
18
|
+
* ≤640 detection fallback — is a property of the PIPELINE, not of a node's
|
|
19
|
+
* hardware: a per-node TTL would mean the same camera produces different crop
|
|
20
|
+
* quality depending on which node the balancer placed it on, and nobody could
|
|
21
|
+
* tell that from the stored media. Node-level RAM pressure is already handled
|
|
22
|
+
* by the per-session budget ceiling, which is itself one of these knobs.
|
|
23
|
+
*
|
|
24
|
+
* ## What each knob costs
|
|
25
|
+
*
|
|
26
|
+
* A retained frame is a full NATIVE-resolution copy in system RAM. With the
|
|
27
|
+
* default pinned-RGB24 lease path (`CAMSTACK_SESSION_PINNED_RGB_CROP`, on):
|
|
28
|
+
* 4K ≈ 24.9 MB/frame, 1080p ≈ 6.2 MB/frame. On the YUV420P path (flag off, and
|
|
29
|
+
* for software-decoded sessions): 4K ≈ 12.4 MB, 1080p ≈ 3.1 MB. Worst-case
|
|
30
|
+
* resident RAM for ONE busy camera ≈ frameBytes × deliveredFps × ttlSeconds,
|
|
31
|
+
* clamped by the budget ceiling. See `docs/design/decode-path.md` → "Lease
|
|
32
|
+
* admission" for what actually gets admitted.
|
|
33
|
+
*/
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
import type { HydratedSettingsView } from './detail-crop.js';
|
|
36
|
+
/**
|
|
37
|
+
* Store identity of the lease knobs in `pipeline-orchestrator`'s GLOBAL
|
|
38
|
+
* (cluster-wide) settings. Keys are unique across that addon's whole schema, so
|
|
39
|
+
* the reader can walk every section instead of trusting the section id.
|
|
40
|
+
*/
|
|
41
|
+
export declare const NATIVE_LEASE_SECTION_ID = "native-lease";
|
|
42
|
+
export declare const NATIVE_LEASE_TTL_KEY = "nativeLeaseTtlMs";
|
|
43
|
+
export declare const NATIVE_LEASE_BUDGET_KEY = "nativeLeaseBudgetMb";
|
|
44
|
+
export declare const NATIVE_LEASE_ACTIVITY_KEY = "nativeLeaseActivityMs";
|
|
45
|
+
export declare const NATIVE_LEASE_ADMISSION_KEY = "nativeLeaseAdmission";
|
|
46
|
+
/**
|
|
47
|
+
* WHICH delivered frames the decode worker retains a native copy of.
|
|
48
|
+
*
|
|
49
|
+
* - `all` — every frame the worker delivered to the runner. The shipped
|
|
50
|
+
* behaviour, and the only correct one if something can ask for a crop of a
|
|
51
|
+
* frame the runner never sent to inference.
|
|
52
|
+
* - `inferred` — only the frames the runner ADMITTED to its detection queue.
|
|
53
|
+
* A native-crop request always names a `frameId` that rode an inference
|
|
54
|
+
* result, so that is the only set a request can name. How much it drops is
|
|
55
|
+
* the two-plane governor's admit ratio and nothing else: measured at ~50% on
|
|
56
|
+
* this cluster, not the ~80% the design sketch assumed, because the governor
|
|
57
|
+
* was not throttling as hard as the sketch supposed. Read
|
|
58
|
+
* `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
|
|
59
|
+
* of you rather than quoting a number from here. The newest delivered frame is
|
|
60
|
+
* croppable regardless — it is still the worker's reserved slot, not a lease —
|
|
61
|
+
* which covers the one-frame race between a mark and the supersede that
|
|
62
|
+
* consumes it.
|
|
63
|
+
*/
|
|
64
|
+
export declare const NativeLeaseAdmissionSchema: z.ZodEnum<{
|
|
65
|
+
all: "all";
|
|
66
|
+
inferred: "inferred";
|
|
67
|
+
}>;
|
|
68
|
+
export type NativeLeaseAdmission = z.infer<typeof NativeLeaseAdmissionSchema>;
|
|
69
|
+
/**
|
|
70
|
+
* Operator-tunable native-lease settings. Bounds are enforced HERE (not only in
|
|
71
|
+
* the slider) because the value also travels to a forked child process, where a
|
|
72
|
+
* junk number would silently become a 0-length or unbounded retention window.
|
|
73
|
+
*/
|
|
74
|
+
export declare const NativeLeaseSettingsSchema: z.ZodObject<{
|
|
75
|
+
ttlMs: z.ZodNumber;
|
|
76
|
+
budgetMb: z.ZodNumber;
|
|
77
|
+
activityMs: z.ZodNumber;
|
|
78
|
+
admission: z.ZodEnum<{
|
|
79
|
+
all: "all";
|
|
80
|
+
inferred: "inferred";
|
|
81
|
+
}>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
export type NativeLeaseSettings = z.infer<typeof NativeLeaseSettingsSchema>;
|
|
84
|
+
/**
|
|
85
|
+
* The values in force when the operator has set nothing — byte-for-byte the
|
|
86
|
+
* constants the decode worker shipped with as env-var defaults, so making these
|
|
87
|
+
* settings changed no behaviour on the day it landed.
|
|
88
|
+
*/
|
|
89
|
+
export declare const DEFAULT_NATIVE_LEASE_SETTINGS: NativeLeaseSettings;
|
|
90
|
+
/**
|
|
91
|
+
* Only the knobs the operator has ACTUALLY set. Distinguishing "set" from
|
|
92
|
+
* "absent" is what makes the documented precedence (setting > env > default)
|
|
93
|
+
* expressible: an absent knob leaves the env-var escape hatch working.
|
|
94
|
+
*/
|
|
95
|
+
export type NativeLeaseSettingsOverride = Partial<NativeLeaseSettings>;
|
|
96
|
+
/** Slider bounds for the operator-facing knobs (orchestrator settings UI). */
|
|
97
|
+
export declare const NATIVE_LEASE_TTL_FIELD: {
|
|
98
|
+
readonly min: 250;
|
|
99
|
+
readonly max: 10000;
|
|
100
|
+
readonly step: 50;
|
|
101
|
+
readonly default: number;
|
|
102
|
+
};
|
|
103
|
+
export declare const NATIVE_LEASE_BUDGET_FIELD: {
|
|
104
|
+
readonly min: 0;
|
|
105
|
+
readonly max: 4096;
|
|
106
|
+
readonly step: 64;
|
|
107
|
+
readonly default: number;
|
|
108
|
+
};
|
|
109
|
+
export declare const NATIVE_LEASE_ACTIVITY_FIELD: {
|
|
110
|
+
readonly min: 0;
|
|
111
|
+
readonly max: 120000;
|
|
112
|
+
readonly step: 1000;
|
|
113
|
+
readonly default: number;
|
|
114
|
+
};
|
|
115
|
+
/** Select options for the admission knob (orchestrator settings UI). */
|
|
116
|
+
export declare const NATIVE_LEASE_ADMISSION_FIELD: {
|
|
117
|
+
readonly options: readonly [{
|
|
118
|
+
readonly value: "all";
|
|
119
|
+
readonly label: "Every delivered frame";
|
|
120
|
+
}, {
|
|
121
|
+
readonly value: "inferred";
|
|
122
|
+
readonly label: "Only frames sent to inference";
|
|
123
|
+
}];
|
|
124
|
+
readonly default: "all" | "inferred";
|
|
125
|
+
};
|
|
126
|
+
/** The four knobs, as a key union. */
|
|
127
|
+
export type NativeLeaseKnob = keyof NativeLeaseSettings;
|
|
128
|
+
/** The three knobs whose value is a bounded integer (everything but `admission`). */
|
|
129
|
+
export type NativeLeaseNumberKnob = 'ttlMs' | 'budgetMb' | 'activityMs';
|
|
130
|
+
/**
|
|
131
|
+
* Narrow a FLAT settings record to the knobs the operator set.
|
|
132
|
+
*
|
|
133
|
+
* Per-FIELD parse, deliberately: a junk TTL must not also discard a valid
|
|
134
|
+
* budget. An absent, out-of-bounds or default-valued knob is OMITTED (not
|
|
135
|
+
* clamped, not defaulted) so the caller can still fall through to the env
|
|
136
|
+
* override — clamping here would turn a typo into a value nobody chose. See
|
|
137
|
+
* {@link readKnob} for why the default counts as unset.
|
|
138
|
+
*/
|
|
139
|
+
export declare function readNativeLeaseOverride(config: Readonly<Record<string, unknown>>): NativeLeaseSettingsOverride;
|
|
140
|
+
/**
|
|
141
|
+
* Extract the operator's lease overrides from an
|
|
142
|
+
* `addon-settings.getGlobalSettings` payload.
|
|
143
|
+
*
|
|
144
|
+
* Walks EVERY section rather than looking inside {@link NATIVE_LEASE_SECTION_ID}
|
|
145
|
+
* alone: the keys are unique across the addon's schema, and a section rename
|
|
146
|
+
* must not silently revert the whole cluster to the defaults. A `null` payload
|
|
147
|
+
* (addon mid-boot) means "operator set nothing" — the env/default fallback then
|
|
148
|
+
* applies, which is the correct read of "I could not ask".
|
|
149
|
+
*/
|
|
150
|
+
export declare function pickNativeLeaseOverride(view: HydratedSettingsView | null): NativeLeaseSettingsOverride;
|
|
@@ -1934,9 +1934,38 @@ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
|
|
|
1934
1934
|
/**
|
|
1935
1935
|
* Build the tRPC request options that pin a single capability call to `nodeId`.
|
|
1936
1936
|
* Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
|
|
1937
|
+
*
|
|
1938
|
+
* ## The id is normalised here, and it has to be
|
|
1939
|
+
*
|
|
1940
|
+
* A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
|
|
1941
|
+
* worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
|
|
1942
|
+
* compares a pin against real node ids, so such a pin matches nothing and the
|
|
1943
|
+
* call fails with `no provider registered for cap "…"`. The local-first
|
|
1944
|
+
* resolver already guarded against this (`localNodeId.split('/')[0]`), which
|
|
1945
|
+
* made the hazard invisible: unpinned calls worked, and only an explicit pin —
|
|
1946
|
+
* the thing you reach for when you specifically need THIS node — silently
|
|
1947
|
+
* addressed a node that does not exist.
|
|
1948
|
+
*
|
|
1949
|
+
* Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
|
|
1950
|
+
* own node to read the host's hardware-decode backend. It never once answered,
|
|
1951
|
+
* so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
|
|
1952
|
+
* while D67's whole premise was that the decoder addon is the authority on
|
|
1953
|
+
* hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
|
|
1954
|
+
* hardware", which was false.
|
|
1955
|
+
*
|
|
1956
|
+
* Normalising in the ONE constructor fixes every caller at once, which is why
|
|
1957
|
+
* it is here and not at the call sites.
|
|
1937
1958
|
*/
|
|
1938
1959
|
function nodePin(nodeId) {
|
|
1939
|
-
return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
|
|
1960
|
+
return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
|
|
1964
|
+
* is idempotent, so passing an already-clean id costs nothing.
|
|
1965
|
+
*/
|
|
1966
|
+
function toNodeId(idOrRunnerId) {
|
|
1967
|
+
const head = idOrRunnerId.split("/")[0];
|
|
1968
|
+
return head === void 0 || head.length === 0 ? idOrRunnerId : head;
|
|
1940
1969
|
}
|
|
1941
1970
|
/**
|
|
1942
1971
|
* Read a per-call node pin out of a tRPC `op.context` (transport side).
|
|
@@ -3180,6 +3209,7 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3180
3209
|
privacyMask: {
|
|
3181
3210
|
getOptions: (input) => dispatch("privacy-mask", "privacyMask", "getOptions", "query", input),
|
|
3182
3211
|
setMask: (input) => dispatch("privacy-mask", "privacyMask", "setMask", "mutation", input),
|
|
3212
|
+
setAudioEnabled: (input) => dispatch("privacy-mask", "privacyMask", "setAudioEnabled", "mutation", input),
|
|
3183
3213
|
getStatus: (input) => dispatch("privacy-mask", "privacyMask", "getStatus", "query", input)
|
|
3184
3214
|
},
|
|
3185
3215
|
ptz: {
|
|
@@ -3414,6 +3444,7 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3414
3444
|
getDeviceConfig: (input) => dispatchSystem("recording", "getDeviceConfig", "query", input),
|
|
3415
3445
|
locateSegment: (input) => dispatchSystem("recording", "locateSegment", "query", input),
|
|
3416
3446
|
readSegmentBytes: (input) => dispatchSystem("recording", "readSegmentBytes", "query", input),
|
|
3447
|
+
readGopBytes: (input) => dispatchSystem("recording", "readGopBytes", "query", input),
|
|
3417
3448
|
setDeviceConfig: (input) => dispatchSystem("recording", "setDeviceConfig", "mutation", input),
|
|
3418
3449
|
rescanStorage: (input) => dispatchSystem("recording", "rescanStorage", "mutation", input),
|
|
3419
3450
|
pruneFootage: (input) => dispatchSystem("recording", "pruneFootage", "mutation", input),
|
|
@@ -4129,6 +4160,12 @@ Object.defineProperty(exports, "systemMethod", {
|
|
|
4129
4160
|
return systemMethod;
|
|
4130
4161
|
}
|
|
4131
4162
|
});
|
|
4163
|
+
Object.defineProperty(exports, "toNodeId", {
|
|
4164
|
+
enumerable: true,
|
|
4165
|
+
get: function() {
|
|
4166
|
+
return toNodeId;
|
|
4167
|
+
}
|
|
4168
|
+
});
|
|
4132
4169
|
Object.defineProperty(exports, "viewerUiCapability", {
|
|
4133
4170
|
enumerable: true,
|
|
4134
4171
|
get: function() {
|
|
@@ -1934,9 +1934,38 @@ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
|
|
|
1934
1934
|
/**
|
|
1935
1935
|
* Build the tRPC request options that pin a single capability call to `nodeId`.
|
|
1936
1936
|
* Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
|
|
1937
|
+
*
|
|
1938
|
+
* ## The id is normalised here, and it has to be
|
|
1939
|
+
*
|
|
1940
|
+
* A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
|
|
1941
|
+
* worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
|
|
1942
|
+
* compares a pin against real node ids, so such a pin matches nothing and the
|
|
1943
|
+
* call fails with `no provider registered for cap "…"`. The local-first
|
|
1944
|
+
* resolver already guarded against this (`localNodeId.split('/')[0]`), which
|
|
1945
|
+
* made the hazard invisible: unpinned calls worked, and only an explicit pin —
|
|
1946
|
+
* the thing you reach for when you specifically need THIS node — silently
|
|
1947
|
+
* addressed a node that does not exist.
|
|
1948
|
+
*
|
|
1949
|
+
* Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
|
|
1950
|
+
* own node to read the host's hardware-decode backend. It never once answered,
|
|
1951
|
+
* so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
|
|
1952
|
+
* while D67's whole premise was that the decoder addon is the authority on
|
|
1953
|
+
* hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
|
|
1954
|
+
* hardware", which was false.
|
|
1955
|
+
*
|
|
1956
|
+
* Normalising in the ONE constructor fixes every caller at once, which is why
|
|
1957
|
+
* it is here and not at the call sites.
|
|
1937
1958
|
*/
|
|
1938
1959
|
function nodePin(nodeId) {
|
|
1939
|
-
return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
|
|
1960
|
+
return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
|
|
1964
|
+
* is idempotent, so passing an already-clean id costs nothing.
|
|
1965
|
+
*/
|
|
1966
|
+
function toNodeId(idOrRunnerId) {
|
|
1967
|
+
const head = idOrRunnerId.split("/")[0];
|
|
1968
|
+
return head === void 0 || head.length === 0 ? idOrRunnerId : head;
|
|
1940
1969
|
}
|
|
1941
1970
|
/**
|
|
1942
1971
|
* Read a per-call node pin out of a tRPC `op.context` (transport side).
|
|
@@ -3180,6 +3209,7 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3180
3209
|
privacyMask: {
|
|
3181
3210
|
getOptions: (input) => dispatch("privacy-mask", "privacyMask", "getOptions", "query", input),
|
|
3182
3211
|
setMask: (input) => dispatch("privacy-mask", "privacyMask", "setMask", "mutation", input),
|
|
3212
|
+
setAudioEnabled: (input) => dispatch("privacy-mask", "privacyMask", "setAudioEnabled", "mutation", input),
|
|
3183
3213
|
getStatus: (input) => dispatch("privacy-mask", "privacyMask", "getStatus", "query", input)
|
|
3184
3214
|
},
|
|
3185
3215
|
ptz: {
|
|
@@ -3414,6 +3444,7 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3414
3444
|
getDeviceConfig: (input) => dispatchSystem("recording", "getDeviceConfig", "query", input),
|
|
3415
3445
|
locateSegment: (input) => dispatchSystem("recording", "locateSegment", "query", input),
|
|
3416
3446
|
readSegmentBytes: (input) => dispatchSystem("recording", "readSegmentBytes", "query", input),
|
|
3447
|
+
readGopBytes: (input) => dispatchSystem("recording", "readGopBytes", "query", input),
|
|
3417
3448
|
setDeviceConfig: (input) => dispatchSystem("recording", "setDeviceConfig", "mutation", input),
|
|
3418
3449
|
rescanStorage: (input) => dispatchSystem("recording", "rescanStorage", "mutation", input),
|
|
3419
3450
|
pruneFootage: (input) => dispatchSystem("recording", "pruneFootage", "mutation", input),
|
|
@@ -3661,4 +3692,4 @@ function sleepCancellable(ms, signal) {
|
|
|
3661
3692
|
});
|
|
3662
3693
|
}
|
|
3663
3694
|
//#endregion
|
|
3664
|
-
export {
|
|
3695
|
+
export { FrameHandleSchema as $, method as A, readinessKey as B, DeviceType as C, collectHydratedFieldEntries as Ct, event as D, DEVICE_STATUS_METHOD as E, resolveHydratedFieldValue as Et, readNodePin as F, CamProfileSchema as G, BrokerStatsSchema as H, toNodeId as I, CameraStreamSchema as J, CamStreamKindSchema as K, ReadinessRegistry as L, systemMethod as M, CAP_NODE_PIN_CONTEXT_KEY as N, expandCapMethods as O, nodePin as P, FrameHandleFormatSchema as Q, ReadinessTimeoutError as R, DeviceRole as S, WELL_KNOWN_TAB_MAP as St, DEVICE_SETTINGS_CONTRIBUTION_METHODS as T, hydrateSchema as Tt, BrokerStatusSchema as U, scopeKey as V, CAM_PROFILE_ORDER as W, DecodedFrameSchema as X, DecodedAudioChunkSchema as Y, EncodedPacketSchema as Z, RawStateResultSchema as _, createDurableState as _t, asJsonObject as a, SubscribeAudioChunksInputSchema as at, ChargingStatus as b, isEvent as bt, parseJsonArray as c, SubscribeFramesResultSchema as ct, DEVICE_SCOPED_CAPS as d, parseProfileBrokerId as dt, ProfileRtspEntrySchema as et, isDeviceScopedCap as f, selectAssignedProfileSlots as ft, createSliceHandle as g, normalizeAddonInitResult as gt, createMirrorSource as h, BaseAddon as ht, asJsonArray as i, StreamSourceSchema as it, resolveCapMount as j, isDeviceConfigCap as k, parseJsonObject as l, makeProfileBrokerId as lt, createLazyTrpcSource as m, DisposerChain as mt, sleepCancellable as n, ProfileSlotStatusSchema as nt, asNumber as o, SubscribeAudioChunksResultSchema as ot, createDeviceProxy as p, DATAPLANE_SECRET_HEADER as pt, CamStreamResolutionSchema as q, asBoolean as r, StreamSourceEntrySchema$1 as rt, asString as s, SubscribeFramesInputSchema as st, sleep as t, ProfileSlotSchema as tt, parseJsonUnknown as u, makeSourceBrokerId as ut, deviceOpsCapability as v, createEvent as vt, adminUiCapability as w, collectHydratedFieldValues as wt, DeviceFeature as x, WELL_KNOWN_TABS as xt, viewerUiCapability as y, emitReadiness as yt, emitDownForOwnedCaps as z };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The addon id as the REGISTRY, the wire and the durable stores spell it.
|
|
3
|
+
*
|
|
4
|
+
* `AddonContext.id` does not agree with itself across the two context
|
|
5
|
+
* factories:
|
|
6
|
+
*
|
|
7
|
+
* - a FORKED addon gets the bare manifest id
|
|
8
|
+
* (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
|
|
9
|
+
* - an addon co-located in hub-main gets it PREFIXED
|
|
10
|
+
* (`server/backend/src/core/addon/addon-registry.service.ts` →
|
|
11
|
+
* ``id: `addon:${addonId}` ``).
|
|
12
|
+
*
|
|
13
|
+
* Everything an addon might compare `ctx.id` AGAINST carries the bare form:
|
|
14
|
+
* `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
|
|
15
|
+
* `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
|
|
16
|
+
*
|
|
17
|
+
* So `entry.providerAddonId === this.ctx.id` is silently, permanently false
|
|
18
|
+
* for a builtin — and only for a builtin, which is why it survives a green
|
|
19
|
+
* suite whose fake supplies the bare id. That is exactly how camera 615's
|
|
20
|
+
* virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
|
|
21
|
+
* binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
|
|
22
|
+
*
|
|
23
|
+
* Route every comparison between `ctx.id` and a registry/store addon id
|
|
24
|
+
* through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
|
|
25
|
+
* enforces it in the processes where the prefix exists.
|
|
26
|
+
*/
|
|
27
|
+
/** The manifest id, whichever spelling of `ctx.id` you were handed. */
|
|
28
|
+
export declare function bareAddonId(id: string): string;
|
|
29
|
+
/** True when both ids name the same addon, prefixed or not. */
|
|
30
|
+
export declare function isSameAddonId(a: string, b: string): boolean;
|