@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.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
- getBroker: method(
6243
- object({ brokerId: string() }),
6244
- custom()
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
- /** Stable id within the addon — kebab-case. */
8790
- stableId: string(),
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
- /** Where the widget makes sense to render. */
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 <WidgetSlot> mount. */
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 at
10083
- * `ProviderRegistration.kind === 'wrapper'` time.
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,