@camstack/addon-provider-rtsp 0.1.27 → 0.1.28
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 +556 -96
- package/dist/addon.js.map +1 -1
- package/dist/addon.mjs +556 -96
- package/dist/addon.mjs.map +1 -1
- package/package.json +3 -3
package/dist/addon.mjs
CHANGED
|
@@ -5081,6 +5081,7 @@ const WELL_KNOWN_TABS = [
|
|
|
5081
5081
|
{ id: "osd", label: "OSD", icon: "type", order: 18 },
|
|
5082
5082
|
{ id: "stream-broker", label: "Stream Broker", icon: "radio", order: 20 },
|
|
5083
5083
|
{ id: "streaming", label: "Streaming", icon: "video", order: 35 },
|
|
5084
|
+
{ id: "ptz", label: "PTZ", icon: "move", order: 40 },
|
|
5084
5085
|
{ id: "pipeline", label: "Detection Pipeline", icon: "cpu", order: 39 },
|
|
5085
5086
|
{ id: "zones", label: "Detection", icon: "shapes", order: 38 },
|
|
5086
5087
|
{ id: "live-stats", label: "Live Stats", icon: "activity", order: 39 },
|
|
@@ -5148,6 +5149,9 @@ function hydrateField(field, values) {
|
|
|
5148
5149
|
return { ...field, value: items };
|
|
5149
5150
|
}
|
|
5150
5151
|
const rawValue = storedValue !== void 0 ? storedValue : defaultValue !== void 0 ? defaultValue : null;
|
|
5152
|
+
if (field.type === "password") {
|
|
5153
|
+
return { ...field, value: "" };
|
|
5154
|
+
}
|
|
5151
5155
|
const value = field.type === "textarea" && field.isJson && rawValue !== null && typeof rawValue === "object" ? JSON.stringify(rawValue, null, 2) : rawValue;
|
|
5152
5156
|
const hydrated = { ...field, value };
|
|
5153
5157
|
return hydrated;
|
|
@@ -5234,7 +5238,19 @@ const DecoderSessionConfigSchema = object({
|
|
|
5234
5238
|
* on every line so `grep tag=broker:5/high` filters one camera
|
|
5235
5239
|
* profile cleanly.
|
|
5236
5240
|
*/
|
|
5237
|
-
tag: string().optional()
|
|
5241
|
+
tag: string().optional(),
|
|
5242
|
+
/**
|
|
5243
|
+
* Where the session delivers decoded frames (Phase 5 / D9):
|
|
5244
|
+
*
|
|
5245
|
+
* - `'callback'` (default) — the legacy pixel path: decoded frames are
|
|
5246
|
+
* buffered as `DecodedFrame`s and drained via `pullFrames`.
|
|
5247
|
+
* - `'shm'` — the shared-memory frame plane: decoded frames are written
|
|
5248
|
+
* into an OS shared-memory ring and drained as zero-pixel
|
|
5249
|
+
* `FrameHandle`s via `pullHandles`. A session is one mode or the
|
|
5250
|
+
* other — `pullFrames` returns nothing for an `'shm'` session and
|
|
5251
|
+
* `pullHandles` returns nothing for a `'callback'` session.
|
|
5252
|
+
*/
|
|
5253
|
+
frameSink: _enum(["callback", "shm"]).default("callback")
|
|
5238
5254
|
});
|
|
5239
5255
|
const YAMNET_TO_MACRO = {
|
|
5240
5256
|
mapping: {
|
|
@@ -6018,6 +6034,53 @@ const DecodedFrameSchema = object({
|
|
|
6018
6034
|
format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
|
|
6019
6035
|
timestamp: number()
|
|
6020
6036
|
});
|
|
6037
|
+
const FrameHandleSchema = object({
|
|
6038
|
+
shmId: string(),
|
|
6039
|
+
slot: number().int().nonnegative(),
|
|
6040
|
+
seq: number().int().nonnegative(),
|
|
6041
|
+
width: number().int().positive(),
|
|
6042
|
+
height: number().int().positive(),
|
|
6043
|
+
format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
|
|
6044
|
+
pts: number(),
|
|
6045
|
+
byteLength: number().int().nonnegative(),
|
|
6046
|
+
nodeId: string(),
|
|
6047
|
+
slotCount: number().int().positive()
|
|
6048
|
+
});
|
|
6049
|
+
const FrameHandleFormatSchema = _enum(["rgb", "bgr", "yuv420", "gray"]);
|
|
6050
|
+
const SubscribeFramesInputSchema = object({
|
|
6051
|
+
brokerId: string(),
|
|
6052
|
+
format: FrameHandleFormatSchema,
|
|
6053
|
+
/**
|
|
6054
|
+
* Optional reader-side cadence hint in frames per second. The broker does
|
|
6055
|
+
* NOT throttle — latest-wins ring reads drop frames implicitly for a slow
|
|
6056
|
+
* consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
|
|
6057
|
+
* the consumer can pace its own `pullFrameHandles` polling.
|
|
6058
|
+
*/
|
|
6059
|
+
maxFps: number().positive().optional(),
|
|
6060
|
+
/** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
|
|
6061
|
+
tag: string().optional()
|
|
6062
|
+
});
|
|
6063
|
+
const SubscribeFramesResultSchema = object({
|
|
6064
|
+
/** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
|
|
6065
|
+
subscriptionId: string(),
|
|
6066
|
+
/** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
|
|
6067
|
+
maxFps: number().nonnegative()
|
|
6068
|
+
});
|
|
6069
|
+
const DecodedAudioChunkSchema = object({
|
|
6070
|
+
data: _instanceof(Uint8Array),
|
|
6071
|
+
sampleRate: number().int().positive(),
|
|
6072
|
+
channels: number().int().positive(),
|
|
6073
|
+
timestamp: number()
|
|
6074
|
+
});
|
|
6075
|
+
const SubscribeAudioChunksInputSchema = object({
|
|
6076
|
+
brokerId: string(),
|
|
6077
|
+
/** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
|
|
6078
|
+
tag: string().optional()
|
|
6079
|
+
});
|
|
6080
|
+
const SubscribeAudioChunksResultSchema = object({
|
|
6081
|
+
/** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
|
|
6082
|
+
subscriptionId: string()
|
|
6083
|
+
});
|
|
6021
6084
|
const BrokerStatusSchema$1 = _enum(["idle", "connecting", "streaming", "error", "stopped"]);
|
|
6022
6085
|
const BrokerStatsSchema = object({
|
|
6023
6086
|
status: BrokerStatusSchema$1,
|
|
@@ -6239,9 +6302,76 @@ const RtpSourceSchema = object({
|
|
|
6239
6302
|
object({ released: boolean(), refcount: number().int().nonnegative() }),
|
|
6240
6303
|
{ kind: "mutation", auth: "admin" }
|
|
6241
6304
|
),
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6305
|
+
/**
|
|
6306
|
+
* ── Decoded audio-chunk plane (Phase 5 / D9) ──────────────────────
|
|
6307
|
+
*
|
|
6308
|
+
* The serialisable replacement for the live-object `IStreamBroker.
|
|
6309
|
+
* onDecodedAudioChunk` callback path. Unlike the video frame plane,
|
|
6310
|
+
* audio chunks are tiny (a ~500ms PCM window is a few KB) so they ship
|
|
6311
|
+
* their bytes INLINE over tRPC — no shared-memory ring. A consumer:
|
|
6312
|
+
*
|
|
6313
|
+
* 1. `subscribeAudioChunks({ brokerId, tag })` — the broker registers
|
|
6314
|
+
* a per-subscription bounded FIFO queue and returns a
|
|
6315
|
+
* `subscriptionId`.
|
|
6316
|
+
* 2. polls `pullAudioChunks({ subscriptionId, maxCount })` — drains
|
|
6317
|
+
* `DecodedAudioChunk[]` in arrival order (FIFO, no latest-wins
|
|
6318
|
+
* drop: an audio gap is audible / breaks an analysis window). The
|
|
6319
|
+
* queue is generously sized; it only drops its oldest chunk if a
|
|
6320
|
+
* truly stalled consumer lets it overflow.
|
|
6321
|
+
* 3. `unsubscribeAudioChunks({ subscriptionId })` on teardown.
|
|
6322
|
+
*/
|
|
6323
|
+
subscribeAudioChunks: method(
|
|
6324
|
+
SubscribeAudioChunksInputSchema,
|
|
6325
|
+
SubscribeAudioChunksResultSchema,
|
|
6326
|
+
{ kind: "mutation" }
|
|
6327
|
+
),
|
|
6328
|
+
pullAudioChunks: method(
|
|
6329
|
+
object({
|
|
6330
|
+
subscriptionId: string(),
|
|
6331
|
+
maxCount: number().int().positive().default(8)
|
|
6332
|
+
}),
|
|
6333
|
+
array(DecodedAudioChunkSchema).readonly()
|
|
6334
|
+
),
|
|
6335
|
+
unsubscribeAudioChunks: method(
|
|
6336
|
+
object({ subscriptionId: string() }),
|
|
6337
|
+
object({ released: boolean() }),
|
|
6338
|
+
{ kind: "mutation" }
|
|
6339
|
+
),
|
|
6340
|
+
/**
|
|
6341
|
+
* ── Shared-memory frame plane (Phase 5 / D9) ──────────────────────
|
|
6342
|
+
*
|
|
6343
|
+
* The handle-based replacement for the live-object `IStreamBroker.
|
|
6344
|
+
* onDecodedFrame` callback path. A consumer:
|
|
6345
|
+
*
|
|
6346
|
+
* 1. `subscribeFrames({ brokerId, format })` — the broker spins up
|
|
6347
|
+
* (or reuses) a `frameSink: 'shm'` decoder session producing that
|
|
6348
|
+
* `format` and returns a `subscriptionId`.
|
|
6349
|
+
* 2. polls `pullFrameHandles({ subscriptionId, maxCount })` — drains
|
|
6350
|
+
* zero-pixel `FrameHandle[]`; each handle is fed to a
|
|
6351
|
+
* `FrameRingReader` that opens the named shm segment and reads the
|
|
6352
|
+
* pixels back zero-copy.
|
|
6353
|
+
* 3. `unsubscribeFrames({ subscriptionId })` on teardown.
|
|
6354
|
+
*
|
|
6355
|
+
* The broker keeps one shm ring per `(brokerId, format)` actually
|
|
6356
|
+
* requested — no broker-side `sharp` conversion. fps throttling is
|
|
6357
|
+
* implicit (latest-wins ring reads drop frames for a slow consumer).
|
|
6358
|
+
*/
|
|
6359
|
+
subscribeFrames: method(
|
|
6360
|
+
SubscribeFramesInputSchema,
|
|
6361
|
+
SubscribeFramesResultSchema,
|
|
6362
|
+
{ kind: "mutation" }
|
|
6363
|
+
),
|
|
6364
|
+
pullFrameHandles: method(
|
|
6365
|
+
object({
|
|
6366
|
+
subscriptionId: string(),
|
|
6367
|
+
maxCount: number().int().positive().default(4)
|
|
6368
|
+
}),
|
|
6369
|
+
array(FrameHandleSchema).readonly()
|
|
6370
|
+
),
|
|
6371
|
+
unsubscribeFrames: method(
|
|
6372
|
+
object({ subscriptionId: string() }),
|
|
6373
|
+
object({ released: boolean() }),
|
|
6374
|
+
{ kind: "mutation" }
|
|
6245
6375
|
),
|
|
6246
6376
|
setPreBufferDuration: method(
|
|
6247
6377
|
object({ brokerId: string(), seconds: number().min(0).max(30) }),
|
|
@@ -6297,6 +6427,8 @@ const cameraStreamsCapability = {
|
|
|
6297
6427
|
name: "camera-streams",
|
|
6298
6428
|
scope: "device",
|
|
6299
6429
|
mode: "singleton",
|
|
6430
|
+
kind: "wrapper",
|
|
6431
|
+
defaultActive: true,
|
|
6300
6432
|
deviceTypes: [DeviceType.Camera],
|
|
6301
6433
|
methods: {
|
|
6302
6434
|
getCameraStreams: method(
|
|
@@ -7393,6 +7525,42 @@ const motionTriggerCapability = {
|
|
|
7393
7525
|
},
|
|
7394
7526
|
runtimeState: MotionTriggerRuntimeStateSchema
|
|
7395
7527
|
};
|
|
7528
|
+
const MotionZoneStatusSchema = object({
|
|
7529
|
+
enabled: boolean(),
|
|
7530
|
+
sensitivity: number(),
|
|
7531
|
+
/** Row-major active-cell grid. Length = gridWidth*gridHeight (see getOptions). */
|
|
7532
|
+
cells: array(boolean()),
|
|
7533
|
+
lastFetchedAt: number()
|
|
7534
|
+
});
|
|
7535
|
+
const MotionZoneOptionsSchema = object({
|
|
7536
|
+
gridWidth: number(),
|
|
7537
|
+
gridHeight: number(),
|
|
7538
|
+
sensitivity: object({ min: number(), max: number(), step: number() })
|
|
7539
|
+
});
|
|
7540
|
+
const MotionZonePatchSchema = object({
|
|
7541
|
+
enabled: boolean().optional(),
|
|
7542
|
+
sensitivity: number().optional(),
|
|
7543
|
+
cells: array(boolean()).optional()
|
|
7544
|
+
});
|
|
7545
|
+
const motionZonesCapability = {
|
|
7546
|
+
name: "motion-zones",
|
|
7547
|
+
scope: "device",
|
|
7548
|
+
mode: "singleton",
|
|
7549
|
+
deviceTypes: [DeviceType.Camera],
|
|
7550
|
+
deviceConfig: {
|
|
7551
|
+
ui: { kind: "widget", widgetId: "host/motion-zones-grid", tab: "motion", label: "Motion Zones" }
|
|
7552
|
+
},
|
|
7553
|
+
methods: {
|
|
7554
|
+
getOptions: method(object({ deviceId: number() }), MotionZoneOptionsSchema),
|
|
7555
|
+
setZone: method(
|
|
7556
|
+
object({ deviceId: number(), patch: MotionZonePatchSchema }),
|
|
7557
|
+
_void(),
|
|
7558
|
+
{ kind: "mutation", auth: "admin" }
|
|
7559
|
+
)
|
|
7560
|
+
},
|
|
7561
|
+
status: { schema: MotionZoneStatusSchema, kind: "poll" },
|
|
7562
|
+
runtimeState: MotionZoneStatusSchema
|
|
7563
|
+
};
|
|
7396
7564
|
const AutotrackTargetTypeSchema = string().describe("Vendor target string (people/vehicle/pet); empty = camera default");
|
|
7397
7565
|
const PtzAutotrackSettingsSchema = object({
|
|
7398
7566
|
targetType: AutotrackTargetTypeSchema,
|
|
@@ -7431,6 +7599,9 @@ const ptzAutotrackCapability = {
|
|
|
7431
7599
|
scope: "device",
|
|
7432
7600
|
mode: "singleton",
|
|
7433
7601
|
deviceTypes: [DeviceType.Camera],
|
|
7602
|
+
deviceConfig: {
|
|
7603
|
+
ui: { kind: "widget", widgetId: "host/ptz-autotrack", tab: "ptz", topTab: true, label: "Auto-Tracking", order: 5 }
|
|
7604
|
+
},
|
|
7434
7605
|
methods: {
|
|
7435
7606
|
/**
|
|
7436
7607
|
* Read the current on/off state + last-applied settings.
|
|
@@ -7497,6 +7668,111 @@ const ptzAutotrackCapability = {
|
|
|
7497
7668
|
*/
|
|
7498
7669
|
runtimeState: PtzAutotrackRuntimeStateSchema
|
|
7499
7670
|
};
|
|
7671
|
+
const StreamProfileSchema = _enum(["main", "sub", "ext"]);
|
|
7672
|
+
const StreamProfileConfigSchema = object({
|
|
7673
|
+
width: number(),
|
|
7674
|
+
height: number(),
|
|
7675
|
+
codec: _enum(["h264", "h265"]),
|
|
7676
|
+
framerate: number(),
|
|
7677
|
+
bitrate: number(),
|
|
7678
|
+
// kbps
|
|
7679
|
+
bitrateMode: _enum(["vbr", "cbr"]).optional(),
|
|
7680
|
+
encoderProfile: _enum(["high", "main", "baseline"]).optional(),
|
|
7681
|
+
gop: number().optional(),
|
|
7682
|
+
audio: boolean().optional()
|
|
7683
|
+
});
|
|
7684
|
+
const StreamParamsStatusSchema = object({
|
|
7685
|
+
/** Per-profile current config. A profile absent = the camera doesn't have it. */
|
|
7686
|
+
main: StreamProfileConfigSchema.optional(),
|
|
7687
|
+
sub: StreamProfileConfigSchema.optional(),
|
|
7688
|
+
ext: StreamProfileConfigSchema.optional(),
|
|
7689
|
+
lastFetchedAt: number()
|
|
7690
|
+
});
|
|
7691
|
+
const StreamProfileOptionsSchema = object({
|
|
7692
|
+
resolutions: array(object({ width: number(), height: number() })),
|
|
7693
|
+
codecs: array(_enum(["h264", "h265"])),
|
|
7694
|
+
framerates: array(number()),
|
|
7695
|
+
/** Allowed bitrate values (kbps). Empty if the camera takes a free range. */
|
|
7696
|
+
bitrates: array(number()),
|
|
7697
|
+
/** Optional [min,max] kbps when the camera accepts a continuous range. */
|
|
7698
|
+
bitrateRange: tuple([number(), number()]).optional(),
|
|
7699
|
+
supportsBitrateMode: boolean(),
|
|
7700
|
+
supportsEncoderProfile: boolean(),
|
|
7701
|
+
supportsGop: boolean(),
|
|
7702
|
+
/** Allowed GOP / keyframe-interval range, in seconds — drives the
|
|
7703
|
+
* I-frame-interval selector. Absent when the camera advertises GOP
|
|
7704
|
+
* support but no concrete range (callers then fall back to a free
|
|
7705
|
+
* numeric input). `{ min, max, step }` per the getOptions convention. */
|
|
7706
|
+
gop: object({ min: number(), max: number(), step: number() }).optional()
|
|
7707
|
+
});
|
|
7708
|
+
const StreamParamsOptionsSchema = object({
|
|
7709
|
+
main: StreamProfileOptionsSchema.optional(),
|
|
7710
|
+
sub: StreamProfileOptionsSchema.optional(),
|
|
7711
|
+
ext: StreamProfileOptionsSchema.optional()
|
|
7712
|
+
});
|
|
7713
|
+
const StreamProfilePatchSchema = object({
|
|
7714
|
+
width: number().optional(),
|
|
7715
|
+
height: number().optional(),
|
|
7716
|
+
codec: _enum(["h264", "h265"]).optional(),
|
|
7717
|
+
framerate: number().optional(),
|
|
7718
|
+
bitrate: number().optional(),
|
|
7719
|
+
bitrateMode: _enum(["vbr", "cbr"]).optional(),
|
|
7720
|
+
encoderProfile: _enum(["high", "main", "baseline"]).optional(),
|
|
7721
|
+
gop: number().optional(),
|
|
7722
|
+
audio: boolean().optional()
|
|
7723
|
+
});
|
|
7724
|
+
const streamParamsCapability = {
|
|
7725
|
+
name: "stream-params",
|
|
7726
|
+
scope: "device",
|
|
7727
|
+
mode: "singleton",
|
|
7728
|
+
deviceTypes: [DeviceType.Camera],
|
|
7729
|
+
deviceConfig: {
|
|
7730
|
+
ui: { kind: "derived-form", builderId: "stream-params", tab: "streaming" }
|
|
7731
|
+
},
|
|
7732
|
+
methods: {
|
|
7733
|
+
getOptions: method(
|
|
7734
|
+
object({ deviceId: number() }),
|
|
7735
|
+
StreamParamsOptionsSchema
|
|
7736
|
+
),
|
|
7737
|
+
setProfile: method(
|
|
7738
|
+
object({
|
|
7739
|
+
deviceId: number(),
|
|
7740
|
+
profile: StreamProfileSchema,
|
|
7741
|
+
patch: StreamProfilePatchSchema
|
|
7742
|
+
}),
|
|
7743
|
+
_void(),
|
|
7744
|
+
{ kind: "mutation", auth: "admin" }
|
|
7745
|
+
),
|
|
7746
|
+
/**
|
|
7747
|
+
* Build the `ConfigUISchema` (admin-ui `ConfigFormBuilder` input
|
|
7748
|
+
* shape) for this camera's stream-encoder settings — one section per
|
|
7749
|
+
* profile (main / sub / ext) with the resolution / codec / framerate
|
|
7750
|
+
* / bitrate / bitrate-mode / encoder-profile / GOP controls the
|
|
7751
|
+
* firmware actually exposes.
|
|
7752
|
+
*
|
|
7753
|
+
* Driven by `getOptions` (camera-probed availability) + `getStatus`
|
|
7754
|
+
* (current per-profile config); each field's `default` is seeded
|
|
7755
|
+
* from the live config so the form renders the camera state in one
|
|
7756
|
+
* pass. Returns `null` when the camera exposes no configurable
|
|
7757
|
+
* stream property — the renderer then shows the unsupported message.
|
|
7758
|
+
*
|
|
7759
|
+
* Output is `z.unknown().nullable()` — the same convention every
|
|
7760
|
+
* other `ConfigUISchema`-returning cap method uses (`device-ops`,
|
|
7761
|
+
* `device-manager`); `ConfigUISchema` is a TS-only type with no
|
|
7762
|
+
* companion Zod schema, and a concrete object would collapse
|
|
7763
|
+
* unrelated AppRouter branches to `unknown` during codegen.
|
|
7764
|
+
*/
|
|
7765
|
+
getConfigSchema: method(
|
|
7766
|
+
object({ deviceId: number() }),
|
|
7767
|
+
unknown().nullable()
|
|
7768
|
+
)
|
|
7769
|
+
},
|
|
7770
|
+
status: {
|
|
7771
|
+
schema: StreamParamsStatusSchema,
|
|
7772
|
+
kind: "poll"
|
|
7773
|
+
},
|
|
7774
|
+
runtimeState: StreamParamsStatusSchema
|
|
7775
|
+
};
|
|
7500
7776
|
const SwitchStatusSchema = object({
|
|
7501
7777
|
on: boolean(),
|
|
7502
7778
|
/** Ms epoch of the last state change. Useful for UI "X minutes ago". */
|
|
@@ -8473,6 +8749,83 @@ const VersionOutputSchema = object({ version: string() });
|
|
|
8473
8749
|
getVersion: method(_void(), VersionOutputSchema)
|
|
8474
8750
|
}
|
|
8475
8751
|
});
|
|
8752
|
+
const MethodAccessSchema = _enum(["view", "create", "delete"]);
|
|
8753
|
+
const AllowedProviderSchema = union([literal("*"), array(string())]);
|
|
8754
|
+
const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
|
|
8755
|
+
const CapScopeSchema = _enum(["device", "system"]);
|
|
8756
|
+
const TokenScopeSchema = discriminatedUnion("type", [
|
|
8757
|
+
object({
|
|
8758
|
+
type: literal("category"),
|
|
8759
|
+
target: CapScopeSchema,
|
|
8760
|
+
access: array(MethodAccessSchema).min(1)
|
|
8761
|
+
}),
|
|
8762
|
+
object({
|
|
8763
|
+
type: literal("capability"),
|
|
8764
|
+
target: string(),
|
|
8765
|
+
access: array(MethodAccessSchema).min(1)
|
|
8766
|
+
}),
|
|
8767
|
+
object({
|
|
8768
|
+
type: literal("addon"),
|
|
8769
|
+
target: string(),
|
|
8770
|
+
access: array(MethodAccessSchema).min(1)
|
|
8771
|
+
}),
|
|
8772
|
+
object({
|
|
8773
|
+
type: literal("device"),
|
|
8774
|
+
/**
|
|
8775
|
+
* One or more deviceIds (serialised as strings for wire-format
|
|
8776
|
+
* consistency with the rest of the union). Matcher accepts if
|
|
8777
|
+
* `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
|
|
8778
|
+
* of one scope-per-device when granting access to a set of cameras.
|
|
8779
|
+
*/
|
|
8780
|
+
targets: array(string()).min(1),
|
|
8781
|
+
access: array(MethodAccessSchema).min(1)
|
|
8782
|
+
})
|
|
8783
|
+
]);
|
|
8784
|
+
object({
|
|
8785
|
+
id: string(),
|
|
8786
|
+
username: string(),
|
|
8787
|
+
passwordHash: string(),
|
|
8788
|
+
/**
|
|
8789
|
+
* Admin bypass. When true, the middleware skips the scope-access
|
|
8790
|
+
* check entirely. There is no other axis of privilege; the legacy
|
|
8791
|
+
* role enum collapsed onto this boolean in v2.
|
|
8792
|
+
*/
|
|
8793
|
+
isAdmin: boolean().default(false),
|
|
8794
|
+
allowedProviders: AllowedProviderSchema,
|
|
8795
|
+
allowedDevices: AllowedDevicesSchema,
|
|
8796
|
+
/**
|
|
8797
|
+
* Scopes granted to this user. Admins bypass; their `scopes` is
|
|
8798
|
+
* ignored. Non-admins without scopes are locked out of every
|
|
8799
|
+
* protected call.
|
|
8800
|
+
*/
|
|
8801
|
+
scopes: array(TokenScopeSchema).default([]),
|
|
8802
|
+
createdAt: number(),
|
|
8803
|
+
updatedAt: number()
|
|
8804
|
+
});
|
|
8805
|
+
object({
|
|
8806
|
+
id: string(),
|
|
8807
|
+
label: string(),
|
|
8808
|
+
isAdmin: boolean().default(false),
|
|
8809
|
+
allowedProviders: AllowedProviderSchema,
|
|
8810
|
+
allowedDevices: AllowedDevicesSchema,
|
|
8811
|
+
tokenHash: string(),
|
|
8812
|
+
tokenPrefix: string(),
|
|
8813
|
+
createdAt: number(),
|
|
8814
|
+
lastUsedAt: number().optional()
|
|
8815
|
+
});
|
|
8816
|
+
object({
|
|
8817
|
+
id: string(),
|
|
8818
|
+
userId: string(),
|
|
8819
|
+
name: string(),
|
|
8820
|
+
tokenHash: string(),
|
|
8821
|
+
tokenPrefix: string(),
|
|
8822
|
+
scopes: array(TokenScopeSchema),
|
|
8823
|
+
// SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
|
|
8824
|
+
// schema accepts both `null` (read from disk) and `undefined` (in-memory).
|
|
8825
|
+
expiresAt: number().nullish(),
|
|
8826
|
+
lastUsedAt: number().nullish(),
|
|
8827
|
+
createdAt: number()
|
|
8828
|
+
});
|
|
8476
8829
|
const SsoBridgeClaimsSchema = object({
|
|
8477
8830
|
userId: string(),
|
|
8478
8831
|
username: string(),
|
|
@@ -8488,7 +8841,18 @@ const SsoBridgeClaimsSchema = object({
|
|
|
8488
8841
|
* JWT WITHOUT verifying the signature — the hub re-verifies on every
|
|
8489
8842
|
* inbound call so trust still rests with the signing hub.
|
|
8490
8843
|
*/
|
|
8491
|
-
hubUrl: string().optional()
|
|
8844
|
+
hubUrl: string().optional(),
|
|
8845
|
+
/** Permission scopes baked into the token. Set by the OAuth
|
|
8846
|
+
* account-linking grant; absent on ordinary SSO-login tokens. */
|
|
8847
|
+
scopes: array(TokenScopeSchema).optional(),
|
|
8848
|
+
/** OAuth authorization-code binding — set only on `oauth-code` tokens. */
|
|
8849
|
+
redirectUri: string().optional(),
|
|
8850
|
+
integrationId: string().optional(),
|
|
8851
|
+
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
8852
|
+
jti: string().optional(),
|
|
8853
|
+
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
8854
|
+
* tokens so the verify path can check the session is not revoked. */
|
|
8855
|
+
sessionId: string().optional()
|
|
8492
8856
|
});
|
|
8493
8857
|
({
|
|
8494
8858
|
methods: {
|
|
@@ -8505,6 +8869,23 @@ const SsoBridgeClaimsSchema = object({
|
|
|
8505
8869
|
)
|
|
8506
8870
|
}
|
|
8507
8871
|
});
|
|
8872
|
+
const OauthIntegrationDescriptorSchema = object({
|
|
8873
|
+
/** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
|
|
8874
|
+
integrationId: string(),
|
|
8875
|
+
/** Human label rendered on the consent page. */
|
|
8876
|
+
displayName: string(),
|
|
8877
|
+
/** Scopes baked into every token issued for this integration. */
|
|
8878
|
+
requestedScopes: array(TokenScopeSchema),
|
|
8879
|
+
/** Allowed redirect_uri prefixes. /api/oauth2/authorize rejects any
|
|
8880
|
+
* redirect_uri that does not start with one of these. Required —
|
|
8881
|
+
* an empty list means the integration can never complete linking. */
|
|
8882
|
+
allowedRedirectPrefixes: array(string()).min(1)
|
|
8883
|
+
});
|
|
8884
|
+
({
|
|
8885
|
+
methods: {
|
|
8886
|
+
getDescriptor: method(_void(), OauthIntegrationDescriptorSchema)
|
|
8887
|
+
}
|
|
8888
|
+
});
|
|
8508
8889
|
const PasskeySummarySchema = object({
|
|
8509
8890
|
credentialId: string(),
|
|
8510
8891
|
label: string(),
|
|
@@ -8785,21 +9166,30 @@ const AddonPageDeclarationSchema = object({
|
|
|
8785
9166
|
});
|
|
8786
9167
|
const WidgetHostEnum = _enum(["device-tab", "dashboard", "integration-detail"]);
|
|
8787
9168
|
const WidgetSizeEnum = _enum(["xs", "sm", "md", "lg", "xl"]);
|
|
9169
|
+
const WidgetRemoteSchema = object({
|
|
9170
|
+
remoteName: string(),
|
|
9171
|
+
exposedModule: string(),
|
|
9172
|
+
componentKey: string().optional()
|
|
9173
|
+
});
|
|
8788
9174
|
const WidgetMetadataSchema = object({
|
|
8789
|
-
|
|
8790
|
-
|
|
9175
|
+
// ── UiContribution core (kind:'remote') ──────────────────────────
|
|
9176
|
+
/** Primary host tab — `'dashboard'`, `'device-tab'`, or a device-detail tab id. */
|
|
9177
|
+
tab: string(),
|
|
9178
|
+
/** Optional sub-tab within `tab`. */
|
|
9179
|
+
subTab: string().optional(),
|
|
8791
9180
|
/** Operator-facing label. */
|
|
8792
9181
|
label: string(),
|
|
9182
|
+
/** Ordering within `(tab, subTab)`, ascending. */
|
|
9183
|
+
order: number().optional(),
|
|
9184
|
+
/** Always `'remote'` — a widget is a Module Federation remote. */
|
|
9185
|
+
kind: literal("remote"),
|
|
9186
|
+
/** MF remote descriptor. */
|
|
9187
|
+
remote: WidgetRemoteSchema,
|
|
9188
|
+
// ── Widget-only metadata ─────────────────────────────────────────
|
|
9189
|
+
/** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
|
|
9190
|
+
stableId: string(),
|
|
8793
9191
|
description: string().optional(),
|
|
8794
9192
|
icon: string().optional(),
|
|
8795
|
-
/**
|
|
8796
|
-
* Module Federation remote name — must match the `name` field on the
|
|
8797
|
-
* widget addon's `federation()` plugin config. Used by the host's
|
|
8798
|
-
* `<WidgetRegistryProvider>` to call `loadRemote('<remoteName>/widgets')`.
|
|
8799
|
-
* Conventionally `addon_<addonid>_widgets` (snake_case; MF names
|
|
8800
|
-
* cannot contain hyphens).
|
|
8801
|
-
*/
|
|
8802
|
-
remoteName: string(),
|
|
8803
9193
|
/**
|
|
8804
9194
|
* Bundle filename inside the addon's `dist/` dir served at
|
|
8805
9195
|
* `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
|
|
@@ -8808,9 +9198,9 @@ const WidgetMetadataSchema = object({
|
|
|
8808
9198
|
* cache-buster URL without a separate filesystem stat.
|
|
8809
9199
|
*/
|
|
8810
9200
|
bundle: string(),
|
|
8811
|
-
/**
|
|
9201
|
+
/** Every host the widget supports. The picker filters on this set. */
|
|
8812
9202
|
hosts: array(WidgetHostEnum).readonly(),
|
|
8813
|
-
/** Required props the host must supply. Validated at
|
|
9203
|
+
/** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
|
|
8814
9204
|
requires: object({
|
|
8815
9205
|
deviceContext: boolean().default(false),
|
|
8816
9206
|
integrationContext: boolean().default(false)
|
|
@@ -8881,6 +9271,16 @@ const InvokeReplyEnvelopeSchema = object({
|
|
|
8881
9271
|
invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
|
|
8882
9272
|
}
|
|
8883
9273
|
});
|
|
9274
|
+
const ShmRingStatsSchema = object({
|
|
9275
|
+
sessionId: string(),
|
|
9276
|
+
slotCount: number().int(),
|
|
9277
|
+
slotByteLength: number().int(),
|
|
9278
|
+
segmentBytes: number().int(),
|
|
9279
|
+
budgetMb: number().int(),
|
|
9280
|
+
framesWritten: number().int(),
|
|
9281
|
+
getFrameHits: number().int(),
|
|
9282
|
+
getFrameMisses: number().int()
|
|
9283
|
+
});
|
|
8884
9284
|
({
|
|
8885
9285
|
methods: {
|
|
8886
9286
|
// ── Discovery ─────────────────────────────────────────────────
|
|
@@ -8908,10 +9308,27 @@ const InvokeReplyEnvelopeSchema = object({
|
|
|
8908
9308
|
url: string()
|
|
8909
9309
|
}), _void()),
|
|
8910
9310
|
// ── Output — polling-based frame retrieval ────────────────────
|
|
9311
|
+
// `pullFrames` drains the pixel `DecodedFrame[]` of a `frameSink:
|
|
9312
|
+
// 'callback'` session. `pullHandles` (Phase 5 / D9) drains the
|
|
9313
|
+
// zero-pixel `FrameHandle[]` of a `frameSink: 'shm'` session — the
|
|
9314
|
+
// broker hands each handle to a `FrameRingReader` that opens the
|
|
9315
|
+
// named segment and reads the pixels back zero-copy. A session is
|
|
9316
|
+
// one mode or the other; the unmatched method returns an empty
|
|
9317
|
+
// array.
|
|
8911
9318
|
pullFrames: method(object({
|
|
8912
9319
|
sessionId: string(),
|
|
8913
9320
|
maxCount: number().default(1)
|
|
8914
9321
|
}), array(DecodedFrameSchema)),
|
|
9322
|
+
pullHandles: method(object({
|
|
9323
|
+
sessionId: string(),
|
|
9324
|
+
maxCount: number().default(1)
|
|
9325
|
+
}), array(FrameHandleSchema)),
|
|
9326
|
+
// ── Frame fetch (Phase 5 / D9 downstream access) ──────────────
|
|
9327
|
+
// Read the pixels a FrameHandle refers to from this node's shm ring.
|
|
9328
|
+
// Returns null when the slot was already recycled (latest-wins).
|
|
9329
|
+
getFrame: method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()),
|
|
9330
|
+
// shm ring usage stats for a session.
|
|
9331
|
+
getShmStats: method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()),
|
|
8915
9332
|
// ── Control ───────────────────────────────────────────────────
|
|
8916
9333
|
updateConfig: method(object({
|
|
8917
9334
|
sessionId: string(),
|
|
@@ -10079,8 +10496,8 @@ const DevicePersistConfigPayloadSchema = object({
|
|
|
10079
10496
|
/**
|
|
10080
10497
|
* Return the addon ids that declared a wrapper provider for `capName`.
|
|
10081
10498
|
* Backs the device-bindings UI's wrapper-picker dropdown. Entries are
|
|
10082
|
-
* sourced from `CapabilityRegistry.wrapperProviders`, populated
|
|
10083
|
-
* `
|
|
10499
|
+
* sourced from `CapabilityRegistry.wrapperProviders`, populated when
|
|
10500
|
+
* the cap definition declares `kind: 'wrapper'`.
|
|
10084
10501
|
*/
|
|
10085
10502
|
listWrappersForCap: method(
|
|
10086
10503
|
object({ capName: string() }),
|
|
@@ -10461,6 +10878,8 @@ const snapshotCapability = {
|
|
|
10461
10878
|
name: "snapshot",
|
|
10462
10879
|
scope: "device",
|
|
10463
10880
|
mode: "singleton",
|
|
10881
|
+
kind: "wrapper",
|
|
10882
|
+
defaultActive: true,
|
|
10464
10883
|
deviceTypes: [DeviceType.Camera],
|
|
10465
10884
|
// Owns per-device snapshot settings (preferred stream, debug logging).
|
|
10466
10885
|
// The three DeviceSettingsContribution methods are auto-added to the
|
|
@@ -11182,6 +11601,18 @@ const PtzMoveCommandSchema = object({
|
|
|
11182
11601
|
zoom: number().optional(),
|
|
11183
11602
|
speed: number().optional()
|
|
11184
11603
|
});
|
|
11604
|
+
PtzPositionSchema.extend({ autofocus: boolean() });
|
|
11605
|
+
const PtzOptionsSchema = object({
|
|
11606
|
+
hasPan: boolean(),
|
|
11607
|
+
hasTilt: boolean(),
|
|
11608
|
+
hasZoom: boolean(),
|
|
11609
|
+
supportsPresets: boolean(),
|
|
11610
|
+
/** Max number of named presets the camera supports, when known. */
|
|
11611
|
+
maxPresets: number().optional(),
|
|
11612
|
+
/** Whether the camera exposes a controllable autofocus toggle
|
|
11613
|
+
* (boolean `hasX` per the getOptions availability convention). */
|
|
11614
|
+
hasAutofocus: boolean()
|
|
11615
|
+
});
|
|
11185
11616
|
({
|
|
11186
11617
|
deviceTypes: [DeviceType.Camera],
|
|
11187
11618
|
methods: {
|
|
@@ -11209,6 +11640,20 @@ const PtzMoveCommandSchema = object({
|
|
|
11209
11640
|
_void(),
|
|
11210
11641
|
{ kind: "mutation" }
|
|
11211
11642
|
),
|
|
11643
|
+
savePreset: method(
|
|
11644
|
+
object({ deviceId: number(), presetId: string(), name: string() }),
|
|
11645
|
+
_void(),
|
|
11646
|
+
{ kind: "mutation", auth: "admin" }
|
|
11647
|
+
),
|
|
11648
|
+
deletePreset: method(
|
|
11649
|
+
object({ deviceId: number(), presetId: string() }),
|
|
11650
|
+
_void(),
|
|
11651
|
+
{ kind: "mutation", auth: "admin" }
|
|
11652
|
+
),
|
|
11653
|
+
getOptions: method(
|
|
11654
|
+
object({ deviceId: number() }),
|
|
11655
|
+
PtzOptionsSchema
|
|
11656
|
+
),
|
|
11212
11657
|
goHome: method(
|
|
11213
11658
|
object({ deviceId: number() }),
|
|
11214
11659
|
_void(),
|
|
@@ -11223,6 +11668,13 @@ const PtzMoveCommandSchema = object({
|
|
|
11223
11668
|
getPosition: method(
|
|
11224
11669
|
object({ deviceId: number() }),
|
|
11225
11670
|
PtzPositionSchema
|
|
11671
|
+
),
|
|
11672
|
+
/** Toggle the camera's autofocus. Only meaningful when
|
|
11673
|
+
* `getOptions().hasAutofocus` is true. */
|
|
11674
|
+
setAutofocus: method(
|
|
11675
|
+
object({ deviceId: number(), enabled: boolean() }),
|
|
11676
|
+
_void(),
|
|
11677
|
+
{ kind: "mutation" }
|
|
11226
11678
|
)
|
|
11227
11679
|
}
|
|
11228
11680
|
});
|
|
@@ -12158,83 +12610,6 @@ const MeshStatusSchema = object({
|
|
|
12158
12610
|
// tabs driven by this cap.
|
|
12159
12611
|
}
|
|
12160
12612
|
});
|
|
12161
|
-
const MethodAccessSchema = _enum(["view", "create", "delete"]);
|
|
12162
|
-
const AllowedProviderSchema = union([literal("*"), array(string())]);
|
|
12163
|
-
const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
|
|
12164
|
-
const CapScopeSchema = _enum(["device", "system"]);
|
|
12165
|
-
const TokenScopeSchema = discriminatedUnion("type", [
|
|
12166
|
-
object({
|
|
12167
|
-
type: literal("category"),
|
|
12168
|
-
target: CapScopeSchema,
|
|
12169
|
-
access: array(MethodAccessSchema).min(1)
|
|
12170
|
-
}),
|
|
12171
|
-
object({
|
|
12172
|
-
type: literal("capability"),
|
|
12173
|
-
target: string(),
|
|
12174
|
-
access: array(MethodAccessSchema).min(1)
|
|
12175
|
-
}),
|
|
12176
|
-
object({
|
|
12177
|
-
type: literal("addon"),
|
|
12178
|
-
target: string(),
|
|
12179
|
-
access: array(MethodAccessSchema).min(1)
|
|
12180
|
-
}),
|
|
12181
|
-
object({
|
|
12182
|
-
type: literal("device"),
|
|
12183
|
-
/**
|
|
12184
|
-
* One or more deviceIds (serialised as strings for wire-format
|
|
12185
|
-
* consistency with the rest of the union). Matcher accepts if
|
|
12186
|
-
* `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
|
|
12187
|
-
* of one scope-per-device when granting access to a set of cameras.
|
|
12188
|
-
*/
|
|
12189
|
-
targets: array(string()).min(1),
|
|
12190
|
-
access: array(MethodAccessSchema).min(1)
|
|
12191
|
-
})
|
|
12192
|
-
]);
|
|
12193
|
-
object({
|
|
12194
|
-
id: string(),
|
|
12195
|
-
username: string(),
|
|
12196
|
-
passwordHash: string(),
|
|
12197
|
-
/**
|
|
12198
|
-
* Admin bypass. When true, the middleware skips the scope-access
|
|
12199
|
-
* check entirely. There is no other axis of privilege; the legacy
|
|
12200
|
-
* role enum collapsed onto this boolean in v2.
|
|
12201
|
-
*/
|
|
12202
|
-
isAdmin: boolean().default(false),
|
|
12203
|
-
allowedProviders: AllowedProviderSchema,
|
|
12204
|
-
allowedDevices: AllowedDevicesSchema,
|
|
12205
|
-
/**
|
|
12206
|
-
* Scopes granted to this user. Admins bypass; their `scopes` is
|
|
12207
|
-
* ignored. Non-admins without scopes are locked out of every
|
|
12208
|
-
* protected call.
|
|
12209
|
-
*/
|
|
12210
|
-
scopes: array(TokenScopeSchema).default([]),
|
|
12211
|
-
createdAt: number(),
|
|
12212
|
-
updatedAt: number()
|
|
12213
|
-
});
|
|
12214
|
-
object({
|
|
12215
|
-
id: string(),
|
|
12216
|
-
label: string(),
|
|
12217
|
-
isAdmin: boolean().default(false),
|
|
12218
|
-
allowedProviders: AllowedProviderSchema,
|
|
12219
|
-
allowedDevices: AllowedDevicesSchema,
|
|
12220
|
-
tokenHash: string(),
|
|
12221
|
-
tokenPrefix: string(),
|
|
12222
|
-
createdAt: number(),
|
|
12223
|
-
lastUsedAt: number().optional()
|
|
12224
|
-
});
|
|
12225
|
-
object({
|
|
12226
|
-
id: string(),
|
|
12227
|
-
userId: string(),
|
|
12228
|
-
name: string(),
|
|
12229
|
-
tokenHash: string(),
|
|
12230
|
-
tokenPrefix: string(),
|
|
12231
|
-
scopes: array(TokenScopeSchema),
|
|
12232
|
-
// SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
|
|
12233
|
-
// schema accepts both `null` (read from disk) and `undefined` (in-memory).
|
|
12234
|
-
expiresAt: number().nullish(),
|
|
12235
|
-
lastUsedAt: number().nullish(),
|
|
12236
|
-
createdAt: number()
|
|
12237
|
-
});
|
|
12238
12613
|
const UserSummarySchema = object({
|
|
12239
12614
|
id: string(),
|
|
12240
12615
|
username: string(),
|
|
@@ -12307,6 +12682,16 @@ const CreateScopedTokenResultSchema = object({
|
|
|
12307
12682
|
token: string(),
|
|
12308
12683
|
record: ScopedTokenSummarySchema
|
|
12309
12684
|
});
|
|
12685
|
+
const OauthSessionSummarySchema = object({
|
|
12686
|
+
id: string(),
|
|
12687
|
+
userId: string(),
|
|
12688
|
+
username: string(),
|
|
12689
|
+
integrationId: string(),
|
|
12690
|
+
scopes: array(TokenScopeSchema),
|
|
12691
|
+
createdAt: number(),
|
|
12692
|
+
lastUsedAt: number(),
|
|
12693
|
+
revokedAt: number().nullable()
|
|
12694
|
+
});
|
|
12310
12695
|
const TotpSetupResultSchema = object({
|
|
12311
12696
|
secret: string(),
|
|
12312
12697
|
otpauthUrl: string()
|
|
@@ -12382,6 +12767,66 @@ const TotpStatusSchema = object({
|
|
|
12382
12767
|
object({ userId: string(), code: string() }),
|
|
12383
12768
|
object({ valid: boolean() }),
|
|
12384
12769
|
{ kind: "mutation", access: "view" }
|
|
12770
|
+
),
|
|
12771
|
+
// ── OAuth account-linking grant ────────────────────────────────
|
|
12772
|
+
//
|
|
12773
|
+
// Core's /oauth2/* endpoints delegate here. Tokens are sso-bridge
|
|
12774
|
+
// JWTs (kinds oauth-code / oauth-access / oauth-refresh) and ALWAYS
|
|
12775
|
+
// carry isAdmin:false — the operator login proves hub control; the
|
|
12776
|
+
// issued token is minimal (device scope only).
|
|
12777
|
+
oauthIssueCode: method(
|
|
12778
|
+
object({
|
|
12779
|
+
integrationId: string(),
|
|
12780
|
+
userId: string(),
|
|
12781
|
+
username: string(),
|
|
12782
|
+
scopes: array(TokenScopeSchema),
|
|
12783
|
+
redirectUri: string(),
|
|
12784
|
+
hubUrl: string()
|
|
12785
|
+
}),
|
|
12786
|
+
object({ code: string() }),
|
|
12787
|
+
{ kind: "mutation", access: "create" }
|
|
12788
|
+
),
|
|
12789
|
+
oauthExchangeCode: method(
|
|
12790
|
+
object({ code: string(), redirectUri: string() }),
|
|
12791
|
+
object({
|
|
12792
|
+
accessToken: string(),
|
|
12793
|
+
refreshToken: string(),
|
|
12794
|
+
expiresIn: number()
|
|
12795
|
+
}).nullable(),
|
|
12796
|
+
{ kind: "mutation", access: "view" }
|
|
12797
|
+
),
|
|
12798
|
+
oauthRefresh: method(
|
|
12799
|
+
object({ refreshToken: string() }),
|
|
12800
|
+
object({
|
|
12801
|
+
accessToken: string(),
|
|
12802
|
+
refreshToken: string(),
|
|
12803
|
+
expiresIn: number()
|
|
12804
|
+
}).nullable(),
|
|
12805
|
+
{ kind: "mutation", access: "view" }
|
|
12806
|
+
),
|
|
12807
|
+
oauthVerifyAccessToken: method(
|
|
12808
|
+
object({ token: string() }),
|
|
12809
|
+
object({
|
|
12810
|
+
userId: string(),
|
|
12811
|
+
username: string(),
|
|
12812
|
+
scopes: array(TokenScopeSchema)
|
|
12813
|
+
}).nullable(),
|
|
12814
|
+
{ access: "view" }
|
|
12815
|
+
),
|
|
12816
|
+
// ── OAuth linked-session management (Phase D) ──────────────────
|
|
12817
|
+
//
|
|
12818
|
+
// The admin UI lists active account-linking sessions and revokes
|
|
12819
|
+
// them; revocation makes the linked integration's tokens fail
|
|
12820
|
+
// verification immediately.
|
|
12821
|
+
listOauthSessions: method(
|
|
12822
|
+
_void(),
|
|
12823
|
+
array(OauthSessionSummarySchema),
|
|
12824
|
+
{ auth: "admin" }
|
|
12825
|
+
),
|
|
12826
|
+
revokeOauthSession: method(
|
|
12827
|
+
object({ id: string() }),
|
|
12828
|
+
object({ success: boolean() }),
|
|
12829
|
+
{ kind: "mutation", auth: "admin", access: "delete" }
|
|
12385
12830
|
)
|
|
12386
12831
|
}
|
|
12387
12832
|
});
|
|
@@ -12598,6 +13043,19 @@ const RenameNodeResultSchema = object({
|
|
|
12598
13043
|
record(string(), ClusterAddonStatusEntrySchema),
|
|
12599
13044
|
{ auth: "admin" }
|
|
12600
13045
|
),
|
|
13046
|
+
getCapUsageGraph: method(
|
|
13047
|
+
object({
|
|
13048
|
+
windowSeconds: number().int().positive().max(300).default(60)
|
|
13049
|
+
}),
|
|
13050
|
+
array(object({
|
|
13051
|
+
callerAddonId: string(),
|
|
13052
|
+
providerAddonId: string(),
|
|
13053
|
+
capName: string(),
|
|
13054
|
+
callsPerMin: number(),
|
|
13055
|
+
lastCallAtMs: number()
|
|
13056
|
+
})).readonly(),
|
|
13057
|
+
{ auth: "admin" }
|
|
13058
|
+
),
|
|
12601
13059
|
/**
|
|
12602
13060
|
* Direct per-node addon listing — calls `$agent.status` on the target
|
|
12603
13061
|
* node (or returns the hub registry for `nodeId === 'hub'`) and surfaces
|
|
@@ -13927,7 +14385,9 @@ const DEVICE_LOCAL_STATE_CAPS = {
|
|
|
13927
14385
|
featureProbe: featureProbeCapability,
|
|
13928
14386
|
motion: motionCapability,
|
|
13929
14387
|
motionTrigger: motionTriggerCapability,
|
|
14388
|
+
motionZones: motionZonesCapability,
|
|
13930
14389
|
ptzAutotrack: ptzAutotrackCapability,
|
|
14390
|
+
streamParams: streamParamsCapability,
|
|
13931
14391
|
switch: switchCapability,
|
|
13932
14392
|
zoneAnalytics: zoneAnalyticsCapability,
|
|
13933
14393
|
zoneRules: zoneRulesCapability,
|