@camstack/addon-provider-rtsp 0.1.27 → 0.1.29

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 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
- getBroker: method(
6245
- object({ brokerId: string() }),
6246
- custom()
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
- /** Stable id within the addon — kebab-case. */
8792
- stableId: string(),
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
- /** Where the widget makes sense to render. */
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 <WidgetSlot> mount. */
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 at
10085
- * `ProviderRegistration.kind === 'wrapper'` time.
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() }),
@@ -10463,6 +10880,8 @@ const snapshotCapability = {
10463
10880
  name: "snapshot",
10464
10881
  scope: "device",
10465
10882
  mode: "singleton",
10883
+ kind: "wrapper",
10884
+ defaultActive: true,
10466
10885
  deviceTypes: [DeviceType.Camera],
10467
10886
  // Owns per-device snapshot settings (preferred stream, debug logging).
10468
10887
  // The three DeviceSettingsContribution methods are auto-added to the
@@ -11184,6 +11603,18 @@ const PtzMoveCommandSchema = object({
11184
11603
  zoom: number().optional(),
11185
11604
  speed: number().optional()
11186
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
+ });
11187
11618
  ({
11188
11619
  deviceTypes: [DeviceType.Camera],
11189
11620
  methods: {
@@ -11211,6 +11642,20 @@ const PtzMoveCommandSchema = object({
11211
11642
  _void(),
11212
11643
  { kind: "mutation" }
11213
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
+ ),
11214
11659
  goHome: method(
11215
11660
  object({ deviceId: number() }),
11216
11661
  _void(),
@@ -11225,6 +11670,13 @@ const PtzMoveCommandSchema = object({
11225
11670
  getPosition: method(
11226
11671
  object({ deviceId: number() }),
11227
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" }
11228
11680
  )
11229
11681
  }
11230
11682
  });
@@ -12160,83 +12612,6 @@ const MeshStatusSchema = object({
12160
12612
  // tabs driven by this cap.
12161
12613
  }
12162
12614
  });
12163
- const MethodAccessSchema = _enum(["view", "create", "delete"]);
12164
- const AllowedProviderSchema = union([literal("*"), array(string())]);
12165
- const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
12166
- const CapScopeSchema = _enum(["device", "system"]);
12167
- const TokenScopeSchema = discriminatedUnion("type", [
12168
- object({
12169
- type: literal("category"),
12170
- target: CapScopeSchema,
12171
- access: array(MethodAccessSchema).min(1)
12172
- }),
12173
- object({
12174
- type: literal("capability"),
12175
- target: string(),
12176
- access: array(MethodAccessSchema).min(1)
12177
- }),
12178
- object({
12179
- type: literal("addon"),
12180
- target: string(),
12181
- access: array(MethodAccessSchema).min(1)
12182
- }),
12183
- object({
12184
- type: literal("device"),
12185
- /**
12186
- * One or more deviceIds (serialised as strings for wire-format
12187
- * consistency with the rest of the union). Matcher accepts if
12188
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
12189
- * of one scope-per-device when granting access to a set of cameras.
12190
- */
12191
- targets: array(string()).min(1),
12192
- access: array(MethodAccessSchema).min(1)
12193
- })
12194
- ]);
12195
- object({
12196
- id: string(),
12197
- username: string(),
12198
- passwordHash: string(),
12199
- /**
12200
- * Admin bypass. When true, the middleware skips the scope-access
12201
- * check entirely. There is no other axis of privilege; the legacy
12202
- * role enum collapsed onto this boolean in v2.
12203
- */
12204
- isAdmin: boolean().default(false),
12205
- allowedProviders: AllowedProviderSchema,
12206
- allowedDevices: AllowedDevicesSchema,
12207
- /**
12208
- * Scopes granted to this user. Admins bypass; their `scopes` is
12209
- * ignored. Non-admins without scopes are locked out of every
12210
- * protected call.
12211
- */
12212
- scopes: array(TokenScopeSchema).default([]),
12213
- createdAt: number(),
12214
- updatedAt: number()
12215
- });
12216
- object({
12217
- id: string(),
12218
- label: string(),
12219
- isAdmin: boolean().default(false),
12220
- allowedProviders: AllowedProviderSchema,
12221
- allowedDevices: AllowedDevicesSchema,
12222
- tokenHash: string(),
12223
- tokenPrefix: string(),
12224
- createdAt: number(),
12225
- lastUsedAt: number().optional()
12226
- });
12227
- object({
12228
- id: string(),
12229
- userId: string(),
12230
- name: string(),
12231
- tokenHash: string(),
12232
- tokenPrefix: string(),
12233
- scopes: array(TokenScopeSchema),
12234
- // SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
12235
- // schema accepts both `null` (read from disk) and `undefined` (in-memory).
12236
- expiresAt: number().nullish(),
12237
- lastUsedAt: number().nullish(),
12238
- createdAt: number()
12239
- });
12240
12615
  const UserSummarySchema = object({
12241
12616
  id: string(),
12242
12617
  username: string(),
@@ -12309,6 +12684,16 @@ const CreateScopedTokenResultSchema = object({
12309
12684
  token: string(),
12310
12685
  record: ScopedTokenSummarySchema
12311
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
+ });
12312
12697
  const TotpSetupResultSchema = object({
12313
12698
  secret: string(),
12314
12699
  otpauthUrl: string()
@@ -12384,6 +12769,66 @@ const TotpStatusSchema = object({
12384
12769
  object({ userId: string(), code: string() }),
12385
12770
  object({ valid: boolean() }),
12386
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" }
12387
12832
  )
12388
12833
  }
12389
12834
  });
@@ -12600,6 +13045,19 @@ const RenameNodeResultSchema = object({
12600
13045
  record(string(), ClusterAddonStatusEntrySchema),
12601
13046
  { auth: "admin" }
12602
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
+ ),
12603
13061
  /**
12604
13062
  * Direct per-node addon listing — calls `$agent.status` on the target
12605
13063
  * node (or returns the hub registry for `nodeId === 'hub'`) and surfaces
@@ -13929,7 +14387,9 @@ const DEVICE_LOCAL_STATE_CAPS = {
13929
14387
  featureProbe: featureProbeCapability,
13930
14388
  motion: motionCapability,
13931
14389
  motionTrigger: motionTriggerCapability,
14390
+ motionZones: motionZonesCapability,
13932
14391
  ptzAutotrack: ptzAutotrackCapability,
14392
+ streamParams: streamParamsCapability,
13933
14393
  switch: switchCapability,
13934
14394
  zoneAnalytics: zoneAnalyticsCapability,
13935
14395
  zoneRules: zoneRulesCapability,