@camstack/addon-admin-ui 0.1.36 → 0.1.38

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.
Files changed (49) hide show
  1. package/dist/assets/{__mfe_internal__admin_ui_host__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-CeLhKMin.js → __mfe_internal__admin_ui_host__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-Cv6qLu_-.js} +1 -1
  2. package/dist/assets/{__mfe_internal__admin_ui_host__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs_commonjs-proxy-DXPQtD9p.js → __mfe_internal__admin_ui_host__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs_commonjs-proxy-CChEG9e0.js} +1 -1
  3. package/dist/assets/{__mfe_internal__admin_ui_host__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-OcRSBqRe.js → __mfe_internal__admin_ui_host__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-C1BmHlGM.js} +1 -1
  4. package/dist/assets/{__mfe_internal__admin_ui_host__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs_commonjs-proxy-GvBGLFI2.js → __mfe_internal__admin_ui_host__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs_commonjs-proxy-Bxv_3KWz.js} +1 -1
  5. package/dist/assets/__mfe_internal__admin_ui_host__loadShare__konva__loadShare__.mjs-g85Oidc0.js +15 -0
  6. package/dist/assets/__mfe_internal__admin_ui_host__loadShare__konva__loadShare__.mjs_commonjs-proxy-Bqeq2Zwj.js +1 -0
  7. package/dist/assets/{__mfe_internal__admin_ui_host__loadShare__react__loadShare__.mjs-Dl4u0xYE.js → __mfe_internal__admin_ui_host__loadShare__react__loadShare__.mjs-DC63qZbr.js} +1 -1
  8. package/dist/assets/{__mfe_internal__admin_ui_host__loadShare__react__loadShare__.mjs_commonjs-proxy-CSyOECfO.js → __mfe_internal__admin_ui_host__loadShare__react__loadShare__.mjs_commonjs-proxy-D2C49VyZ.js} +1 -1
  9. package/dist/assets/{__mfe_internal__admin_ui_host__loadShare__react_mf_2_dom__loadShare__.mjs-CzO_Esxm.js → __mfe_internal__admin_ui_host__loadShare__react_mf_2_dom__loadShare__.mjs-D3WbXy3O.js} +1 -1
  10. package/dist/assets/{__mfe_internal__admin_ui_host__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-xw6c6JeS.js → __mfe_internal__admin_ui_host__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-Csg6EbPB.js} +1 -1
  11. package/dist/assets/{__mfe_internal__admin_ui_host__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-DNpf5SLz.js → __mfe_internal__admin_ui_host__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-CUc0oFNo.js} +2 -2
  12. package/dist/assets/{__mfe_internal__admin_ui_host__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs_commonjs-proxy-BEKtiDI3.js → __mfe_internal__admin_ui_host__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs_commonjs-proxy-vh0hhA5d.js} +1 -1
  13. package/dist/assets/__mfe_internal__admin_ui_host__loadShare__react_mf_2_konva__loadShare__.mjs-CpGtTxci.js +35 -0
  14. package/dist/assets/__mfe_internal__admin_ui_host__loadShare__react_mf_2_konva__loadShare__.mjs_commonjs-proxy-RUDPE9aM.js +1 -0
  15. package/dist/assets/_virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-W-FrsYv6.js +1 -0
  16. package/dist/assets/{devices-D4k34mru.js → devices-GLp7me2a.js} +1 -1
  17. package/dist/assets/hostInit-c5F5KLjV.js +1 -0
  18. package/dist/assets/index-2B5djHSh.js +1150 -0
  19. package/dist/assets/index-ByRJWRUP.js +1 -0
  20. package/dist/assets/index-COoei6lw.css +1 -0
  21. package/dist/assets/index-CtHpLu0L.js +1 -0
  22. package/dist/assets/{index-D92THlot.js → index-CwF7Dxf8.js} +1 -1
  23. package/dist/assets/{index-BI3Z7SSi.js → index-D6khiudP.js} +1 -1
  24. package/dist/assets/index-DNY_otk4.js +151 -0
  25. package/dist/assets/index-DQFO9ZU6.js +1 -0
  26. package/dist/assets/{index-BGZqVJZt.js → index-JJ5NLHxC.js} +1 -1
  27. package/dist/assets/{index-Bwcmc2hW.js → index-TiJwCuZx.js} +1 -1
  28. package/dist/assets/index-WrAHDGEr.js +87 -0
  29. package/dist/assets/index-b0D9qthW.js +1 -0
  30. package/dist/assets/method-access-map-DLEhlu0x.js +1 -0
  31. package/dist/assets/{remoteEntry-BRybuw2S.js → remoteEntry-BJZZ6pth.js} +1 -1
  32. package/dist/assets/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-DUkp33c_.js → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-DWHckfBG.js} +6 -6
  33. package/dist/index.html +6 -6
  34. package/dist/mf-entry-bootstrap-0.js +2 -2
  35. package/dist/server/addon.js +528 -96
  36. package/dist/server/addon.js.map +1 -1
  37. package/dist/sw.js +1 -1
  38. package/package.json +10 -2
  39. package/dist/assets/_virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-DYU-qDnu.js +0 -1
  40. package/dist/assets/hostInit-CBKchP1G.js +0 -1
  41. package/dist/assets/index-BEewR-UX.js +0 -1
  42. package/dist/assets/index-BRSAFSOw.css +0 -1
  43. package/dist/assets/index-BqMPfvQx.js +0 -1
  44. package/dist/assets/index-CP1R6E2R.js +0 -88
  45. package/dist/assets/index-CwjnPcOV.js +0 -152
  46. package/dist/assets/index-Dmon2mdO.js +0 -1
  47. package/dist/assets/index-KlKJOjpC.js +0 -1
  48. package/dist/assets/index-SyrojlmO.js +0 -1156
  49. package/dist/assets/method-access-map-BFPyQ2-G.js +0 -1
@@ -5069,6 +5069,7 @@ const WELL_KNOWN_TABS = [
5069
5069
  { id: "osd", label: "OSD", icon: "type", order: 18 },
5070
5070
  { id: "stream-broker", label: "Stream Broker", icon: "radio", order: 20 },
5071
5071
  { id: "streaming", label: "Streaming", icon: "video", order: 35 },
5072
+ { id: "ptz", label: "PTZ", icon: "move", order: 40 },
5072
5073
  { id: "pipeline", label: "Detection Pipeline", icon: "cpu", order: 39 },
5073
5074
  { id: "zones", label: "Detection", icon: "shapes", order: 38 },
5074
5075
  { id: "live-stats", label: "Live Stats", icon: "activity", order: 39 },
@@ -5136,6 +5137,9 @@ function hydrateField(field, values) {
5136
5137
  return { ...field, value: items };
5137
5138
  }
5138
5139
  const rawValue = storedValue !== void 0 ? storedValue : defaultValue !== void 0 ? defaultValue : null;
5140
+ if (field.type === "password") {
5141
+ return { ...field, value: "" };
5142
+ }
5139
5143
  const value = field.type === "textarea" && field.isJson && rawValue !== null && typeof rawValue === "object" ? JSON.stringify(rawValue, null, 2) : rawValue;
5140
5144
  const hydrated = { ...field, value };
5141
5145
  return hydrated;
@@ -5222,7 +5226,19 @@ const DecoderSessionConfigSchema = object({
5222
5226
  * on every line so `grep tag=broker:5/high` filters one camera
5223
5227
  * profile cleanly.
5224
5228
  */
5225
- tag: string().optional()
5229
+ tag: string().optional(),
5230
+ /**
5231
+ * Where the session delivers decoded frames (Phase 5 / D9):
5232
+ *
5233
+ * - `'callback'` (default) — the legacy pixel path: decoded frames are
5234
+ * buffered as `DecodedFrame`s and drained via `pullFrames`.
5235
+ * - `'shm'` — the shared-memory frame plane: decoded frames are written
5236
+ * into an OS shared-memory ring and drained as zero-pixel
5237
+ * `FrameHandle`s via `pullHandles`. A session is one mode or the
5238
+ * other — `pullFrames` returns nothing for an `'shm'` session and
5239
+ * `pullHandles` returns nothing for a `'callback'` session.
5240
+ */
5241
+ frameSink: _enum(["callback", "shm"]).default("callback")
5226
5242
  });
5227
5243
  const YAMNET_TO_MACRO = {
5228
5244
  mapping: {
@@ -5952,6 +5968,53 @@ const DecodedFrameSchema = object({
5952
5968
  format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
5953
5969
  timestamp: number()
5954
5970
  });
5971
+ const FrameHandleSchema = object({
5972
+ shmId: string(),
5973
+ slot: number().int().nonnegative(),
5974
+ seq: number().int().nonnegative(),
5975
+ width: number().int().positive(),
5976
+ height: number().int().positive(),
5977
+ format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
5978
+ pts: number(),
5979
+ byteLength: number().int().nonnegative(),
5980
+ nodeId: string(),
5981
+ slotCount: number().int().positive()
5982
+ });
5983
+ const FrameHandleFormatSchema = _enum(["rgb", "bgr", "yuv420", "gray"]);
5984
+ const SubscribeFramesInputSchema = object({
5985
+ brokerId: string(),
5986
+ format: FrameHandleFormatSchema,
5987
+ /**
5988
+ * Optional reader-side cadence hint in frames per second. The broker does
5989
+ * NOT throttle — latest-wins ring reads drop frames implicitly for a slow
5990
+ * consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
5991
+ * the consumer can pace its own `pullFrameHandles` polling.
5992
+ */
5993
+ maxFps: number().positive().optional(),
5994
+ /** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
5995
+ tag: string().optional()
5996
+ });
5997
+ const SubscribeFramesResultSchema = object({
5998
+ /** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
5999
+ subscriptionId: string(),
6000
+ /** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
6001
+ maxFps: number().nonnegative()
6002
+ });
6003
+ const DecodedAudioChunkSchema = object({
6004
+ data: _instanceof(Uint8Array),
6005
+ sampleRate: number().int().positive(),
6006
+ channels: number().int().positive(),
6007
+ timestamp: number()
6008
+ });
6009
+ const SubscribeAudioChunksInputSchema = object({
6010
+ brokerId: string(),
6011
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
6012
+ tag: string().optional()
6013
+ });
6014
+ const SubscribeAudioChunksResultSchema = object({
6015
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
6016
+ subscriptionId: string()
6017
+ });
5955
6018
  const BrokerStatusSchema$1 = _enum(["idle", "connecting", "streaming", "error", "stopped"]);
5956
6019
  const BrokerStatsSchema = object({
5957
6020
  status: BrokerStatusSchema$1,
@@ -6173,9 +6236,76 @@ const RtpSourceSchema = object({
6173
6236
  object({ released: boolean(), refcount: number().int().nonnegative() }),
6174
6237
  { kind: "mutation", auth: "admin" }
6175
6238
  ),
6176
- getBroker: method(
6177
- object({ brokerId: string() }),
6178
- custom()
6239
+ /**
6240
+ * ── Decoded audio-chunk plane (Phase 5 / D9) ──────────────────────
6241
+ *
6242
+ * The serialisable replacement for the live-object `IStreamBroker.
6243
+ * onDecodedAudioChunk` callback path. Unlike the video frame plane,
6244
+ * audio chunks are tiny (a ~500ms PCM window is a few KB) so they ship
6245
+ * their bytes INLINE over tRPC — no shared-memory ring. A consumer:
6246
+ *
6247
+ * 1. `subscribeAudioChunks({ brokerId, tag })` — the broker registers
6248
+ * a per-subscription bounded FIFO queue and returns a
6249
+ * `subscriptionId`.
6250
+ * 2. polls `pullAudioChunks({ subscriptionId, maxCount })` — drains
6251
+ * `DecodedAudioChunk[]` in arrival order (FIFO, no latest-wins
6252
+ * drop: an audio gap is audible / breaks an analysis window). The
6253
+ * queue is generously sized; it only drops its oldest chunk if a
6254
+ * truly stalled consumer lets it overflow.
6255
+ * 3. `unsubscribeAudioChunks({ subscriptionId })` on teardown.
6256
+ */
6257
+ subscribeAudioChunks: method(
6258
+ SubscribeAudioChunksInputSchema,
6259
+ SubscribeAudioChunksResultSchema,
6260
+ { kind: "mutation" }
6261
+ ),
6262
+ pullAudioChunks: method(
6263
+ object({
6264
+ subscriptionId: string(),
6265
+ maxCount: number().int().positive().default(8)
6266
+ }),
6267
+ array(DecodedAudioChunkSchema).readonly()
6268
+ ),
6269
+ unsubscribeAudioChunks: method(
6270
+ object({ subscriptionId: string() }),
6271
+ object({ released: boolean() }),
6272
+ { kind: "mutation" }
6273
+ ),
6274
+ /**
6275
+ * ── Shared-memory frame plane (Phase 5 / D9) ──────────────────────
6276
+ *
6277
+ * The handle-based replacement for the live-object `IStreamBroker.
6278
+ * onDecodedFrame` callback path. A consumer:
6279
+ *
6280
+ * 1. `subscribeFrames({ brokerId, format })` — the broker spins up
6281
+ * (or reuses) a `frameSink: 'shm'` decoder session producing that
6282
+ * `format` and returns a `subscriptionId`.
6283
+ * 2. polls `pullFrameHandles({ subscriptionId, maxCount })` — drains
6284
+ * zero-pixel `FrameHandle[]`; each handle is fed to a
6285
+ * `FrameRingReader` that opens the named shm segment and reads the
6286
+ * pixels back zero-copy.
6287
+ * 3. `unsubscribeFrames({ subscriptionId })` on teardown.
6288
+ *
6289
+ * The broker keeps one shm ring per `(brokerId, format)` actually
6290
+ * requested — no broker-side `sharp` conversion. fps throttling is
6291
+ * implicit (latest-wins ring reads drop frames for a slow consumer).
6292
+ */
6293
+ subscribeFrames: method(
6294
+ SubscribeFramesInputSchema,
6295
+ SubscribeFramesResultSchema,
6296
+ { kind: "mutation" }
6297
+ ),
6298
+ pullFrameHandles: method(
6299
+ object({
6300
+ subscriptionId: string(),
6301
+ maxCount: number().int().positive().default(4)
6302
+ }),
6303
+ array(FrameHandleSchema).readonly()
6304
+ ),
6305
+ unsubscribeFrames: method(
6306
+ object({ subscriptionId: string() }),
6307
+ object({ released: boolean() }),
6308
+ { kind: "mutation" }
6179
6309
  ),
6180
6310
  setPreBufferDuration: method(
6181
6311
  object({ brokerId: string(), seconds: number().min(0).max(30) }),
@@ -7253,6 +7383,34 @@ MotionTriggerStatusSchema.extend({
7253
7383
  }) }
7254
7384
  }
7255
7385
  });
7386
+ object({
7387
+ enabled: boolean(),
7388
+ sensitivity: number(),
7389
+ /** Row-major active-cell grid. Length = gridWidth*gridHeight (see getOptions). */
7390
+ cells: array(boolean()),
7391
+ lastFetchedAt: number()
7392
+ });
7393
+ const MotionZoneOptionsSchema = object({
7394
+ gridWidth: number(),
7395
+ gridHeight: number(),
7396
+ sensitivity: object({ min: number(), max: number(), step: number() })
7397
+ });
7398
+ const MotionZonePatchSchema = object({
7399
+ enabled: boolean().optional(),
7400
+ sensitivity: number().optional(),
7401
+ cells: array(boolean()).optional()
7402
+ });
7403
+ ({
7404
+ deviceTypes: [DeviceType.Camera],
7405
+ methods: {
7406
+ getOptions: method(object({ deviceId: number() }), MotionZoneOptionsSchema),
7407
+ setZone: method(
7408
+ object({ deviceId: number(), patch: MotionZonePatchSchema }),
7409
+ _void(),
7410
+ { kind: "mutation", auth: "admin" }
7411
+ )
7412
+ }
7413
+ });
7256
7414
  const AutotrackTargetTypeSchema = string().describe("Vendor target string (people/vehicle/pet); empty = camera default");
7257
7415
  const PtzAutotrackSettingsSchema = object({
7258
7416
  targetType: AutotrackTargetTypeSchema,
@@ -7341,6 +7499,100 @@ PtzAutotrackStatusSchema.extend({
7341
7499
  }) }
7342
7500
  }
7343
7501
  });
7502
+ const StreamProfileSchema = _enum(["main", "sub", "ext"]);
7503
+ const StreamProfileConfigSchema = object({
7504
+ width: number(),
7505
+ height: number(),
7506
+ codec: _enum(["h264", "h265"]),
7507
+ framerate: number(),
7508
+ bitrate: number(),
7509
+ // kbps
7510
+ bitrateMode: _enum(["vbr", "cbr"]).optional(),
7511
+ encoderProfile: _enum(["high", "main", "baseline"]).optional(),
7512
+ gop: number().optional(),
7513
+ audio: boolean().optional()
7514
+ });
7515
+ object({
7516
+ /** Per-profile current config. A profile absent = the camera doesn't have it. */
7517
+ main: StreamProfileConfigSchema.optional(),
7518
+ sub: StreamProfileConfigSchema.optional(),
7519
+ ext: StreamProfileConfigSchema.optional(),
7520
+ lastFetchedAt: number()
7521
+ });
7522
+ const StreamProfileOptionsSchema = object({
7523
+ resolutions: array(object({ width: number(), height: number() })),
7524
+ codecs: array(_enum(["h264", "h265"])),
7525
+ framerates: array(number()),
7526
+ /** Allowed bitrate values (kbps). Empty if the camera takes a free range. */
7527
+ bitrates: array(number()),
7528
+ /** Optional [min,max] kbps when the camera accepts a continuous range. */
7529
+ bitrateRange: tuple([number(), number()]).optional(),
7530
+ supportsBitrateMode: boolean(),
7531
+ supportsEncoderProfile: boolean(),
7532
+ supportsGop: boolean(),
7533
+ /** Allowed GOP / keyframe-interval range, in seconds — drives the
7534
+ * I-frame-interval selector. Absent when the camera advertises GOP
7535
+ * support but no concrete range (callers then fall back to a free
7536
+ * numeric input). `{ min, max, step }` per the getOptions convention. */
7537
+ gop: object({ min: number(), max: number(), step: number() }).optional()
7538
+ });
7539
+ const StreamParamsOptionsSchema = object({
7540
+ main: StreamProfileOptionsSchema.optional(),
7541
+ sub: StreamProfileOptionsSchema.optional(),
7542
+ ext: StreamProfileOptionsSchema.optional()
7543
+ });
7544
+ const StreamProfilePatchSchema = object({
7545
+ width: number().optional(),
7546
+ height: number().optional(),
7547
+ codec: _enum(["h264", "h265"]).optional(),
7548
+ framerate: number().optional(),
7549
+ bitrate: number().optional(),
7550
+ bitrateMode: _enum(["vbr", "cbr"]).optional(),
7551
+ encoderProfile: _enum(["high", "main", "baseline"]).optional(),
7552
+ gop: number().optional(),
7553
+ audio: boolean().optional()
7554
+ });
7555
+ ({
7556
+ deviceTypes: [DeviceType.Camera],
7557
+ methods: {
7558
+ getOptions: method(
7559
+ object({ deviceId: number() }),
7560
+ StreamParamsOptionsSchema
7561
+ ),
7562
+ setProfile: method(
7563
+ object({
7564
+ deviceId: number(),
7565
+ profile: StreamProfileSchema,
7566
+ patch: StreamProfilePatchSchema
7567
+ }),
7568
+ _void(),
7569
+ { kind: "mutation", auth: "admin" }
7570
+ ),
7571
+ /**
7572
+ * Build the `ConfigUISchema` (admin-ui `ConfigFormBuilder` input
7573
+ * shape) for this camera's stream-encoder settings — one section per
7574
+ * profile (main / sub / ext) with the resolution / codec / framerate
7575
+ * / bitrate / bitrate-mode / encoder-profile / GOP controls the
7576
+ * firmware actually exposes.
7577
+ *
7578
+ * Driven by `getOptions` (camera-probed availability) + `getStatus`
7579
+ * (current per-profile config); each field's `default` is seeded
7580
+ * from the live config so the form renders the camera state in one
7581
+ * pass. Returns `null` when the camera exposes no configurable
7582
+ * stream property — the renderer then shows the unsupported message.
7583
+ *
7584
+ * Output is `z.unknown().nullable()` — the same convention every
7585
+ * other `ConfigUISchema`-returning cap method uses (`device-ops`,
7586
+ * `device-manager`); `ConfigUISchema` is a TS-only type with no
7587
+ * companion Zod schema, and a concrete object would collapse
7588
+ * unrelated AppRouter branches to `unknown` during codegen.
7589
+ */
7590
+ getConfigSchema: method(
7591
+ object({ deviceId: number() }),
7592
+ unknown().nullable()
7593
+ )
7594
+ }
7595
+ });
7344
7596
  object({
7345
7597
  on: boolean(),
7346
7598
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
@@ -8282,6 +8534,83 @@ const adminUiCapability = {
8282
8534
  getVersion: method(_void(), VersionOutputSchema)
8283
8535
  }
8284
8536
  };
8537
+ const MethodAccessSchema = _enum(["view", "create", "delete"]);
8538
+ const AllowedProviderSchema = union([literal("*"), array(string())]);
8539
+ const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
8540
+ const CapScopeSchema = _enum(["device", "system"]);
8541
+ const TokenScopeSchema = discriminatedUnion("type", [
8542
+ object({
8543
+ type: literal("category"),
8544
+ target: CapScopeSchema,
8545
+ access: array(MethodAccessSchema).min(1)
8546
+ }),
8547
+ object({
8548
+ type: literal("capability"),
8549
+ target: string(),
8550
+ access: array(MethodAccessSchema).min(1)
8551
+ }),
8552
+ object({
8553
+ type: literal("addon"),
8554
+ target: string(),
8555
+ access: array(MethodAccessSchema).min(1)
8556
+ }),
8557
+ object({
8558
+ type: literal("device"),
8559
+ /**
8560
+ * One or more deviceIds (serialised as strings for wire-format
8561
+ * consistency with the rest of the union). Matcher accepts if
8562
+ * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
8563
+ * of one scope-per-device when granting access to a set of cameras.
8564
+ */
8565
+ targets: array(string()).min(1),
8566
+ access: array(MethodAccessSchema).min(1)
8567
+ })
8568
+ ]);
8569
+ object({
8570
+ id: string(),
8571
+ username: string(),
8572
+ passwordHash: string(),
8573
+ /**
8574
+ * Admin bypass. When true, the middleware skips the scope-access
8575
+ * check entirely. There is no other axis of privilege; the legacy
8576
+ * role enum collapsed onto this boolean in v2.
8577
+ */
8578
+ isAdmin: boolean().default(false),
8579
+ allowedProviders: AllowedProviderSchema,
8580
+ allowedDevices: AllowedDevicesSchema,
8581
+ /**
8582
+ * Scopes granted to this user. Admins bypass; their `scopes` is
8583
+ * ignored. Non-admins without scopes are locked out of every
8584
+ * protected call.
8585
+ */
8586
+ scopes: array(TokenScopeSchema).default([]),
8587
+ createdAt: number(),
8588
+ updatedAt: number()
8589
+ });
8590
+ object({
8591
+ id: string(),
8592
+ label: string(),
8593
+ isAdmin: boolean().default(false),
8594
+ allowedProviders: AllowedProviderSchema,
8595
+ allowedDevices: AllowedDevicesSchema,
8596
+ tokenHash: string(),
8597
+ tokenPrefix: string(),
8598
+ createdAt: number(),
8599
+ lastUsedAt: number().optional()
8600
+ });
8601
+ object({
8602
+ id: string(),
8603
+ userId: string(),
8604
+ name: string(),
8605
+ tokenHash: string(),
8606
+ tokenPrefix: string(),
8607
+ scopes: array(TokenScopeSchema),
8608
+ // SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
8609
+ // schema accepts both `null` (read from disk) and `undefined` (in-memory).
8610
+ expiresAt: number().nullish(),
8611
+ lastUsedAt: number().nullish(),
8612
+ createdAt: number()
8613
+ });
8285
8614
  const SsoBridgeClaimsSchema = object({
8286
8615
  userId: string(),
8287
8616
  username: string(),
@@ -8297,7 +8626,18 @@ const SsoBridgeClaimsSchema = object({
8297
8626
  * JWT WITHOUT verifying the signature — the hub re-verifies on every
8298
8627
  * inbound call so trust still rests with the signing hub.
8299
8628
  */
8300
- hubUrl: string().optional()
8629
+ hubUrl: string().optional(),
8630
+ /** Permission scopes baked into the token. Set by the OAuth
8631
+ * account-linking grant; absent on ordinary SSO-login tokens. */
8632
+ scopes: array(TokenScopeSchema).optional(),
8633
+ /** OAuth authorization-code binding — set only on `oauth-code` tokens. */
8634
+ redirectUri: string().optional(),
8635
+ integrationId: string().optional(),
8636
+ /** JWT ID — unique per issued code; consumed-set enforces single-use. */
8637
+ jti: string().optional(),
8638
+ /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
8639
+ * tokens so the verify path can check the session is not revoked. */
8640
+ sessionId: string().optional()
8301
8641
  });
8302
8642
  ({
8303
8643
  methods: {
@@ -8314,6 +8654,23 @@ const SsoBridgeClaimsSchema = object({
8314
8654
  )
8315
8655
  }
8316
8656
  });
8657
+ const OauthIntegrationDescriptorSchema = object({
8658
+ /** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
8659
+ integrationId: string(),
8660
+ /** Human label rendered on the consent page. */
8661
+ displayName: string(),
8662
+ /** Scopes baked into every token issued for this integration. */
8663
+ requestedScopes: array(TokenScopeSchema),
8664
+ /** Allowed redirect_uri prefixes. /api/oauth2/authorize rejects any
8665
+ * redirect_uri that does not start with one of these. Required —
8666
+ * an empty list means the integration can never complete linking. */
8667
+ allowedRedirectPrefixes: array(string()).min(1)
8668
+ });
8669
+ ({
8670
+ methods: {
8671
+ getDescriptor: method(_void(), OauthIntegrationDescriptorSchema)
8672
+ }
8673
+ });
8317
8674
  const PasskeySummarySchema = object({
8318
8675
  credentialId: string(),
8319
8676
  label: string(),
@@ -8594,21 +8951,30 @@ const AddonPageDeclarationSchema = object({
8594
8951
  });
8595
8952
  const WidgetHostEnum = _enum(["device-tab", "dashboard", "integration-detail"]);
8596
8953
  const WidgetSizeEnum = _enum(["xs", "sm", "md", "lg", "xl"]);
8954
+ const WidgetRemoteSchema = object({
8955
+ remoteName: string(),
8956
+ exposedModule: string(),
8957
+ componentKey: string().optional()
8958
+ });
8597
8959
  const WidgetMetadataSchema = object({
8598
- /** Stable id within the addon — kebab-case. */
8599
- stableId: string(),
8960
+ // ── UiContribution core (kind:'remote') ──────────────────────────
8961
+ /** Primary host tab — `'dashboard'`, `'device-tab'`, or a device-detail tab id. */
8962
+ tab: string(),
8963
+ /** Optional sub-tab within `tab`. */
8964
+ subTab: string().optional(),
8600
8965
  /** Operator-facing label. */
8601
8966
  label: string(),
8967
+ /** Ordering within `(tab, subTab)`, ascending. */
8968
+ order: number().optional(),
8969
+ /** Always `'remote'` — a widget is a Module Federation remote. */
8970
+ kind: literal("remote"),
8971
+ /** MF remote descriptor. */
8972
+ remote: WidgetRemoteSchema,
8973
+ // ── Widget-only metadata ─────────────────────────────────────────
8974
+ /** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
8975
+ stableId: string(),
8602
8976
  description: string().optional(),
8603
8977
  icon: string().optional(),
8604
- /**
8605
- * Module Federation remote name — must match the `name` field on the
8606
- * widget addon's `federation()` plugin config. Used by the host's
8607
- * `<WidgetRegistryProvider>` to call `loadRemote('<remoteName>/widgets')`.
8608
- * Conventionally `addon_<addonid>_widgets` (snake_case; MF names
8609
- * cannot contain hyphens).
8610
- */
8611
- remoteName: string(),
8612
8978
  /**
8613
8979
  * Bundle filename inside the addon's `dist/` dir served at
8614
8980
  * `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
@@ -8617,9 +8983,9 @@ const WidgetMetadataSchema = object({
8617
8983
  * cache-buster URL without a separate filesystem stat.
8618
8984
  */
8619
8985
  bundle: string(),
8620
- /** Where the widget makes sense to render. */
8986
+ /** Every host the widget supports. The picker filters on this set. */
8621
8987
  hosts: array(WidgetHostEnum).readonly(),
8622
- /** Required props the host must supply. Validated at <WidgetSlot> mount. */
8988
+ /** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
8623
8989
  requires: object({
8624
8990
  deviceContext: boolean().default(false),
8625
8991
  integrationContext: boolean().default(false)
@@ -8690,6 +9056,16 @@ const InvokeReplyEnvelopeSchema = object({
8690
9056
  invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
8691
9057
  }
8692
9058
  });
9059
+ const ShmRingStatsSchema = object({
9060
+ sessionId: string(),
9061
+ slotCount: number().int(),
9062
+ slotByteLength: number().int(),
9063
+ segmentBytes: number().int(),
9064
+ budgetMb: number().int(),
9065
+ framesWritten: number().int(),
9066
+ getFrameHits: number().int(),
9067
+ getFrameMisses: number().int()
9068
+ });
8693
9069
  ({
8694
9070
  methods: {
8695
9071
  // ── Discovery ─────────────────────────────────────────────────
@@ -8717,10 +9093,27 @@ const InvokeReplyEnvelopeSchema = object({
8717
9093
  url: string()
8718
9094
  }), _void()),
8719
9095
  // ── Output — polling-based frame retrieval ────────────────────
9096
+ // `pullFrames` drains the pixel `DecodedFrame[]` of a `frameSink:
9097
+ // 'callback'` session. `pullHandles` (Phase 5 / D9) drains the
9098
+ // zero-pixel `FrameHandle[]` of a `frameSink: 'shm'` session — the
9099
+ // broker hands each handle to a `FrameRingReader` that opens the
9100
+ // named segment and reads the pixels back zero-copy. A session is
9101
+ // one mode or the other; the unmatched method returns an empty
9102
+ // array.
8720
9103
  pullFrames: method(object({
8721
9104
  sessionId: string(),
8722
9105
  maxCount: number().default(1)
8723
9106
  }), array(DecodedFrameSchema)),
9107
+ pullHandles: method(object({
9108
+ sessionId: string(),
9109
+ maxCount: number().default(1)
9110
+ }), array(FrameHandleSchema)),
9111
+ // ── Frame fetch (Phase 5 / D9 downstream access) ──────────────
9112
+ // Read the pixels a FrameHandle refers to from this node's shm ring.
9113
+ // Returns null when the slot was already recycled (latest-wins).
9114
+ getFrame: method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()),
9115
+ // shm ring usage stats for a session.
9116
+ getShmStats: method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()),
8724
9117
  // ── Control ───────────────────────────────────────────────────
8725
9118
  updateConfig: method(object({
8726
9119
  sessionId: string(),
@@ -9888,8 +10281,8 @@ const DevicePersistConfigPayloadSchema = object({
9888
10281
  /**
9889
10282
  * Return the addon ids that declared a wrapper provider for `capName`.
9890
10283
  * Backs the device-bindings UI's wrapper-picker dropdown. Entries are
9891
- * sourced from `CapabilityRegistry.wrapperProviders`, populated at
9892
- * `ProviderRegistration.kind === 'wrapper'` time.
10284
+ * sourced from `CapabilityRegistry.wrapperProviders`, populated when
10285
+ * the cap definition declares `kind: 'wrapper'`.
9893
10286
  */
9894
10287
  listWrappersForCap: method(
9895
10288
  object({ capName: string() }),
@@ -10979,6 +11372,18 @@ const PtzMoveCommandSchema = object({
10979
11372
  zoom: number().optional(),
10980
11373
  speed: number().optional()
10981
11374
  });
11375
+ PtzPositionSchema.extend({ autofocus: boolean() });
11376
+ const PtzOptionsSchema = object({
11377
+ hasPan: boolean(),
11378
+ hasTilt: boolean(),
11379
+ hasZoom: boolean(),
11380
+ supportsPresets: boolean(),
11381
+ /** Max number of named presets the camera supports, when known. */
11382
+ maxPresets: number().optional(),
11383
+ /** Whether the camera exposes a controllable autofocus toggle
11384
+ * (boolean `hasX` per the getOptions availability convention). */
11385
+ hasAutofocus: boolean()
11386
+ });
10982
11387
  ({
10983
11388
  deviceTypes: [DeviceType.Camera],
10984
11389
  methods: {
@@ -11006,6 +11411,20 @@ const PtzMoveCommandSchema = object({
11006
11411
  _void(),
11007
11412
  { kind: "mutation" }
11008
11413
  ),
11414
+ savePreset: method(
11415
+ object({ deviceId: number(), presetId: string(), name: string() }),
11416
+ _void(),
11417
+ { kind: "mutation", auth: "admin" }
11418
+ ),
11419
+ deletePreset: method(
11420
+ object({ deviceId: number(), presetId: string() }),
11421
+ _void(),
11422
+ { kind: "mutation", auth: "admin" }
11423
+ ),
11424
+ getOptions: method(
11425
+ object({ deviceId: number() }),
11426
+ PtzOptionsSchema
11427
+ ),
11009
11428
  goHome: method(
11010
11429
  object({ deviceId: number() }),
11011
11430
  _void(),
@@ -11020,6 +11439,13 @@ const PtzMoveCommandSchema = object({
11020
11439
  getPosition: method(
11021
11440
  object({ deviceId: number() }),
11022
11441
  PtzPositionSchema
11442
+ ),
11443
+ /** Toggle the camera's autofocus. Only meaningful when
11444
+ * `getOptions().hasAutofocus` is true. */
11445
+ setAutofocus: method(
11446
+ object({ deviceId: number(), enabled: boolean() }),
11447
+ _void(),
11448
+ { kind: "mutation" }
11023
11449
  )
11024
11450
  }
11025
11451
  });
@@ -11955,83 +12381,6 @@ const MeshStatusSchema = object({
11955
12381
  // tabs driven by this cap.
11956
12382
  }
11957
12383
  });
11958
- const MethodAccessSchema = _enum(["view", "create", "delete"]);
11959
- const AllowedProviderSchema = union([literal("*"), array(string())]);
11960
- const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
11961
- const CapScopeSchema = _enum(["device", "system"]);
11962
- const TokenScopeSchema = discriminatedUnion("type", [
11963
- object({
11964
- type: literal("category"),
11965
- target: CapScopeSchema,
11966
- access: array(MethodAccessSchema).min(1)
11967
- }),
11968
- object({
11969
- type: literal("capability"),
11970
- target: string(),
11971
- access: array(MethodAccessSchema).min(1)
11972
- }),
11973
- object({
11974
- type: literal("addon"),
11975
- target: string(),
11976
- access: array(MethodAccessSchema).min(1)
11977
- }),
11978
- object({
11979
- type: literal("device"),
11980
- /**
11981
- * One or more deviceIds (serialised as strings for wire-format
11982
- * consistency with the rest of the union). Matcher accepts if
11983
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
11984
- * of one scope-per-device when granting access to a set of cameras.
11985
- */
11986
- targets: array(string()).min(1),
11987
- access: array(MethodAccessSchema).min(1)
11988
- })
11989
- ]);
11990
- object({
11991
- id: string(),
11992
- username: string(),
11993
- passwordHash: string(),
11994
- /**
11995
- * Admin bypass. When true, the middleware skips the scope-access
11996
- * check entirely. There is no other axis of privilege; the legacy
11997
- * role enum collapsed onto this boolean in v2.
11998
- */
11999
- isAdmin: boolean().default(false),
12000
- allowedProviders: AllowedProviderSchema,
12001
- allowedDevices: AllowedDevicesSchema,
12002
- /**
12003
- * Scopes granted to this user. Admins bypass; their `scopes` is
12004
- * ignored. Non-admins without scopes are locked out of every
12005
- * protected call.
12006
- */
12007
- scopes: array(TokenScopeSchema).default([]),
12008
- createdAt: number(),
12009
- updatedAt: number()
12010
- });
12011
- object({
12012
- id: string(),
12013
- label: string(),
12014
- isAdmin: boolean().default(false),
12015
- allowedProviders: AllowedProviderSchema,
12016
- allowedDevices: AllowedDevicesSchema,
12017
- tokenHash: string(),
12018
- tokenPrefix: string(),
12019
- createdAt: number(),
12020
- lastUsedAt: number().optional()
12021
- });
12022
- object({
12023
- id: string(),
12024
- userId: string(),
12025
- name: string(),
12026
- tokenHash: string(),
12027
- tokenPrefix: string(),
12028
- scopes: array(TokenScopeSchema),
12029
- // SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
12030
- // schema accepts both `null` (read from disk) and `undefined` (in-memory).
12031
- expiresAt: number().nullish(),
12032
- lastUsedAt: number().nullish(),
12033
- createdAt: number()
12034
- });
12035
12384
  const UserSummarySchema = object({
12036
12385
  id: string(),
12037
12386
  username: string(),
@@ -12104,6 +12453,16 @@ const CreateScopedTokenResultSchema = object({
12104
12453
  token: string(),
12105
12454
  record: ScopedTokenSummarySchema
12106
12455
  });
12456
+ const OauthSessionSummarySchema = object({
12457
+ id: string(),
12458
+ userId: string(),
12459
+ username: string(),
12460
+ integrationId: string(),
12461
+ scopes: array(TokenScopeSchema),
12462
+ createdAt: number(),
12463
+ lastUsedAt: number(),
12464
+ revokedAt: number().nullable()
12465
+ });
12107
12466
  const TotpSetupResultSchema = object({
12108
12467
  secret: string(),
12109
12468
  otpauthUrl: string()
@@ -12179,6 +12538,66 @@ const TotpStatusSchema = object({
12179
12538
  object({ userId: string(), code: string() }),
12180
12539
  object({ valid: boolean() }),
12181
12540
  { kind: "mutation", access: "view" }
12541
+ ),
12542
+ // ── OAuth account-linking grant ────────────────────────────────
12543
+ //
12544
+ // Core's /oauth2/* endpoints delegate here. Tokens are sso-bridge
12545
+ // JWTs (kinds oauth-code / oauth-access / oauth-refresh) and ALWAYS
12546
+ // carry isAdmin:false — the operator login proves hub control; the
12547
+ // issued token is minimal (device scope only).
12548
+ oauthIssueCode: method(
12549
+ object({
12550
+ integrationId: string(),
12551
+ userId: string(),
12552
+ username: string(),
12553
+ scopes: array(TokenScopeSchema),
12554
+ redirectUri: string(),
12555
+ hubUrl: string()
12556
+ }),
12557
+ object({ code: string() }),
12558
+ { kind: "mutation", access: "create" }
12559
+ ),
12560
+ oauthExchangeCode: method(
12561
+ object({ code: string(), redirectUri: string() }),
12562
+ object({
12563
+ accessToken: string(),
12564
+ refreshToken: string(),
12565
+ expiresIn: number()
12566
+ }).nullable(),
12567
+ { kind: "mutation", access: "view" }
12568
+ ),
12569
+ oauthRefresh: method(
12570
+ object({ refreshToken: string() }),
12571
+ object({
12572
+ accessToken: string(),
12573
+ refreshToken: string(),
12574
+ expiresIn: number()
12575
+ }).nullable(),
12576
+ { kind: "mutation", access: "view" }
12577
+ ),
12578
+ oauthVerifyAccessToken: method(
12579
+ object({ token: string() }),
12580
+ object({
12581
+ userId: string(),
12582
+ username: string(),
12583
+ scopes: array(TokenScopeSchema)
12584
+ }).nullable(),
12585
+ { access: "view" }
12586
+ ),
12587
+ // ── OAuth linked-session management (Phase D) ──────────────────
12588
+ //
12589
+ // The admin UI lists active account-linking sessions and revokes
12590
+ // them; revocation makes the linked integration's tokens fail
12591
+ // verification immediately.
12592
+ listOauthSessions: method(
12593
+ _void(),
12594
+ array(OauthSessionSummarySchema),
12595
+ { auth: "admin" }
12596
+ ),
12597
+ revokeOauthSession: method(
12598
+ object({ id: string() }),
12599
+ object({ success: boolean() }),
12600
+ { kind: "mutation", auth: "admin", access: "delete" }
12182
12601
  )
12183
12602
  }
12184
12603
  });
@@ -12395,6 +12814,19 @@ const RenameNodeResultSchema = object({
12395
12814
  record(string(), ClusterAddonStatusEntrySchema),
12396
12815
  { auth: "admin" }
12397
12816
  ),
12817
+ getCapUsageGraph: method(
12818
+ object({
12819
+ windowSeconds: number().int().positive().max(300).default(60)
12820
+ }),
12821
+ array(object({
12822
+ callerAddonId: string(),
12823
+ providerAddonId: string(),
12824
+ capName: string(),
12825
+ callsPerMin: number(),
12826
+ lastCallAtMs: number()
12827
+ })).readonly(),
12828
+ { auth: "admin" }
12829
+ ),
12398
12830
  /**
12399
12831
  * Direct per-node addon listing — calls `$agent.status` on the target
12400
12832
  * node (or returns the hub registry for `nodeId === 'hub'`) and surfaces