@camstack/addon-provider-rtsp 0.1.26 → 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 +584 -326
- package/dist/addon.js.map +1 -1
- package/dist/addon.mjs +584 -326
- package/dist/addon.mjs.map +1 -1
- package/package.json +3 -3
package/dist/addon.js
CHANGED
|
@@ -5083,6 +5083,7 @@ const WELL_KNOWN_TABS = [
|
|
|
5083
5083
|
{ id: "osd", label: "OSD", icon: "type", order: 18 },
|
|
5084
5084
|
{ id: "stream-broker", label: "Stream Broker", icon: "radio", order: 20 },
|
|
5085
5085
|
{ id: "streaming", label: "Streaming", icon: "video", order: 35 },
|
|
5086
|
+
{ id: "ptz", label: "PTZ", icon: "move", order: 40 },
|
|
5086
5087
|
{ id: "pipeline", label: "Detection Pipeline", icon: "cpu", order: 39 },
|
|
5087
5088
|
{ id: "zones", label: "Detection", icon: "shapes", order: 38 },
|
|
5088
5089
|
{ id: "live-stats", label: "Live Stats", icon: "activity", order: 39 },
|
|
@@ -5150,6 +5151,9 @@ function hydrateField(field, values) {
|
|
|
5150
5151
|
return { ...field, value: items };
|
|
5151
5152
|
}
|
|
5152
5153
|
const rawValue = storedValue !== void 0 ? storedValue : defaultValue !== void 0 ? defaultValue : null;
|
|
5154
|
+
if (field.type === "password") {
|
|
5155
|
+
return { ...field, value: "" };
|
|
5156
|
+
}
|
|
5153
5157
|
const value = field.type === "textarea" && field.isJson && rawValue !== null && typeof rawValue === "object" ? JSON.stringify(rawValue, null, 2) : rawValue;
|
|
5154
5158
|
const hydrated = { ...field, value };
|
|
5155
5159
|
return hydrated;
|
|
@@ -5236,7 +5240,19 @@ const DecoderSessionConfigSchema = object({
|
|
|
5236
5240
|
* on every line so `grep tag=broker:5/high` filters one camera
|
|
5237
5241
|
* profile cleanly.
|
|
5238
5242
|
*/
|
|
5239
|
-
tag: string().optional()
|
|
5243
|
+
tag: string().optional(),
|
|
5244
|
+
/**
|
|
5245
|
+
* Where the session delivers decoded frames (Phase 5 / D9):
|
|
5246
|
+
*
|
|
5247
|
+
* - `'callback'` (default) — the legacy pixel path: decoded frames are
|
|
5248
|
+
* buffered as `DecodedFrame`s and drained via `pullFrames`.
|
|
5249
|
+
* - `'shm'` — the shared-memory frame plane: decoded frames are written
|
|
5250
|
+
* into an OS shared-memory ring and drained as zero-pixel
|
|
5251
|
+
* `FrameHandle`s via `pullHandles`. A session is one mode or the
|
|
5252
|
+
* other — `pullFrames` returns nothing for an `'shm'` session and
|
|
5253
|
+
* `pullHandles` returns nothing for a `'callback'` session.
|
|
5254
|
+
*/
|
|
5255
|
+
frameSink: _enum(["callback", "shm"]).default("callback")
|
|
5240
5256
|
});
|
|
5241
5257
|
const YAMNET_TO_MACRO = {
|
|
5242
5258
|
mapping: {
|
|
@@ -6020,6 +6036,53 @@ const DecodedFrameSchema = object({
|
|
|
6020
6036
|
format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
|
|
6021
6037
|
timestamp: number()
|
|
6022
6038
|
});
|
|
6039
|
+
const FrameHandleSchema = object({
|
|
6040
|
+
shmId: string(),
|
|
6041
|
+
slot: number().int().nonnegative(),
|
|
6042
|
+
seq: number().int().nonnegative(),
|
|
6043
|
+
width: number().int().positive(),
|
|
6044
|
+
height: number().int().positive(),
|
|
6045
|
+
format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
|
|
6046
|
+
pts: number(),
|
|
6047
|
+
byteLength: number().int().nonnegative(),
|
|
6048
|
+
nodeId: string(),
|
|
6049
|
+
slotCount: number().int().positive()
|
|
6050
|
+
});
|
|
6051
|
+
const FrameHandleFormatSchema = _enum(["rgb", "bgr", "yuv420", "gray"]);
|
|
6052
|
+
const SubscribeFramesInputSchema = object({
|
|
6053
|
+
brokerId: string(),
|
|
6054
|
+
format: FrameHandleFormatSchema,
|
|
6055
|
+
/**
|
|
6056
|
+
* Optional reader-side cadence hint in frames per second. The broker does
|
|
6057
|
+
* NOT throttle — latest-wins ring reads drop frames implicitly for a slow
|
|
6058
|
+
* consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
|
|
6059
|
+
* the consumer can pace its own `pullFrameHandles` polling.
|
|
6060
|
+
*/
|
|
6061
|
+
maxFps: number().positive().optional(),
|
|
6062
|
+
/** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
|
|
6063
|
+
tag: string().optional()
|
|
6064
|
+
});
|
|
6065
|
+
const SubscribeFramesResultSchema = object({
|
|
6066
|
+
/** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
|
|
6067
|
+
subscriptionId: string(),
|
|
6068
|
+
/** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
|
|
6069
|
+
maxFps: number().nonnegative()
|
|
6070
|
+
});
|
|
6071
|
+
const DecodedAudioChunkSchema = object({
|
|
6072
|
+
data: _instanceof(Uint8Array),
|
|
6073
|
+
sampleRate: number().int().positive(),
|
|
6074
|
+
channels: number().int().positive(),
|
|
6075
|
+
timestamp: number()
|
|
6076
|
+
});
|
|
6077
|
+
const SubscribeAudioChunksInputSchema = object({
|
|
6078
|
+
brokerId: string(),
|
|
6079
|
+
/** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
|
|
6080
|
+
tag: string().optional()
|
|
6081
|
+
});
|
|
6082
|
+
const SubscribeAudioChunksResultSchema = object({
|
|
6083
|
+
/** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
|
|
6084
|
+
subscriptionId: string()
|
|
6085
|
+
});
|
|
6023
6086
|
const BrokerStatusSchema$1 = _enum(["idle", "connecting", "streaming", "error", "stopped"]);
|
|
6024
6087
|
const BrokerStatsSchema = object({
|
|
6025
6088
|
status: BrokerStatusSchema$1,
|
|
@@ -6241,9 +6304,76 @@ const RtpSourceSchema = object({
|
|
|
6241
6304
|
object({ released: boolean(), refcount: number().int().nonnegative() }),
|
|
6242
6305
|
{ kind: "mutation", auth: "admin" }
|
|
6243
6306
|
),
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6307
|
+
/**
|
|
6308
|
+
* ── Decoded audio-chunk plane (Phase 5 / D9) ──────────────────────
|
|
6309
|
+
*
|
|
6310
|
+
* The serialisable replacement for the live-object `IStreamBroker.
|
|
6311
|
+
* onDecodedAudioChunk` callback path. Unlike the video frame plane,
|
|
6312
|
+
* audio chunks are tiny (a ~500ms PCM window is a few KB) so they ship
|
|
6313
|
+
* their bytes INLINE over tRPC — no shared-memory ring. A consumer:
|
|
6314
|
+
*
|
|
6315
|
+
* 1. `subscribeAudioChunks({ brokerId, tag })` — the broker registers
|
|
6316
|
+
* a per-subscription bounded FIFO queue and returns a
|
|
6317
|
+
* `subscriptionId`.
|
|
6318
|
+
* 2. polls `pullAudioChunks({ subscriptionId, maxCount })` — drains
|
|
6319
|
+
* `DecodedAudioChunk[]` in arrival order (FIFO, no latest-wins
|
|
6320
|
+
* drop: an audio gap is audible / breaks an analysis window). The
|
|
6321
|
+
* queue is generously sized; it only drops its oldest chunk if a
|
|
6322
|
+
* truly stalled consumer lets it overflow.
|
|
6323
|
+
* 3. `unsubscribeAudioChunks({ subscriptionId })` on teardown.
|
|
6324
|
+
*/
|
|
6325
|
+
subscribeAudioChunks: method(
|
|
6326
|
+
SubscribeAudioChunksInputSchema,
|
|
6327
|
+
SubscribeAudioChunksResultSchema,
|
|
6328
|
+
{ kind: "mutation" }
|
|
6329
|
+
),
|
|
6330
|
+
pullAudioChunks: method(
|
|
6331
|
+
object({
|
|
6332
|
+
subscriptionId: string(),
|
|
6333
|
+
maxCount: number().int().positive().default(8)
|
|
6334
|
+
}),
|
|
6335
|
+
array(DecodedAudioChunkSchema).readonly()
|
|
6336
|
+
),
|
|
6337
|
+
unsubscribeAudioChunks: method(
|
|
6338
|
+
object({ subscriptionId: string() }),
|
|
6339
|
+
object({ released: boolean() }),
|
|
6340
|
+
{ kind: "mutation" }
|
|
6341
|
+
),
|
|
6342
|
+
/**
|
|
6343
|
+
* ── Shared-memory frame plane (Phase 5 / D9) ──────────────────────
|
|
6344
|
+
*
|
|
6345
|
+
* The handle-based replacement for the live-object `IStreamBroker.
|
|
6346
|
+
* onDecodedFrame` callback path. A consumer:
|
|
6347
|
+
*
|
|
6348
|
+
* 1. `subscribeFrames({ brokerId, format })` — the broker spins up
|
|
6349
|
+
* (or reuses) a `frameSink: 'shm'` decoder session producing that
|
|
6350
|
+
* `format` and returns a `subscriptionId`.
|
|
6351
|
+
* 2. polls `pullFrameHandles({ subscriptionId, maxCount })` — drains
|
|
6352
|
+
* zero-pixel `FrameHandle[]`; each handle is fed to a
|
|
6353
|
+
* `FrameRingReader` that opens the named shm segment and reads the
|
|
6354
|
+
* pixels back zero-copy.
|
|
6355
|
+
* 3. `unsubscribeFrames({ subscriptionId })` on teardown.
|
|
6356
|
+
*
|
|
6357
|
+
* The broker keeps one shm ring per `(brokerId, format)` actually
|
|
6358
|
+
* requested — no broker-side `sharp` conversion. fps throttling is
|
|
6359
|
+
* implicit (latest-wins ring reads drop frames for a slow consumer).
|
|
6360
|
+
*/
|
|
6361
|
+
subscribeFrames: method(
|
|
6362
|
+
SubscribeFramesInputSchema,
|
|
6363
|
+
SubscribeFramesResultSchema,
|
|
6364
|
+
{ kind: "mutation" }
|
|
6365
|
+
),
|
|
6366
|
+
pullFrameHandles: method(
|
|
6367
|
+
object({
|
|
6368
|
+
subscriptionId: string(),
|
|
6369
|
+
maxCount: number().int().positive().default(4)
|
|
6370
|
+
}),
|
|
6371
|
+
array(FrameHandleSchema).readonly()
|
|
6372
|
+
),
|
|
6373
|
+
unsubscribeFrames: method(
|
|
6374
|
+
object({ subscriptionId: string() }),
|
|
6375
|
+
object({ released: boolean() }),
|
|
6376
|
+
{ kind: "mutation" }
|
|
6247
6377
|
),
|
|
6248
6378
|
setPreBufferDuration: method(
|
|
6249
6379
|
object({ brokerId: string(), seconds: number().min(0).max(30) }),
|
|
@@ -6299,6 +6429,8 @@ const cameraStreamsCapability = {
|
|
|
6299
6429
|
name: "camera-streams",
|
|
6300
6430
|
scope: "device",
|
|
6301
6431
|
mode: "singleton",
|
|
6432
|
+
kind: "wrapper",
|
|
6433
|
+
defaultActive: true,
|
|
6302
6434
|
deviceTypes: [DeviceType.Camera],
|
|
6303
6435
|
methods: {
|
|
6304
6436
|
getCameraStreams: method(
|
|
@@ -7395,6 +7527,42 @@ const motionTriggerCapability = {
|
|
|
7395
7527
|
},
|
|
7396
7528
|
runtimeState: MotionTriggerRuntimeStateSchema
|
|
7397
7529
|
};
|
|
7530
|
+
const MotionZoneStatusSchema = object({
|
|
7531
|
+
enabled: boolean(),
|
|
7532
|
+
sensitivity: number(),
|
|
7533
|
+
/** Row-major active-cell grid. Length = gridWidth*gridHeight (see getOptions). */
|
|
7534
|
+
cells: array(boolean()),
|
|
7535
|
+
lastFetchedAt: number()
|
|
7536
|
+
});
|
|
7537
|
+
const MotionZoneOptionsSchema = object({
|
|
7538
|
+
gridWidth: number(),
|
|
7539
|
+
gridHeight: number(),
|
|
7540
|
+
sensitivity: object({ min: number(), max: number(), step: number() })
|
|
7541
|
+
});
|
|
7542
|
+
const MotionZonePatchSchema = object({
|
|
7543
|
+
enabled: boolean().optional(),
|
|
7544
|
+
sensitivity: number().optional(),
|
|
7545
|
+
cells: array(boolean()).optional()
|
|
7546
|
+
});
|
|
7547
|
+
const motionZonesCapability = {
|
|
7548
|
+
name: "motion-zones",
|
|
7549
|
+
scope: "device",
|
|
7550
|
+
mode: "singleton",
|
|
7551
|
+
deviceTypes: [DeviceType.Camera],
|
|
7552
|
+
deviceConfig: {
|
|
7553
|
+
ui: { kind: "widget", widgetId: "host/motion-zones-grid", tab: "motion", label: "Motion Zones" }
|
|
7554
|
+
},
|
|
7555
|
+
methods: {
|
|
7556
|
+
getOptions: method(object({ deviceId: number() }), MotionZoneOptionsSchema),
|
|
7557
|
+
setZone: method(
|
|
7558
|
+
object({ deviceId: number(), patch: MotionZonePatchSchema }),
|
|
7559
|
+
_void(),
|
|
7560
|
+
{ kind: "mutation", auth: "admin" }
|
|
7561
|
+
)
|
|
7562
|
+
},
|
|
7563
|
+
status: { schema: MotionZoneStatusSchema, kind: "poll" },
|
|
7564
|
+
runtimeState: MotionZoneStatusSchema
|
|
7565
|
+
};
|
|
7398
7566
|
const AutotrackTargetTypeSchema = string().describe("Vendor target string (people/vehicle/pet); empty = camera default");
|
|
7399
7567
|
const PtzAutotrackSettingsSchema = object({
|
|
7400
7568
|
targetType: AutotrackTargetTypeSchema,
|
|
@@ -7433,6 +7601,9 @@ const ptzAutotrackCapability = {
|
|
|
7433
7601
|
scope: "device",
|
|
7434
7602
|
mode: "singleton",
|
|
7435
7603
|
deviceTypes: [DeviceType.Camera],
|
|
7604
|
+
deviceConfig: {
|
|
7605
|
+
ui: { kind: "widget", widgetId: "host/ptz-autotrack", tab: "ptz", topTab: true, label: "Auto-Tracking", order: 5 }
|
|
7606
|
+
},
|
|
7436
7607
|
methods: {
|
|
7437
7608
|
/**
|
|
7438
7609
|
* Read the current on/off state + last-applied settings.
|
|
@@ -7499,6 +7670,111 @@ const ptzAutotrackCapability = {
|
|
|
7499
7670
|
*/
|
|
7500
7671
|
runtimeState: PtzAutotrackRuntimeStateSchema
|
|
7501
7672
|
};
|
|
7673
|
+
const StreamProfileSchema = _enum(["main", "sub", "ext"]);
|
|
7674
|
+
const StreamProfileConfigSchema = object({
|
|
7675
|
+
width: number(),
|
|
7676
|
+
height: number(),
|
|
7677
|
+
codec: _enum(["h264", "h265"]),
|
|
7678
|
+
framerate: number(),
|
|
7679
|
+
bitrate: number(),
|
|
7680
|
+
// kbps
|
|
7681
|
+
bitrateMode: _enum(["vbr", "cbr"]).optional(),
|
|
7682
|
+
encoderProfile: _enum(["high", "main", "baseline"]).optional(),
|
|
7683
|
+
gop: number().optional(),
|
|
7684
|
+
audio: boolean().optional()
|
|
7685
|
+
});
|
|
7686
|
+
const StreamParamsStatusSchema = object({
|
|
7687
|
+
/** Per-profile current config. A profile absent = the camera doesn't have it. */
|
|
7688
|
+
main: StreamProfileConfigSchema.optional(),
|
|
7689
|
+
sub: StreamProfileConfigSchema.optional(),
|
|
7690
|
+
ext: StreamProfileConfigSchema.optional(),
|
|
7691
|
+
lastFetchedAt: number()
|
|
7692
|
+
});
|
|
7693
|
+
const StreamProfileOptionsSchema = object({
|
|
7694
|
+
resolutions: array(object({ width: number(), height: number() })),
|
|
7695
|
+
codecs: array(_enum(["h264", "h265"])),
|
|
7696
|
+
framerates: array(number()),
|
|
7697
|
+
/** Allowed bitrate values (kbps). Empty if the camera takes a free range. */
|
|
7698
|
+
bitrates: array(number()),
|
|
7699
|
+
/** Optional [min,max] kbps when the camera accepts a continuous range. */
|
|
7700
|
+
bitrateRange: tuple([number(), number()]).optional(),
|
|
7701
|
+
supportsBitrateMode: boolean(),
|
|
7702
|
+
supportsEncoderProfile: boolean(),
|
|
7703
|
+
supportsGop: boolean(),
|
|
7704
|
+
/** Allowed GOP / keyframe-interval range, in seconds — drives the
|
|
7705
|
+
* I-frame-interval selector. Absent when the camera advertises GOP
|
|
7706
|
+
* support but no concrete range (callers then fall back to a free
|
|
7707
|
+
* numeric input). `{ min, max, step }` per the getOptions convention. */
|
|
7708
|
+
gop: object({ min: number(), max: number(), step: number() }).optional()
|
|
7709
|
+
});
|
|
7710
|
+
const StreamParamsOptionsSchema = object({
|
|
7711
|
+
main: StreamProfileOptionsSchema.optional(),
|
|
7712
|
+
sub: StreamProfileOptionsSchema.optional(),
|
|
7713
|
+
ext: StreamProfileOptionsSchema.optional()
|
|
7714
|
+
});
|
|
7715
|
+
const StreamProfilePatchSchema = object({
|
|
7716
|
+
width: number().optional(),
|
|
7717
|
+
height: number().optional(),
|
|
7718
|
+
codec: _enum(["h264", "h265"]).optional(),
|
|
7719
|
+
framerate: number().optional(),
|
|
7720
|
+
bitrate: number().optional(),
|
|
7721
|
+
bitrateMode: _enum(["vbr", "cbr"]).optional(),
|
|
7722
|
+
encoderProfile: _enum(["high", "main", "baseline"]).optional(),
|
|
7723
|
+
gop: number().optional(),
|
|
7724
|
+
audio: boolean().optional()
|
|
7725
|
+
});
|
|
7726
|
+
const streamParamsCapability = {
|
|
7727
|
+
name: "stream-params",
|
|
7728
|
+
scope: "device",
|
|
7729
|
+
mode: "singleton",
|
|
7730
|
+
deviceTypes: [DeviceType.Camera],
|
|
7731
|
+
deviceConfig: {
|
|
7732
|
+
ui: { kind: "derived-form", builderId: "stream-params", tab: "streaming" }
|
|
7733
|
+
},
|
|
7734
|
+
methods: {
|
|
7735
|
+
getOptions: method(
|
|
7736
|
+
object({ deviceId: number() }),
|
|
7737
|
+
StreamParamsOptionsSchema
|
|
7738
|
+
),
|
|
7739
|
+
setProfile: method(
|
|
7740
|
+
object({
|
|
7741
|
+
deviceId: number(),
|
|
7742
|
+
profile: StreamProfileSchema,
|
|
7743
|
+
patch: StreamProfilePatchSchema
|
|
7744
|
+
}),
|
|
7745
|
+
_void(),
|
|
7746
|
+
{ kind: "mutation", auth: "admin" }
|
|
7747
|
+
),
|
|
7748
|
+
/**
|
|
7749
|
+
* Build the `ConfigUISchema` (admin-ui `ConfigFormBuilder` input
|
|
7750
|
+
* shape) for this camera's stream-encoder settings — one section per
|
|
7751
|
+
* profile (main / sub / ext) with the resolution / codec / framerate
|
|
7752
|
+
* / bitrate / bitrate-mode / encoder-profile / GOP controls the
|
|
7753
|
+
* firmware actually exposes.
|
|
7754
|
+
*
|
|
7755
|
+
* Driven by `getOptions` (camera-probed availability) + `getStatus`
|
|
7756
|
+
* (current per-profile config); each field's `default` is seeded
|
|
7757
|
+
* from the live config so the form renders the camera state in one
|
|
7758
|
+
* pass. Returns `null` when the camera exposes no configurable
|
|
7759
|
+
* stream property — the renderer then shows the unsupported message.
|
|
7760
|
+
*
|
|
7761
|
+
* Output is `z.unknown().nullable()` — the same convention every
|
|
7762
|
+
* other `ConfigUISchema`-returning cap method uses (`device-ops`,
|
|
7763
|
+
* `device-manager`); `ConfigUISchema` is a TS-only type with no
|
|
7764
|
+
* companion Zod schema, and a concrete object would collapse
|
|
7765
|
+
* unrelated AppRouter branches to `unknown` during codegen.
|
|
7766
|
+
*/
|
|
7767
|
+
getConfigSchema: method(
|
|
7768
|
+
object({ deviceId: number() }),
|
|
7769
|
+
unknown().nullable()
|
|
7770
|
+
)
|
|
7771
|
+
},
|
|
7772
|
+
status: {
|
|
7773
|
+
schema: StreamParamsStatusSchema,
|
|
7774
|
+
kind: "poll"
|
|
7775
|
+
},
|
|
7776
|
+
runtimeState: StreamParamsStatusSchema
|
|
7777
|
+
};
|
|
7502
7778
|
const SwitchStatusSchema = object({
|
|
7503
7779
|
on: boolean(),
|
|
7504
7780
|
/** Ms epoch of the last state change. Useful for UI "X minutes ago". */
|
|
@@ -8475,6 +8751,83 @@ const VersionOutputSchema = object({ version: string() });
|
|
|
8475
8751
|
getVersion: method(_void(), VersionOutputSchema)
|
|
8476
8752
|
}
|
|
8477
8753
|
});
|
|
8754
|
+
const MethodAccessSchema = _enum(["view", "create", "delete"]);
|
|
8755
|
+
const AllowedProviderSchema = union([literal("*"), array(string())]);
|
|
8756
|
+
const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
|
|
8757
|
+
const CapScopeSchema = _enum(["device", "system"]);
|
|
8758
|
+
const TokenScopeSchema = discriminatedUnion("type", [
|
|
8759
|
+
object({
|
|
8760
|
+
type: literal("category"),
|
|
8761
|
+
target: CapScopeSchema,
|
|
8762
|
+
access: array(MethodAccessSchema).min(1)
|
|
8763
|
+
}),
|
|
8764
|
+
object({
|
|
8765
|
+
type: literal("capability"),
|
|
8766
|
+
target: string(),
|
|
8767
|
+
access: array(MethodAccessSchema).min(1)
|
|
8768
|
+
}),
|
|
8769
|
+
object({
|
|
8770
|
+
type: literal("addon"),
|
|
8771
|
+
target: string(),
|
|
8772
|
+
access: array(MethodAccessSchema).min(1)
|
|
8773
|
+
}),
|
|
8774
|
+
object({
|
|
8775
|
+
type: literal("device"),
|
|
8776
|
+
/**
|
|
8777
|
+
* One or more deviceIds (serialised as strings for wire-format
|
|
8778
|
+
* consistency with the rest of the union). Matcher accepts if
|
|
8779
|
+
* `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
|
|
8780
|
+
* of one scope-per-device when granting access to a set of cameras.
|
|
8781
|
+
*/
|
|
8782
|
+
targets: array(string()).min(1),
|
|
8783
|
+
access: array(MethodAccessSchema).min(1)
|
|
8784
|
+
})
|
|
8785
|
+
]);
|
|
8786
|
+
object({
|
|
8787
|
+
id: string(),
|
|
8788
|
+
username: string(),
|
|
8789
|
+
passwordHash: string(),
|
|
8790
|
+
/**
|
|
8791
|
+
* Admin bypass. When true, the middleware skips the scope-access
|
|
8792
|
+
* check entirely. There is no other axis of privilege; the legacy
|
|
8793
|
+
* role enum collapsed onto this boolean in v2.
|
|
8794
|
+
*/
|
|
8795
|
+
isAdmin: boolean().default(false),
|
|
8796
|
+
allowedProviders: AllowedProviderSchema,
|
|
8797
|
+
allowedDevices: AllowedDevicesSchema,
|
|
8798
|
+
/**
|
|
8799
|
+
* Scopes granted to this user. Admins bypass; their `scopes` is
|
|
8800
|
+
* ignored. Non-admins without scopes are locked out of every
|
|
8801
|
+
* protected call.
|
|
8802
|
+
*/
|
|
8803
|
+
scopes: array(TokenScopeSchema).default([]),
|
|
8804
|
+
createdAt: number(),
|
|
8805
|
+
updatedAt: number()
|
|
8806
|
+
});
|
|
8807
|
+
object({
|
|
8808
|
+
id: string(),
|
|
8809
|
+
label: string(),
|
|
8810
|
+
isAdmin: boolean().default(false),
|
|
8811
|
+
allowedProviders: AllowedProviderSchema,
|
|
8812
|
+
allowedDevices: AllowedDevicesSchema,
|
|
8813
|
+
tokenHash: string(),
|
|
8814
|
+
tokenPrefix: string(),
|
|
8815
|
+
createdAt: number(),
|
|
8816
|
+
lastUsedAt: number().optional()
|
|
8817
|
+
});
|
|
8818
|
+
object({
|
|
8819
|
+
id: string(),
|
|
8820
|
+
userId: string(),
|
|
8821
|
+
name: string(),
|
|
8822
|
+
tokenHash: string(),
|
|
8823
|
+
tokenPrefix: string(),
|
|
8824
|
+
scopes: array(TokenScopeSchema),
|
|
8825
|
+
// SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
|
|
8826
|
+
// schema accepts both `null` (read from disk) and `undefined` (in-memory).
|
|
8827
|
+
expiresAt: number().nullish(),
|
|
8828
|
+
lastUsedAt: number().nullish(),
|
|
8829
|
+
createdAt: number()
|
|
8830
|
+
});
|
|
8478
8831
|
const SsoBridgeClaimsSchema = object({
|
|
8479
8832
|
userId: string(),
|
|
8480
8833
|
username: string(),
|
|
@@ -8490,7 +8843,18 @@ const SsoBridgeClaimsSchema = object({
|
|
|
8490
8843
|
* JWT WITHOUT verifying the signature — the hub re-verifies on every
|
|
8491
8844
|
* inbound call so trust still rests with the signing hub.
|
|
8492
8845
|
*/
|
|
8493
|
-
hubUrl: string().optional()
|
|
8846
|
+
hubUrl: string().optional(),
|
|
8847
|
+
/** Permission scopes baked into the token. Set by the OAuth
|
|
8848
|
+
* account-linking grant; absent on ordinary SSO-login tokens. */
|
|
8849
|
+
scopes: array(TokenScopeSchema).optional(),
|
|
8850
|
+
/** OAuth authorization-code binding — set only on `oauth-code` tokens. */
|
|
8851
|
+
redirectUri: string().optional(),
|
|
8852
|
+
integrationId: string().optional(),
|
|
8853
|
+
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
8854
|
+
jti: string().optional(),
|
|
8855
|
+
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
8856
|
+
* tokens so the verify path can check the session is not revoked. */
|
|
8857
|
+
sessionId: string().optional()
|
|
8494
8858
|
});
|
|
8495
8859
|
({
|
|
8496
8860
|
methods: {
|
|
@@ -8507,6 +8871,23 @@ const SsoBridgeClaimsSchema = object({
|
|
|
8507
8871
|
)
|
|
8508
8872
|
}
|
|
8509
8873
|
});
|
|
8874
|
+
const OauthIntegrationDescriptorSchema = object({
|
|
8875
|
+
/** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
|
|
8876
|
+
integrationId: string(),
|
|
8877
|
+
/** Human label rendered on the consent page. */
|
|
8878
|
+
displayName: string(),
|
|
8879
|
+
/** Scopes baked into every token issued for this integration. */
|
|
8880
|
+
requestedScopes: array(TokenScopeSchema),
|
|
8881
|
+
/** Allowed redirect_uri prefixes. /api/oauth2/authorize rejects any
|
|
8882
|
+
* redirect_uri that does not start with one of these. Required —
|
|
8883
|
+
* an empty list means the integration can never complete linking. */
|
|
8884
|
+
allowedRedirectPrefixes: array(string()).min(1)
|
|
8885
|
+
});
|
|
8886
|
+
({
|
|
8887
|
+
methods: {
|
|
8888
|
+
getDescriptor: method(_void(), OauthIntegrationDescriptorSchema)
|
|
8889
|
+
}
|
|
8890
|
+
});
|
|
8510
8891
|
const PasskeySummarySchema = object({
|
|
8511
8892
|
credentialId: string(),
|
|
8512
8893
|
label: string(),
|
|
@@ -8787,21 +9168,30 @@ const AddonPageDeclarationSchema = object({
|
|
|
8787
9168
|
});
|
|
8788
9169
|
const WidgetHostEnum = _enum(["device-tab", "dashboard", "integration-detail"]);
|
|
8789
9170
|
const WidgetSizeEnum = _enum(["xs", "sm", "md", "lg", "xl"]);
|
|
9171
|
+
const WidgetRemoteSchema = object({
|
|
9172
|
+
remoteName: string(),
|
|
9173
|
+
exposedModule: string(),
|
|
9174
|
+
componentKey: string().optional()
|
|
9175
|
+
});
|
|
8790
9176
|
const WidgetMetadataSchema = object({
|
|
8791
|
-
|
|
8792
|
-
|
|
9177
|
+
// ── UiContribution core (kind:'remote') ──────────────────────────
|
|
9178
|
+
/** Primary host tab — `'dashboard'`, `'device-tab'`, or a device-detail tab id. */
|
|
9179
|
+
tab: string(),
|
|
9180
|
+
/** Optional sub-tab within `tab`. */
|
|
9181
|
+
subTab: string().optional(),
|
|
8793
9182
|
/** Operator-facing label. */
|
|
8794
9183
|
label: string(),
|
|
9184
|
+
/** Ordering within `(tab, subTab)`, ascending. */
|
|
9185
|
+
order: number().optional(),
|
|
9186
|
+
/** Always `'remote'` — a widget is a Module Federation remote. */
|
|
9187
|
+
kind: literal("remote"),
|
|
9188
|
+
/** MF remote descriptor. */
|
|
9189
|
+
remote: WidgetRemoteSchema,
|
|
9190
|
+
// ── Widget-only metadata ─────────────────────────────────────────
|
|
9191
|
+
/** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
|
|
9192
|
+
stableId: string(),
|
|
8795
9193
|
description: string().optional(),
|
|
8796
9194
|
icon: string().optional(),
|
|
8797
|
-
/**
|
|
8798
|
-
* Module Federation remote name — must match the `name` field on the
|
|
8799
|
-
* widget addon's `federation()` plugin config. Used by the host's
|
|
8800
|
-
* `<WidgetRegistryProvider>` to call `loadRemote('<remoteName>/widgets')`.
|
|
8801
|
-
* Conventionally `addon_<addonid>_widgets` (snake_case; MF names
|
|
8802
|
-
* cannot contain hyphens).
|
|
8803
|
-
*/
|
|
8804
|
-
remoteName: string(),
|
|
8805
9195
|
/**
|
|
8806
9196
|
* Bundle filename inside the addon's `dist/` dir served at
|
|
8807
9197
|
* `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
|
|
@@ -8810,9 +9200,9 @@ const WidgetMetadataSchema = object({
|
|
|
8810
9200
|
* cache-buster URL without a separate filesystem stat.
|
|
8811
9201
|
*/
|
|
8812
9202
|
bundle: string(),
|
|
8813
|
-
/**
|
|
9203
|
+
/** Every host the widget supports. The picker filters on this set. */
|
|
8814
9204
|
hosts: array(WidgetHostEnum).readonly(),
|
|
8815
|
-
/** Required props the host must supply. Validated at
|
|
9205
|
+
/** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
|
|
8816
9206
|
requires: object({
|
|
8817
9207
|
deviceContext: boolean().default(false),
|
|
8818
9208
|
integrationContext: boolean().default(false)
|
|
@@ -8883,6 +9273,16 @@ const InvokeReplyEnvelopeSchema = object({
|
|
|
8883
9273
|
invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
|
|
8884
9274
|
}
|
|
8885
9275
|
});
|
|
9276
|
+
const ShmRingStatsSchema = object({
|
|
9277
|
+
sessionId: string(),
|
|
9278
|
+
slotCount: number().int(),
|
|
9279
|
+
slotByteLength: number().int(),
|
|
9280
|
+
segmentBytes: number().int(),
|
|
9281
|
+
budgetMb: number().int(),
|
|
9282
|
+
framesWritten: number().int(),
|
|
9283
|
+
getFrameHits: number().int(),
|
|
9284
|
+
getFrameMisses: number().int()
|
|
9285
|
+
});
|
|
8886
9286
|
({
|
|
8887
9287
|
methods: {
|
|
8888
9288
|
// ── Discovery ─────────────────────────────────────────────────
|
|
@@ -8910,10 +9310,27 @@ const InvokeReplyEnvelopeSchema = object({
|
|
|
8910
9310
|
url: string()
|
|
8911
9311
|
}), _void()),
|
|
8912
9312
|
// ── Output — polling-based frame retrieval ────────────────────
|
|
9313
|
+
// `pullFrames` drains the pixel `DecodedFrame[]` of a `frameSink:
|
|
9314
|
+
// 'callback'` session. `pullHandles` (Phase 5 / D9) drains the
|
|
9315
|
+
// zero-pixel `FrameHandle[]` of a `frameSink: 'shm'` session — the
|
|
9316
|
+
// broker hands each handle to a `FrameRingReader` that opens the
|
|
9317
|
+
// named segment and reads the pixels back zero-copy. A session is
|
|
9318
|
+
// one mode or the other; the unmatched method returns an empty
|
|
9319
|
+
// array.
|
|
8913
9320
|
pullFrames: method(object({
|
|
8914
9321
|
sessionId: string(),
|
|
8915
9322
|
maxCount: number().default(1)
|
|
8916
9323
|
}), array(DecodedFrameSchema)),
|
|
9324
|
+
pullHandles: method(object({
|
|
9325
|
+
sessionId: string(),
|
|
9326
|
+
maxCount: number().default(1)
|
|
9327
|
+
}), array(FrameHandleSchema)),
|
|
9328
|
+
// ── Frame fetch (Phase 5 / D9 downstream access) ──────────────
|
|
9329
|
+
// Read the pixels a FrameHandle refers to from this node's shm ring.
|
|
9330
|
+
// Returns null when the slot was already recycled (latest-wins).
|
|
9331
|
+
getFrame: method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()),
|
|
9332
|
+
// shm ring usage stats for a session.
|
|
9333
|
+
getShmStats: method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()),
|
|
8917
9334
|
// ── Control ───────────────────────────────────────────────────
|
|
8918
9335
|
updateConfig: method(object({
|
|
8919
9336
|
sessionId: string(),
|
|
@@ -10081,8 +10498,8 @@ const DevicePersistConfigPayloadSchema = object({
|
|
|
10081
10498
|
/**
|
|
10082
10499
|
* Return the addon ids that declared a wrapper provider for `capName`.
|
|
10083
10500
|
* Backs the device-bindings UI's wrapper-picker dropdown. Entries are
|
|
10084
|
-
* sourced from `CapabilityRegistry.wrapperProviders`, populated
|
|
10085
|
-
* `
|
|
10501
|
+
* sourced from `CapabilityRegistry.wrapperProviders`, populated when
|
|
10502
|
+
* the cap definition declares `kind: 'wrapper'`.
|
|
10086
10503
|
*/
|
|
10087
10504
|
listWrappersForCap: method(
|
|
10088
10505
|
object({ capName: string() }),
|
|
@@ -10388,51 +10805,6 @@ const AuthResultSchema = object({
|
|
|
10388
10805
|
validateToken: method(object({ token: string() }), AuthResultSchema.nullable())
|
|
10389
10806
|
}
|
|
10390
10807
|
});
|
|
10391
|
-
const AuthProviderInfoSchema = object({
|
|
10392
|
-
/** Stable id matching the addon id (used for `getLoginUrl({addonId,…})`). */
|
|
10393
|
-
addonId: string(),
|
|
10394
|
-
/**
|
|
10395
|
-
* Per-instance id when one addon registers multiple "logical"
|
|
10396
|
-
* providers (e.g. OIDC with Google + Microsoft + custom). The login
|
|
10397
|
-
* URL becomes `/addon/${addonId}/${instanceId}/start` — handler reads
|
|
10398
|
-
* `:instanceId` from the route. Empty/unset means the addon is a
|
|
10399
|
-
* single-instance provider; the URL is `/addon/${addonId}/start`.
|
|
10400
|
-
*/
|
|
10401
|
-
instanceId: string().optional(),
|
|
10402
|
-
/** Display label shown on the login button + admin row. */
|
|
10403
|
-
displayName: string(),
|
|
10404
|
-
/** Optional iconography hint (lucide-react icon name OR emoji). */
|
|
10405
|
-
icon: string().optional(),
|
|
10406
|
-
/** When true, the provider exposes a redirect-based login flow
|
|
10407
|
-
* (`getLoginUrl` returns a URL the browser navigates to). */
|
|
10408
|
-
hasRedirectFlow: boolean(),
|
|
10409
|
-
/** When true, the provider exposes a credential-form login flow
|
|
10410
|
-
* (`validateCredentials` accepts username + password). */
|
|
10411
|
-
hasCredentialFlow: boolean(),
|
|
10412
|
-
/** Provider kind, drives admin-UI hint dispatch (oidc / saml / totp / …). */
|
|
10413
|
-
kind: string().optional(),
|
|
10414
|
-
/** Operator-facing status string (e.g. "Connected to https://login.acme.com"). */
|
|
10415
|
-
status: string().optional(),
|
|
10416
|
-
/** When false, the provider is registered but disabled by config; the
|
|
10417
|
-
* UI surfaces it as inactive without enumerating it for login. */
|
|
10418
|
-
enabled: boolean()
|
|
10419
|
-
});
|
|
10420
|
-
({
|
|
10421
|
-
methods: {
|
|
10422
|
-
/** All registered auth providers, both enabled and disabled. */
|
|
10423
|
-
listProviders: method(_void(), array(AuthProviderInfoSchema).readonly()),
|
|
10424
|
-
/**
|
|
10425
|
-
* Toggle a provider's enabled flag. Disabled providers stay
|
|
10426
|
-
* registered but aren't surfaced on the login page. The orchestrator
|
|
10427
|
-
* persists the state in `addon-settings` so it survives restarts.
|
|
10428
|
-
*/
|
|
10429
|
-
setProviderEnabled: method(
|
|
10430
|
-
object({ addonId: string(), enabled: boolean() }),
|
|
10431
|
-
object({ success: literal(true) }),
|
|
10432
|
-
{ kind: "mutation", auth: "admin" }
|
|
10433
|
-
)
|
|
10434
|
-
}
|
|
10435
|
-
});
|
|
10436
10808
|
const NetworkEndpointSchema = object({
|
|
10437
10809
|
url: string(),
|
|
10438
10810
|
hostname: string(),
|
|
@@ -10464,55 +10836,13 @@ const NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
|
|
|
10464
10836
|
getEndpoint: method(_void(), NetworkEndpointSchema.nullable()),
|
|
10465
10837
|
getStatus: method(_void(), NetworkAccessStatusSchema),
|
|
10466
10838
|
/**
|
|
10467
|
-
* Enumerate every active ingress entry.
|
|
10468
|
-
*
|
|
10469
|
-
*
|
|
10839
|
+
* Enumerate every active ingress entry. Providers that expose only a
|
|
10840
|
+
* single endpoint may omit this method; callers fall back to
|
|
10841
|
+
* `getEndpoint()` in that case.
|
|
10470
10842
|
*/
|
|
10471
10843
|
listEndpoints: method(_void(), array(NetworkEndpointEntrySchema).readonly())
|
|
10472
10844
|
}
|
|
10473
10845
|
});
|
|
10474
|
-
const RemoteAccessEndpointSchema = object({
|
|
10475
|
-
url: string(),
|
|
10476
|
-
hostname: string(),
|
|
10477
|
-
port: number(),
|
|
10478
|
-
protocol: _enum(["http", "https"])
|
|
10479
|
-
});
|
|
10480
|
-
const RemoteAccessProviderInfoSchema = object({
|
|
10481
|
-
/** Stable id matching the addon id. */
|
|
10482
|
-
addonId: string(),
|
|
10483
|
-
/** Display label shown on the admin row — sourced from the addon manifest. */
|
|
10484
|
-
displayName: string(),
|
|
10485
|
-
/** When false, the provider is registered but disabled. */
|
|
10486
|
-
enabled: boolean(),
|
|
10487
|
-
/** True when the underlying tunnel/connection is up. */
|
|
10488
|
-
connected: boolean(),
|
|
10489
|
-
/** Public-facing endpoint, when connected. Null otherwise. */
|
|
10490
|
-
endpoint: RemoteAccessEndpointSchema.nullable(),
|
|
10491
|
-
/** Last error message (when connected=false), if available. */
|
|
10492
|
-
error: string().optional()
|
|
10493
|
-
});
|
|
10494
|
-
({
|
|
10495
|
-
methods: {
|
|
10496
|
-
/** All registered remote-access providers + their live status. */
|
|
10497
|
-
listProviders: method(_void(), array(RemoteAccessProviderInfoSchema).readonly()),
|
|
10498
|
-
/**
|
|
10499
|
-
* Start a specific provider's tunnel. Per-provider config still
|
|
10500
|
-
* lives on the addon's settings panel; this is just the on/off
|
|
10501
|
-
* trigger so the admin UI can manage the lifecycle from one place.
|
|
10502
|
-
*/
|
|
10503
|
-
startProvider: method(
|
|
10504
|
-
object({ addonId: string() }),
|
|
10505
|
-
RemoteAccessEndpointSchema,
|
|
10506
|
-
{ kind: "mutation", auth: "admin" }
|
|
10507
|
-
),
|
|
10508
|
-
/** Stop a specific provider's tunnel (idempotent on already-stopped). */
|
|
10509
|
-
stopProvider: method(
|
|
10510
|
-
object({ addonId: string() }),
|
|
10511
|
-
object({ success: literal(true) }),
|
|
10512
|
-
{ kind: "mutation", auth: "admin" }
|
|
10513
|
-
)
|
|
10514
|
-
}
|
|
10515
|
-
});
|
|
10516
10846
|
const TurnServerSchema = object({
|
|
10517
10847
|
/** Single URL or list of URLs (e.g. "turn:turn.example.com:3478?transport=udp"). */
|
|
10518
10848
|
urls: union([string(), array(string())]),
|
|
@@ -10532,45 +10862,6 @@ const TurnServerSchema = object({
|
|
|
10532
10862
|
)
|
|
10533
10863
|
}
|
|
10534
10864
|
});
|
|
10535
|
-
const TurnProviderInfoSchema = object({
|
|
10536
|
-
/** Stable id matching the addon id. */
|
|
10537
|
-
addonId: string(),
|
|
10538
|
-
/** Display label shown on the admin row — sourced from the addon manifest. */
|
|
10539
|
-
displayName: string(),
|
|
10540
|
-
/** When false, the provider is registered but disabled. */
|
|
10541
|
-
enabled: boolean(),
|
|
10542
|
-
/** Number of servers this provider is currently exposing. */
|
|
10543
|
-
serverCount: number(),
|
|
10544
|
-
/**
|
|
10545
|
-
* Flat list of every TURN/STUN URL this provider currently exposes.
|
|
10546
|
-
* One row per URL (multi-URL ICE server entries are flattened). The
|
|
10547
|
-
* admin UI shows this in a compact per-provider list so operators
|
|
10548
|
-
* can verify what's actually being negotiated without having to dig
|
|
10549
|
-
* into the combined `getAllServers` output.
|
|
10550
|
-
*/
|
|
10551
|
-
urls: array(string()).readonly(),
|
|
10552
|
-
/** Last fetch error (when serverCount=0 due to API failure), if any. */
|
|
10553
|
-
error: string().optional()
|
|
10554
|
-
});
|
|
10555
|
-
({
|
|
10556
|
-
methods: {
|
|
10557
|
-
/** All registered TURN providers + per-provider stats. */
|
|
10558
|
-
listProviders: method(_void(), array(TurnProviderInfoSchema).readonly()),
|
|
10559
|
-
/**
|
|
10560
|
-
* Combined list of TURN/STUN servers from all ENABLED providers.
|
|
10561
|
-
* Consumed by the WebRTC layer at session-creation time —
|
|
10562
|
-
* implementations may fetch fresh short-lived credentials each
|
|
10563
|
-
* call (e.g. Cloudflare API), so consumers SHOULD call per-session.
|
|
10564
|
-
*/
|
|
10565
|
-
getAllServers: method(_void(), array(TurnServerSchema).readonly()),
|
|
10566
|
-
/** Toggle a provider's enabled flag. */
|
|
10567
|
-
setProviderEnabled: method(
|
|
10568
|
-
object({ addonId: string(), enabled: boolean() }),
|
|
10569
|
-
object({ success: literal(true) }),
|
|
10570
|
-
{ kind: "mutation", auth: "admin" }
|
|
10571
|
-
)
|
|
10572
|
-
}
|
|
10573
|
-
});
|
|
10574
10865
|
const SnapshotImageSchema = object({
|
|
10575
10866
|
base64: string(),
|
|
10576
10867
|
contentType: string()
|
|
@@ -10589,6 +10880,8 @@ const snapshotCapability = {
|
|
|
10589
10880
|
name: "snapshot",
|
|
10590
10881
|
scope: "device",
|
|
10591
10882
|
mode: "singleton",
|
|
10883
|
+
kind: "wrapper",
|
|
10884
|
+
defaultActive: true,
|
|
10592
10885
|
deviceTypes: [DeviceType.Camera],
|
|
10593
10886
|
// Owns per-device snapshot settings (preferred stream, debug logging).
|
|
10594
10887
|
// The three DeviceSettingsContribution methods are auto-added to the
|
|
@@ -11310,6 +11603,18 @@ const PtzMoveCommandSchema = object({
|
|
|
11310
11603
|
zoom: number().optional(),
|
|
11311
11604
|
speed: number().optional()
|
|
11312
11605
|
});
|
|
11606
|
+
PtzPositionSchema.extend({ autofocus: boolean() });
|
|
11607
|
+
const PtzOptionsSchema = object({
|
|
11608
|
+
hasPan: boolean(),
|
|
11609
|
+
hasTilt: boolean(),
|
|
11610
|
+
hasZoom: boolean(),
|
|
11611
|
+
supportsPresets: boolean(),
|
|
11612
|
+
/** Max number of named presets the camera supports, when known. */
|
|
11613
|
+
maxPresets: number().optional(),
|
|
11614
|
+
/** Whether the camera exposes a controllable autofocus toggle
|
|
11615
|
+
* (boolean `hasX` per the getOptions availability convention). */
|
|
11616
|
+
hasAutofocus: boolean()
|
|
11617
|
+
});
|
|
11313
11618
|
({
|
|
11314
11619
|
deviceTypes: [DeviceType.Camera],
|
|
11315
11620
|
methods: {
|
|
@@ -11337,6 +11642,20 @@ const PtzMoveCommandSchema = object({
|
|
|
11337
11642
|
_void(),
|
|
11338
11643
|
{ kind: "mutation" }
|
|
11339
11644
|
),
|
|
11645
|
+
savePreset: method(
|
|
11646
|
+
object({ deviceId: number(), presetId: string(), name: string() }),
|
|
11647
|
+
_void(),
|
|
11648
|
+
{ kind: "mutation", auth: "admin" }
|
|
11649
|
+
),
|
|
11650
|
+
deletePreset: method(
|
|
11651
|
+
object({ deviceId: number(), presetId: string() }),
|
|
11652
|
+
_void(),
|
|
11653
|
+
{ kind: "mutation", auth: "admin" }
|
|
11654
|
+
),
|
|
11655
|
+
getOptions: method(
|
|
11656
|
+
object({ deviceId: number() }),
|
|
11657
|
+
PtzOptionsSchema
|
|
11658
|
+
),
|
|
11340
11659
|
goHome: method(
|
|
11341
11660
|
object({ deviceId: number() }),
|
|
11342
11661
|
_void(),
|
|
@@ -11351,6 +11670,13 @@ const PtzMoveCommandSchema = object({
|
|
|
11351
11670
|
getPosition: method(
|
|
11352
11671
|
object({ deviceId: number() }),
|
|
11353
11672
|
PtzPositionSchema
|
|
11673
|
+
),
|
|
11674
|
+
/** Toggle the camera's autofocus. Only meaningful when
|
|
11675
|
+
* `getOptions().hasAutofocus` is true. */
|
|
11676
|
+
setAutofocus: method(
|
|
11677
|
+
object({ deviceId: number(), enabled: boolean() }),
|
|
11678
|
+
_void(),
|
|
11679
|
+
{ kind: "mutation" }
|
|
11354
11680
|
)
|
|
11355
11681
|
}
|
|
11356
11682
|
});
|
|
@@ -12050,7 +12376,7 @@ const AllowedAddressesSchema = object({
|
|
|
12050
12376
|
)
|
|
12051
12377
|
}
|
|
12052
12378
|
});
|
|
12053
|
-
const MeshEndpointSchema
|
|
12379
|
+
const MeshEndpointSchema = object({
|
|
12054
12380
|
/** Stable identifier within the provider (e.g. `mesh-ipv4`, `magicdns`, `funnel`). */
|
|
12055
12381
|
id: string(),
|
|
12056
12382
|
/** Operator-facing label (e.g. "Mesh IPv4", "MagicDNS"). */
|
|
@@ -12123,7 +12449,7 @@ const MeshStatusSchema = object({
|
|
|
12123
12449
|
/** Number of peers visible to this host (excluding self). */
|
|
12124
12450
|
peerCount: number(),
|
|
12125
12451
|
/** Every endpoint this provider exposes for the current host. */
|
|
12126
|
-
endpoints: array(MeshEndpointSchema
|
|
12452
|
+
endpoints: array(MeshEndpointSchema).readonly(),
|
|
12127
12453
|
/** Last error from the daemon, when not joined. */
|
|
12128
12454
|
error: string().optional(),
|
|
12129
12455
|
// ── Account / tenant identity (generic across providers) ────────
|
|
@@ -12286,182 +12612,6 @@ const MeshStatusSchema = object({
|
|
|
12286
12612
|
// tabs driven by this cap.
|
|
12287
12613
|
}
|
|
12288
12614
|
});
|
|
12289
|
-
const MeshEndpointSchema = object({
|
|
12290
|
-
id: string(),
|
|
12291
|
-
label: string(),
|
|
12292
|
-
scope: _enum(["mesh", "public"]),
|
|
12293
|
-
url: string(),
|
|
12294
|
-
hostname: string(),
|
|
12295
|
-
port: number(),
|
|
12296
|
-
protocol: _enum(["http", "https"])
|
|
12297
|
-
});
|
|
12298
|
-
const MeshProviderInfoSchema = object({
|
|
12299
|
-
/** Stable id matching the addon id. */
|
|
12300
|
-
addonId: string(),
|
|
12301
|
-
/** Display label shown on the admin row — sourced from the addon manifest. */
|
|
12302
|
-
displayName: string(),
|
|
12303
|
-
/** True when the host is joined to this provider's mesh. */
|
|
12304
|
-
joined: boolean(),
|
|
12305
|
-
/** Local mesh IP (empty when not joined). */
|
|
12306
|
-
meshIp: string(),
|
|
12307
|
-
/** MagicDNS / mesh hostname (empty when not configured). */
|
|
12308
|
-
magicDnsHostname: string(),
|
|
12309
|
-
/** Peer count (excluding self). */
|
|
12310
|
-
peerCount: number(),
|
|
12311
|
-
/** Active endpoints (mesh IP + MagicDNS + optional public Funnel). */
|
|
12312
|
-
endpoints: array(MeshEndpointSchema).readonly(),
|
|
12313
|
-
/** Last error reported by the provider. */
|
|
12314
|
-
error: string().optional(),
|
|
12315
|
-
// ── Generic identity fields mirrored from MeshStatus ─────────────
|
|
12316
|
-
/** Tenant / tailnet / network display name. Empty pre-join. */
|
|
12317
|
-
tenantName: string(),
|
|
12318
|
-
/** Mesh DNS suffix (e.g. tailXXXX.ts.net). Empty when not configured. */
|
|
12319
|
-
magicDnsSuffix: string(),
|
|
12320
|
-
/** Authenticated user / account login. Null for token-only providers. */
|
|
12321
|
-
userLogin: string().nullable(),
|
|
12322
|
-
/** Provider control-plane URL. */
|
|
12323
|
-
controlPlaneUrl: string(),
|
|
12324
|
-
/** Machine-key expiry (epoch ms). Null when keys don't rotate. */
|
|
12325
|
-
keyExpiry: number().nullable()
|
|
12326
|
-
});
|
|
12327
|
-
({
|
|
12328
|
-
methods: {
|
|
12329
|
-
/** All registered mesh-network providers + live status. */
|
|
12330
|
-
listProviders: method(_void(), array(MeshProviderInfoSchema).readonly()),
|
|
12331
|
-
/**
|
|
12332
|
-
* Join the mesh of a specific provider. Per-provider config still
|
|
12333
|
-
* lives on its settings panel; the orchestrator forwards.
|
|
12334
|
-
*/
|
|
12335
|
-
joinProvider: method(
|
|
12336
|
-
object({
|
|
12337
|
-
addonId: string(),
|
|
12338
|
-
authKey: string().min(8),
|
|
12339
|
-
hostname: string().optional()
|
|
12340
|
-
}),
|
|
12341
|
-
object({ joined: literal(true) }),
|
|
12342
|
-
{ kind: "mutation" }
|
|
12343
|
-
),
|
|
12344
|
-
leaveProvider: method(
|
|
12345
|
-
object({ addonId: string() }),
|
|
12346
|
-
object({ success: literal(true) }),
|
|
12347
|
-
{ kind: "mutation" }
|
|
12348
|
-
),
|
|
12349
|
-
/**
|
|
12350
|
-
* Browser-redirect login flow. Forwards to the named provider's
|
|
12351
|
-
* `mesh-network.startLogin` and returns the URL the daemon
|
|
12352
|
-
* prints. UI opens it in a new tab, then polls `listProviders`
|
|
12353
|
-
* for `joined: true`.
|
|
12354
|
-
*/
|
|
12355
|
-
startLoginProvider: method(
|
|
12356
|
-
object({
|
|
12357
|
-
addonId: string(),
|
|
12358
|
-
hostname: string().optional()
|
|
12359
|
-
}),
|
|
12360
|
-
object({ loginUrl: string() }),
|
|
12361
|
-
{ kind: "mutation" }
|
|
12362
|
-
),
|
|
12363
|
-
/**
|
|
12364
|
-
* Sign out of the provider's account entirely (`mesh-network.logout`).
|
|
12365
|
-
* Distinct from `leaveProvider` which only takes the host off-mesh;
|
|
12366
|
-
* `logoutProvider` wipes credentials so the next start requires a
|
|
12367
|
-
* fresh login.
|
|
12368
|
-
*/
|
|
12369
|
-
logoutProvider: method(
|
|
12370
|
-
object({ addonId: string() }),
|
|
12371
|
-
object({ loggedOut: literal(true) }),
|
|
12372
|
-
{ kind: "mutation" }
|
|
12373
|
-
),
|
|
12374
|
-
/**
|
|
12375
|
-
* Per-provider peer list. Forwards to `mesh-network.listPeers` on
|
|
12376
|
-
* the addressed provider. Separate from `listProviders` because
|
|
12377
|
-
* peer payloads can be large on a heavily-populated tailnet —
|
|
12378
|
-
* fetch only when the operator opens the Peers tab.
|
|
12379
|
-
*/
|
|
12380
|
-
listProviderPeers: method(
|
|
12381
|
-
object({ addonId: string() }),
|
|
12382
|
-
object({
|
|
12383
|
-
peers: array(MeshPeerSchema).readonly()
|
|
12384
|
-
})
|
|
12385
|
-
)
|
|
12386
|
-
}
|
|
12387
|
-
});
|
|
12388
|
-
const MethodAccessSchema = _enum(["view", "create", "delete"]);
|
|
12389
|
-
const AllowedProviderSchema = union([literal("*"), array(string())]);
|
|
12390
|
-
const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
|
|
12391
|
-
const CapScopeSchema = _enum(["device", "system"]);
|
|
12392
|
-
const TokenScopeSchema = discriminatedUnion("type", [
|
|
12393
|
-
object({
|
|
12394
|
-
type: literal("category"),
|
|
12395
|
-
target: CapScopeSchema,
|
|
12396
|
-
access: array(MethodAccessSchema).min(1)
|
|
12397
|
-
}),
|
|
12398
|
-
object({
|
|
12399
|
-
type: literal("capability"),
|
|
12400
|
-
target: string(),
|
|
12401
|
-
access: array(MethodAccessSchema).min(1)
|
|
12402
|
-
}),
|
|
12403
|
-
object({
|
|
12404
|
-
type: literal("addon"),
|
|
12405
|
-
target: string(),
|
|
12406
|
-
access: array(MethodAccessSchema).min(1)
|
|
12407
|
-
}),
|
|
12408
|
-
object({
|
|
12409
|
-
type: literal("device"),
|
|
12410
|
-
/**
|
|
12411
|
-
* One or more deviceIds (serialised as strings for wire-format
|
|
12412
|
-
* consistency with the rest of the union). Matcher accepts if
|
|
12413
|
-
* `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
|
|
12414
|
-
* of one scope-per-device when granting access to a set of cameras.
|
|
12415
|
-
*/
|
|
12416
|
-
targets: array(string()).min(1),
|
|
12417
|
-
access: array(MethodAccessSchema).min(1)
|
|
12418
|
-
})
|
|
12419
|
-
]);
|
|
12420
|
-
object({
|
|
12421
|
-
id: string(),
|
|
12422
|
-
username: string(),
|
|
12423
|
-
passwordHash: string(),
|
|
12424
|
-
/**
|
|
12425
|
-
* Admin bypass. When true, the middleware skips the scope-access
|
|
12426
|
-
* check entirely. There is no other axis of privilege; the legacy
|
|
12427
|
-
* role enum collapsed onto this boolean in v2.
|
|
12428
|
-
*/
|
|
12429
|
-
isAdmin: boolean().default(false),
|
|
12430
|
-
allowedProviders: AllowedProviderSchema,
|
|
12431
|
-
allowedDevices: AllowedDevicesSchema,
|
|
12432
|
-
/**
|
|
12433
|
-
* Scopes granted to this user. Admins bypass; their `scopes` is
|
|
12434
|
-
* ignored. Non-admins without scopes are locked out of every
|
|
12435
|
-
* protected call.
|
|
12436
|
-
*/
|
|
12437
|
-
scopes: array(TokenScopeSchema).default([]),
|
|
12438
|
-
createdAt: number(),
|
|
12439
|
-
updatedAt: number()
|
|
12440
|
-
});
|
|
12441
|
-
object({
|
|
12442
|
-
id: string(),
|
|
12443
|
-
label: string(),
|
|
12444
|
-
isAdmin: boolean().default(false),
|
|
12445
|
-
allowedProviders: AllowedProviderSchema,
|
|
12446
|
-
allowedDevices: AllowedDevicesSchema,
|
|
12447
|
-
tokenHash: string(),
|
|
12448
|
-
tokenPrefix: string(),
|
|
12449
|
-
createdAt: number(),
|
|
12450
|
-
lastUsedAt: number().optional()
|
|
12451
|
-
});
|
|
12452
|
-
object({
|
|
12453
|
-
id: string(),
|
|
12454
|
-
userId: string(),
|
|
12455
|
-
name: string(),
|
|
12456
|
-
tokenHash: string(),
|
|
12457
|
-
tokenPrefix: string(),
|
|
12458
|
-
scopes: array(TokenScopeSchema),
|
|
12459
|
-
// SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
|
|
12460
|
-
// schema accepts both `null` (read from disk) and `undefined` (in-memory).
|
|
12461
|
-
expiresAt: number().nullish(),
|
|
12462
|
-
lastUsedAt: number().nullish(),
|
|
12463
|
-
createdAt: number()
|
|
12464
|
-
});
|
|
12465
12615
|
const UserSummarySchema = object({
|
|
12466
12616
|
id: string(),
|
|
12467
12617
|
username: string(),
|
|
@@ -12534,6 +12684,16 @@ const CreateScopedTokenResultSchema = object({
|
|
|
12534
12684
|
token: string(),
|
|
12535
12685
|
record: ScopedTokenSummarySchema
|
|
12536
12686
|
});
|
|
12687
|
+
const OauthSessionSummarySchema = object({
|
|
12688
|
+
id: string(),
|
|
12689
|
+
userId: string(),
|
|
12690
|
+
username: string(),
|
|
12691
|
+
integrationId: string(),
|
|
12692
|
+
scopes: array(TokenScopeSchema),
|
|
12693
|
+
createdAt: number(),
|
|
12694
|
+
lastUsedAt: number(),
|
|
12695
|
+
revokedAt: number().nullable()
|
|
12696
|
+
});
|
|
12537
12697
|
const TotpSetupResultSchema = object({
|
|
12538
12698
|
secret: string(),
|
|
12539
12699
|
otpauthUrl: string()
|
|
@@ -12609,6 +12769,66 @@ const TotpStatusSchema = object({
|
|
|
12609
12769
|
object({ userId: string(), code: string() }),
|
|
12610
12770
|
object({ valid: boolean() }),
|
|
12611
12771
|
{ kind: "mutation", access: "view" }
|
|
12772
|
+
),
|
|
12773
|
+
// ── OAuth account-linking grant ────────────────────────────────
|
|
12774
|
+
//
|
|
12775
|
+
// Core's /oauth2/* endpoints delegate here. Tokens are sso-bridge
|
|
12776
|
+
// JWTs (kinds oauth-code / oauth-access / oauth-refresh) and ALWAYS
|
|
12777
|
+
// carry isAdmin:false — the operator login proves hub control; the
|
|
12778
|
+
// issued token is minimal (device scope only).
|
|
12779
|
+
oauthIssueCode: method(
|
|
12780
|
+
object({
|
|
12781
|
+
integrationId: string(),
|
|
12782
|
+
userId: string(),
|
|
12783
|
+
username: string(),
|
|
12784
|
+
scopes: array(TokenScopeSchema),
|
|
12785
|
+
redirectUri: string(),
|
|
12786
|
+
hubUrl: string()
|
|
12787
|
+
}),
|
|
12788
|
+
object({ code: string() }),
|
|
12789
|
+
{ kind: "mutation", access: "create" }
|
|
12790
|
+
),
|
|
12791
|
+
oauthExchangeCode: method(
|
|
12792
|
+
object({ code: string(), redirectUri: string() }),
|
|
12793
|
+
object({
|
|
12794
|
+
accessToken: string(),
|
|
12795
|
+
refreshToken: string(),
|
|
12796
|
+
expiresIn: number()
|
|
12797
|
+
}).nullable(),
|
|
12798
|
+
{ kind: "mutation", access: "view" }
|
|
12799
|
+
),
|
|
12800
|
+
oauthRefresh: method(
|
|
12801
|
+
object({ refreshToken: string() }),
|
|
12802
|
+
object({
|
|
12803
|
+
accessToken: string(),
|
|
12804
|
+
refreshToken: string(),
|
|
12805
|
+
expiresIn: number()
|
|
12806
|
+
}).nullable(),
|
|
12807
|
+
{ kind: "mutation", access: "view" }
|
|
12808
|
+
),
|
|
12809
|
+
oauthVerifyAccessToken: method(
|
|
12810
|
+
object({ token: string() }),
|
|
12811
|
+
object({
|
|
12812
|
+
userId: string(),
|
|
12813
|
+
username: string(),
|
|
12814
|
+
scopes: array(TokenScopeSchema)
|
|
12815
|
+
}).nullable(),
|
|
12816
|
+
{ access: "view" }
|
|
12817
|
+
),
|
|
12818
|
+
// ── OAuth linked-session management (Phase D) ──────────────────
|
|
12819
|
+
//
|
|
12820
|
+
// The admin UI lists active account-linking sessions and revokes
|
|
12821
|
+
// them; revocation makes the linked integration's tokens fail
|
|
12822
|
+
// verification immediately.
|
|
12823
|
+
listOauthSessions: method(
|
|
12824
|
+
_void(),
|
|
12825
|
+
array(OauthSessionSummarySchema),
|
|
12826
|
+
{ auth: "admin" }
|
|
12827
|
+
),
|
|
12828
|
+
revokeOauthSession: method(
|
|
12829
|
+
object({ id: string() }),
|
|
12830
|
+
object({ success: boolean() }),
|
|
12831
|
+
{ kind: "mutation", auth: "admin", access: "delete" }
|
|
12612
12832
|
)
|
|
12613
12833
|
}
|
|
12614
12834
|
});
|
|
@@ -12825,6 +13045,19 @@ const RenameNodeResultSchema = object({
|
|
|
12825
13045
|
record(string(), ClusterAddonStatusEntrySchema),
|
|
12826
13046
|
{ auth: "admin" }
|
|
12827
13047
|
),
|
|
13048
|
+
getCapUsageGraph: method(
|
|
13049
|
+
object({
|
|
13050
|
+
windowSeconds: number().int().positive().max(300).default(60)
|
|
13051
|
+
}),
|
|
13052
|
+
array(object({
|
|
13053
|
+
callerAddonId: string(),
|
|
13054
|
+
providerAddonId: string(),
|
|
13055
|
+
capName: string(),
|
|
13056
|
+
callsPerMin: number(),
|
|
13057
|
+
lastCallAtMs: number()
|
|
13058
|
+
})).readonly(),
|
|
13059
|
+
{ auth: "admin" }
|
|
13060
|
+
),
|
|
12828
13061
|
/**
|
|
12829
13062
|
* Direct per-node addon listing — calls `$agent.status` on the target
|
|
12830
13063
|
* node (or returns the hub registry for `nodeId === 'hub'`) and surfaces
|
|
@@ -13254,6 +13487,29 @@ const CustomActionInputSchema = object({
|
|
|
13254
13487
|
isActive: boolean()
|
|
13255
13488
|
})).readonly()
|
|
13256
13489
|
),
|
|
13490
|
+
/**
|
|
13491
|
+
* Toggle a single collection-cap provider on/off. Generic write-side
|
|
13492
|
+
* counterpart of `listCapabilityProviders` — drives the per-provider
|
|
13493
|
+
* Enable/Disable affordance in admin pages (TURN servers, etc.)
|
|
13494
|
+
* without needing a bespoke orchestrator cap.
|
|
13495
|
+
*
|
|
13496
|
+
* Reaches the hub's `CapabilityRegistry` directly:
|
|
13497
|
+
* `enableCollectionProvider` / `disableCollectionProvider` flip the
|
|
13498
|
+
* registry-level `disabledProviders` set. `getCollectionEntries`
|
|
13499
|
+
* already filters disabled providers out, so a disabled provider
|
|
13500
|
+
* drops out of every collection aggregate immediately. Only valid
|
|
13501
|
+
* for `mode: 'collection'` caps — the registry no-ops + warns for
|
|
13502
|
+
* singletons.
|
|
13503
|
+
*/
|
|
13504
|
+
setCapabilityProviderEnabled: method(
|
|
13505
|
+
object({
|
|
13506
|
+
capName: string().min(1),
|
|
13507
|
+
addonId: string().min(1),
|
|
13508
|
+
enabled: boolean()
|
|
13509
|
+
}),
|
|
13510
|
+
object({ success: literal(true) }),
|
|
13511
|
+
{ kind: "mutation", auth: "admin" }
|
|
13512
|
+
),
|
|
13257
13513
|
/**
|
|
13258
13514
|
* Live-update one of the framework packages marked
|
|
13259
13515
|
* `camstack.system: true` (`@camstack/types|kernel|core|sdk|ui-library`).
|
|
@@ -14131,7 +14387,9 @@ const DEVICE_LOCAL_STATE_CAPS = {
|
|
|
14131
14387
|
featureProbe: featureProbeCapability,
|
|
14132
14388
|
motion: motionCapability,
|
|
14133
14389
|
motionTrigger: motionTriggerCapability,
|
|
14390
|
+
motionZones: motionZonesCapability,
|
|
14134
14391
|
ptzAutotrack: ptzAutotrackCapability,
|
|
14392
|
+
streamParams: streamParamsCapability,
|
|
14135
14393
|
switch: switchCapability,
|
|
14136
14394
|
zoneAnalytics: zoneAnalyticsCapability,
|
|
14137
14395
|
zoneRules: zoneRulesCapability,
|