@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.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() }),
@@ -10386,51 +10803,6 @@ const AuthResultSchema = object({
10386
10803
  validateToken: method(object({ token: string() }), AuthResultSchema.nullable())
10387
10804
  }
10388
10805
  });
10389
- const AuthProviderInfoSchema = object({
10390
- /** Stable id matching the addon id (used for `getLoginUrl({addonId,…})`). */
10391
- addonId: string(),
10392
- /**
10393
- * Per-instance id when one addon registers multiple "logical"
10394
- * providers (e.g. OIDC with Google + Microsoft + custom). The login
10395
- * URL becomes `/addon/${addonId}/${instanceId}/start` — handler reads
10396
- * `:instanceId` from the route. Empty/unset means the addon is a
10397
- * single-instance provider; the URL is `/addon/${addonId}/start`.
10398
- */
10399
- instanceId: string().optional(),
10400
- /** Display label shown on the login button + admin row. */
10401
- displayName: string(),
10402
- /** Optional iconography hint (lucide-react icon name OR emoji). */
10403
- icon: string().optional(),
10404
- /** When true, the provider exposes a redirect-based login flow
10405
- * (`getLoginUrl` returns a URL the browser navigates to). */
10406
- hasRedirectFlow: boolean(),
10407
- /** When true, the provider exposes a credential-form login flow
10408
- * (`validateCredentials` accepts username + password). */
10409
- hasCredentialFlow: boolean(),
10410
- /** Provider kind, drives admin-UI hint dispatch (oidc / saml / totp / …). */
10411
- kind: string().optional(),
10412
- /** Operator-facing status string (e.g. "Connected to https://login.acme.com"). */
10413
- status: string().optional(),
10414
- /** When false, the provider is registered but disabled by config; the
10415
- * UI surfaces it as inactive without enumerating it for login. */
10416
- enabled: boolean()
10417
- });
10418
- ({
10419
- methods: {
10420
- /** All registered auth providers, both enabled and disabled. */
10421
- listProviders: method(_void(), array(AuthProviderInfoSchema).readonly()),
10422
- /**
10423
- * Toggle a provider's enabled flag. Disabled providers stay
10424
- * registered but aren't surfaced on the login page. The orchestrator
10425
- * persists the state in `addon-settings` so it survives restarts.
10426
- */
10427
- setProviderEnabled: method(
10428
- object({ addonId: string(), enabled: boolean() }),
10429
- object({ success: literal(true) }),
10430
- { kind: "mutation", auth: "admin" }
10431
- )
10432
- }
10433
- });
10434
10806
  const NetworkEndpointSchema = object({
10435
10807
  url: string(),
10436
10808
  hostname: string(),
@@ -10462,55 +10834,13 @@ const NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
10462
10834
  getEndpoint: method(_void(), NetworkEndpointSchema.nullable()),
10463
10835
  getStatus: method(_void(), NetworkAccessStatusSchema),
10464
10836
  /**
10465
- * Enumerate every active ingress entry. Default implementation (when
10466
- * the provider omits this method) is derived from `getEndpoint()` —
10467
- * see the remote-access orchestrator for the fallback path.
10837
+ * Enumerate every active ingress entry. Providers that expose only a
10838
+ * single endpoint may omit this method; callers fall back to
10839
+ * `getEndpoint()` in that case.
10468
10840
  */
10469
10841
  listEndpoints: method(_void(), array(NetworkEndpointEntrySchema).readonly())
10470
10842
  }
10471
10843
  });
10472
- const RemoteAccessEndpointSchema = object({
10473
- url: string(),
10474
- hostname: string(),
10475
- port: number(),
10476
- protocol: _enum(["http", "https"])
10477
- });
10478
- const RemoteAccessProviderInfoSchema = object({
10479
- /** Stable id matching the addon id. */
10480
- addonId: string(),
10481
- /** Display label shown on the admin row — sourced from the addon manifest. */
10482
- displayName: string(),
10483
- /** When false, the provider is registered but disabled. */
10484
- enabled: boolean(),
10485
- /** True when the underlying tunnel/connection is up. */
10486
- connected: boolean(),
10487
- /** Public-facing endpoint, when connected. Null otherwise. */
10488
- endpoint: RemoteAccessEndpointSchema.nullable(),
10489
- /** Last error message (when connected=false), if available. */
10490
- error: string().optional()
10491
- });
10492
- ({
10493
- methods: {
10494
- /** All registered remote-access providers + their live status. */
10495
- listProviders: method(_void(), array(RemoteAccessProviderInfoSchema).readonly()),
10496
- /**
10497
- * Start a specific provider's tunnel. Per-provider config still
10498
- * lives on the addon's settings panel; this is just the on/off
10499
- * trigger so the admin UI can manage the lifecycle from one place.
10500
- */
10501
- startProvider: method(
10502
- object({ addonId: string() }),
10503
- RemoteAccessEndpointSchema,
10504
- { kind: "mutation", auth: "admin" }
10505
- ),
10506
- /** Stop a specific provider's tunnel (idempotent on already-stopped). */
10507
- stopProvider: method(
10508
- object({ addonId: string() }),
10509
- object({ success: literal(true) }),
10510
- { kind: "mutation", auth: "admin" }
10511
- )
10512
- }
10513
- });
10514
10844
  const TurnServerSchema = object({
10515
10845
  /** Single URL or list of URLs (e.g. "turn:turn.example.com:3478?transport=udp"). */
10516
10846
  urls: union([string(), array(string())]),
@@ -10530,45 +10860,6 @@ const TurnServerSchema = object({
10530
10860
  )
10531
10861
  }
10532
10862
  });
10533
- const TurnProviderInfoSchema = object({
10534
- /** Stable id matching the addon id. */
10535
- addonId: string(),
10536
- /** Display label shown on the admin row — sourced from the addon manifest. */
10537
- displayName: string(),
10538
- /** When false, the provider is registered but disabled. */
10539
- enabled: boolean(),
10540
- /** Number of servers this provider is currently exposing. */
10541
- serverCount: number(),
10542
- /**
10543
- * Flat list of every TURN/STUN URL this provider currently exposes.
10544
- * One row per URL (multi-URL ICE server entries are flattened). The
10545
- * admin UI shows this in a compact per-provider list so operators
10546
- * can verify what's actually being negotiated without having to dig
10547
- * into the combined `getAllServers` output.
10548
- */
10549
- urls: array(string()).readonly(),
10550
- /** Last fetch error (when serverCount=0 due to API failure), if any. */
10551
- error: string().optional()
10552
- });
10553
- ({
10554
- methods: {
10555
- /** All registered TURN providers + per-provider stats. */
10556
- listProviders: method(_void(), array(TurnProviderInfoSchema).readonly()),
10557
- /**
10558
- * Combined list of TURN/STUN servers from all ENABLED providers.
10559
- * Consumed by the WebRTC layer at session-creation time —
10560
- * implementations may fetch fresh short-lived credentials each
10561
- * call (e.g. Cloudflare API), so consumers SHOULD call per-session.
10562
- */
10563
- getAllServers: method(_void(), array(TurnServerSchema).readonly()),
10564
- /** Toggle a provider's enabled flag. */
10565
- setProviderEnabled: method(
10566
- object({ addonId: string(), enabled: boolean() }),
10567
- object({ success: literal(true) }),
10568
- { kind: "mutation", auth: "admin" }
10569
- )
10570
- }
10571
- });
10572
10863
  const SnapshotImageSchema = object({
10573
10864
  base64: string(),
10574
10865
  contentType: string()
@@ -10587,6 +10878,8 @@ const snapshotCapability = {
10587
10878
  name: "snapshot",
10588
10879
  scope: "device",
10589
10880
  mode: "singleton",
10881
+ kind: "wrapper",
10882
+ defaultActive: true,
10590
10883
  deviceTypes: [DeviceType.Camera],
10591
10884
  // Owns per-device snapshot settings (preferred stream, debug logging).
10592
10885
  // The three DeviceSettingsContribution methods are auto-added to the
@@ -11308,6 +11601,18 @@ const PtzMoveCommandSchema = object({
11308
11601
  zoom: number().optional(),
11309
11602
  speed: number().optional()
11310
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
+ });
11311
11616
  ({
11312
11617
  deviceTypes: [DeviceType.Camera],
11313
11618
  methods: {
@@ -11335,6 +11640,20 @@ const PtzMoveCommandSchema = object({
11335
11640
  _void(),
11336
11641
  { kind: "mutation" }
11337
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
+ ),
11338
11657
  goHome: method(
11339
11658
  object({ deviceId: number() }),
11340
11659
  _void(),
@@ -11349,6 +11668,13 @@ const PtzMoveCommandSchema = object({
11349
11668
  getPosition: method(
11350
11669
  object({ deviceId: number() }),
11351
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" }
11352
11678
  )
11353
11679
  }
11354
11680
  });
@@ -12048,7 +12374,7 @@ const AllowedAddressesSchema = object({
12048
12374
  )
12049
12375
  }
12050
12376
  });
12051
- const MeshEndpointSchema$1 = object({
12377
+ const MeshEndpointSchema = object({
12052
12378
  /** Stable identifier within the provider (e.g. `mesh-ipv4`, `magicdns`, `funnel`). */
12053
12379
  id: string(),
12054
12380
  /** Operator-facing label (e.g. "Mesh IPv4", "MagicDNS"). */
@@ -12121,7 +12447,7 @@ const MeshStatusSchema = object({
12121
12447
  /** Number of peers visible to this host (excluding self). */
12122
12448
  peerCount: number(),
12123
12449
  /** Every endpoint this provider exposes for the current host. */
12124
- endpoints: array(MeshEndpointSchema$1).readonly(),
12450
+ endpoints: array(MeshEndpointSchema).readonly(),
12125
12451
  /** Last error from the daemon, when not joined. */
12126
12452
  error: string().optional(),
12127
12453
  // ── Account / tenant identity (generic across providers) ────────
@@ -12284,182 +12610,6 @@ const MeshStatusSchema = object({
12284
12610
  // tabs driven by this cap.
12285
12611
  }
12286
12612
  });
12287
- const MeshEndpointSchema = object({
12288
- id: string(),
12289
- label: string(),
12290
- scope: _enum(["mesh", "public"]),
12291
- url: string(),
12292
- hostname: string(),
12293
- port: number(),
12294
- protocol: _enum(["http", "https"])
12295
- });
12296
- const MeshProviderInfoSchema = object({
12297
- /** Stable id matching the addon id. */
12298
- addonId: string(),
12299
- /** Display label shown on the admin row — sourced from the addon manifest. */
12300
- displayName: string(),
12301
- /** True when the host is joined to this provider's mesh. */
12302
- joined: boolean(),
12303
- /** Local mesh IP (empty when not joined). */
12304
- meshIp: string(),
12305
- /** MagicDNS / mesh hostname (empty when not configured). */
12306
- magicDnsHostname: string(),
12307
- /** Peer count (excluding self). */
12308
- peerCount: number(),
12309
- /** Active endpoints (mesh IP + MagicDNS + optional public Funnel). */
12310
- endpoints: array(MeshEndpointSchema).readonly(),
12311
- /** Last error reported by the provider. */
12312
- error: string().optional(),
12313
- // ── Generic identity fields mirrored from MeshStatus ─────────────
12314
- /** Tenant / tailnet / network display name. Empty pre-join. */
12315
- tenantName: string(),
12316
- /** Mesh DNS suffix (e.g. tailXXXX.ts.net). Empty when not configured. */
12317
- magicDnsSuffix: string(),
12318
- /** Authenticated user / account login. Null for token-only providers. */
12319
- userLogin: string().nullable(),
12320
- /** Provider control-plane URL. */
12321
- controlPlaneUrl: string(),
12322
- /** Machine-key expiry (epoch ms). Null when keys don't rotate. */
12323
- keyExpiry: number().nullable()
12324
- });
12325
- ({
12326
- methods: {
12327
- /** All registered mesh-network providers + live status. */
12328
- listProviders: method(_void(), array(MeshProviderInfoSchema).readonly()),
12329
- /**
12330
- * Join the mesh of a specific provider. Per-provider config still
12331
- * lives on its settings panel; the orchestrator forwards.
12332
- */
12333
- joinProvider: method(
12334
- object({
12335
- addonId: string(),
12336
- authKey: string().min(8),
12337
- hostname: string().optional()
12338
- }),
12339
- object({ joined: literal(true) }),
12340
- { kind: "mutation" }
12341
- ),
12342
- leaveProvider: method(
12343
- object({ addonId: string() }),
12344
- object({ success: literal(true) }),
12345
- { kind: "mutation" }
12346
- ),
12347
- /**
12348
- * Browser-redirect login flow. Forwards to the named provider's
12349
- * `mesh-network.startLogin` and returns the URL the daemon
12350
- * prints. UI opens it in a new tab, then polls `listProviders`
12351
- * for `joined: true`.
12352
- */
12353
- startLoginProvider: method(
12354
- object({
12355
- addonId: string(),
12356
- hostname: string().optional()
12357
- }),
12358
- object({ loginUrl: string() }),
12359
- { kind: "mutation" }
12360
- ),
12361
- /**
12362
- * Sign out of the provider's account entirely (`mesh-network.logout`).
12363
- * Distinct from `leaveProvider` which only takes the host off-mesh;
12364
- * `logoutProvider` wipes credentials so the next start requires a
12365
- * fresh login.
12366
- */
12367
- logoutProvider: method(
12368
- object({ addonId: string() }),
12369
- object({ loggedOut: literal(true) }),
12370
- { kind: "mutation" }
12371
- ),
12372
- /**
12373
- * Per-provider peer list. Forwards to `mesh-network.listPeers` on
12374
- * the addressed provider. Separate from `listProviders` because
12375
- * peer payloads can be large on a heavily-populated tailnet —
12376
- * fetch only when the operator opens the Peers tab.
12377
- */
12378
- listProviderPeers: method(
12379
- object({ addonId: string() }),
12380
- object({
12381
- peers: array(MeshPeerSchema).readonly()
12382
- })
12383
- )
12384
- }
12385
- });
12386
- const MethodAccessSchema = _enum(["view", "create", "delete"]);
12387
- const AllowedProviderSchema = union([literal("*"), array(string())]);
12388
- const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
12389
- const CapScopeSchema = _enum(["device", "system"]);
12390
- const TokenScopeSchema = discriminatedUnion("type", [
12391
- object({
12392
- type: literal("category"),
12393
- target: CapScopeSchema,
12394
- access: array(MethodAccessSchema).min(1)
12395
- }),
12396
- object({
12397
- type: literal("capability"),
12398
- target: string(),
12399
- access: array(MethodAccessSchema).min(1)
12400
- }),
12401
- object({
12402
- type: literal("addon"),
12403
- target: string(),
12404
- access: array(MethodAccessSchema).min(1)
12405
- }),
12406
- object({
12407
- type: literal("device"),
12408
- /**
12409
- * One or more deviceIds (serialised as strings for wire-format
12410
- * consistency with the rest of the union). Matcher accepts if
12411
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
12412
- * of one scope-per-device when granting access to a set of cameras.
12413
- */
12414
- targets: array(string()).min(1),
12415
- access: array(MethodAccessSchema).min(1)
12416
- })
12417
- ]);
12418
- object({
12419
- id: string(),
12420
- username: string(),
12421
- passwordHash: string(),
12422
- /**
12423
- * Admin bypass. When true, the middleware skips the scope-access
12424
- * check entirely. There is no other axis of privilege; the legacy
12425
- * role enum collapsed onto this boolean in v2.
12426
- */
12427
- isAdmin: boolean().default(false),
12428
- allowedProviders: AllowedProviderSchema,
12429
- allowedDevices: AllowedDevicesSchema,
12430
- /**
12431
- * Scopes granted to this user. Admins bypass; their `scopes` is
12432
- * ignored. Non-admins without scopes are locked out of every
12433
- * protected call.
12434
- */
12435
- scopes: array(TokenScopeSchema).default([]),
12436
- createdAt: number(),
12437
- updatedAt: number()
12438
- });
12439
- object({
12440
- id: string(),
12441
- label: string(),
12442
- isAdmin: boolean().default(false),
12443
- allowedProviders: AllowedProviderSchema,
12444
- allowedDevices: AllowedDevicesSchema,
12445
- tokenHash: string(),
12446
- tokenPrefix: string(),
12447
- createdAt: number(),
12448
- lastUsedAt: number().optional()
12449
- });
12450
- object({
12451
- id: string(),
12452
- userId: string(),
12453
- name: string(),
12454
- tokenHash: string(),
12455
- tokenPrefix: string(),
12456
- scopes: array(TokenScopeSchema),
12457
- // SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
12458
- // schema accepts both `null` (read from disk) and `undefined` (in-memory).
12459
- expiresAt: number().nullish(),
12460
- lastUsedAt: number().nullish(),
12461
- createdAt: number()
12462
- });
12463
12613
  const UserSummarySchema = object({
12464
12614
  id: string(),
12465
12615
  username: string(),
@@ -12532,6 +12682,16 @@ const CreateScopedTokenResultSchema = object({
12532
12682
  token: string(),
12533
12683
  record: ScopedTokenSummarySchema
12534
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
+ });
12535
12695
  const TotpSetupResultSchema = object({
12536
12696
  secret: string(),
12537
12697
  otpauthUrl: string()
@@ -12607,6 +12767,66 @@ const TotpStatusSchema = object({
12607
12767
  object({ userId: string(), code: string() }),
12608
12768
  object({ valid: boolean() }),
12609
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" }
12610
12830
  )
12611
12831
  }
12612
12832
  });
@@ -12823,6 +13043,19 @@ const RenameNodeResultSchema = object({
12823
13043
  record(string(), ClusterAddonStatusEntrySchema),
12824
13044
  { auth: "admin" }
12825
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
+ ),
12826
13059
  /**
12827
13060
  * Direct per-node addon listing — calls `$agent.status` on the target
12828
13061
  * node (or returns the hub registry for `nodeId === 'hub'`) and surfaces
@@ -13252,6 +13485,29 @@ const CustomActionInputSchema = object({
13252
13485
  isActive: boolean()
13253
13486
  })).readonly()
13254
13487
  ),
13488
+ /**
13489
+ * Toggle a single collection-cap provider on/off. Generic write-side
13490
+ * counterpart of `listCapabilityProviders` — drives the per-provider
13491
+ * Enable/Disable affordance in admin pages (TURN servers, etc.)
13492
+ * without needing a bespoke orchestrator cap.
13493
+ *
13494
+ * Reaches the hub's `CapabilityRegistry` directly:
13495
+ * `enableCollectionProvider` / `disableCollectionProvider` flip the
13496
+ * registry-level `disabledProviders` set. `getCollectionEntries`
13497
+ * already filters disabled providers out, so a disabled provider
13498
+ * drops out of every collection aggregate immediately. Only valid
13499
+ * for `mode: 'collection'` caps — the registry no-ops + warns for
13500
+ * singletons.
13501
+ */
13502
+ setCapabilityProviderEnabled: method(
13503
+ object({
13504
+ capName: string().min(1),
13505
+ addonId: string().min(1),
13506
+ enabled: boolean()
13507
+ }),
13508
+ object({ success: literal(true) }),
13509
+ { kind: "mutation", auth: "admin" }
13510
+ ),
13255
13511
  /**
13256
13512
  * Live-update one of the framework packages marked
13257
13513
  * `camstack.system: true` (`@camstack/types|kernel|core|sdk|ui-library`).
@@ -14129,7 +14385,9 @@ const DEVICE_LOCAL_STATE_CAPS = {
14129
14385
  featureProbe: featureProbeCapability,
14130
14386
  motion: motionCapability,
14131
14387
  motionTrigger: motionTriggerCapability,
14388
+ motionZones: motionZonesCapability,
14132
14389
  ptzAutotrack: ptzAutotrackCapability,
14390
+ streamParams: streamParamsCapability,
14133
14391
  switch: switchCapability,
14134
14392
  zoneAnalytics: zoneAnalyticsCapability,
14135
14393
  zoneRules: zoneRulesCapability,